* docs: bearer-token auth design spec * docs: harden bearer-auth spec with security review findings * feat(bearer): opt-in M2M bearer-token authentication Adds an opt-in Authorization: Bearer <jwt> path for machine-to-machine clients. Replaces and supersedes the broken approach in PR #93 (synthetic-session that omitted user_identifier and skipped ID-token rejection / replay-protection-semantics / kid-pinning / etc.). Design Two auth entrypoints feed one shared post-auth pipeline: cookie path ─┐ ├── forwardAuthorized(rw, req, *principal) bearer path ─┘ (roles/groups, header injection, security headers, cookie strip, forward) buildPrincipalFromSession and buildPrincipalFromBearerToken produce the same `principal` value type. forwardAuthorized is session-agnostic and runs the existing post-auth work; processAuthorizedRequest now wraps it with the session-specific concerns (backchannel-logout, dirty/Save). The cookie path's behaviour is byte-identical to before this PR; the existing test suite passes unmodified. Security hardening baked into the bearer path - Audience MANDATORY. Startup fails when EnableBearerAuth=true and Audience is empty. - BearerIdentifierClaim defaults to "sub"; "email" is rejected at startup to avoid the unverified-email spoofing footgun. Cookie path's UserIdentifierClaim is unaffected and still defaults to "email". - ID tokens explicitly rejected via the existing detectTokenType helper (nonce, typ=at+jwt, token_use, scope, aud-vs-clientID heuristics); belt-and-braces nonce/token_use=id rejection on top. - alg pinned to asymmetric allowlist (RS/PS/ES 256/384/512) BEFORE JWKS fetch, blocking alg=none and alg=HS* probes from amplifying into upstream calls. - kid length capped at 256 bytes and charset-restricted before JWKS fetch, blocking pathological-kid JWKS amplification. - Multi-audience tokens require azp == clientID. - iat upper-age bound (MaxTokenAgeSeconds, default 24h) bounds clock- manipulation and forever-token abuse. - Identifier sanitization: length cap, control-char + bidi-override + delimiter (, ; =) rejection. - Per-IP failure throttle: configurable threshold/window/penalty; returns 429 + Retry-After. Limits offline-guessing-style attacks and protects the shared rate-limiter / JWKS endpoint. - JTI replay marking suppressed via new internal verifyOpts {skipReplayMarking} so the same bearer can be reused until exp; the blacklist Get stays active so RevokeToken still terminates a bearer token immediately. The existing exported VerifyToken interface is unchanged so all mocks continue to work. - Cookie wins by default when both bearer and cookie are present (safer against browser/extension/proxy bearer injection). Operator can flip via BearerOverridesCookie. - Authorization header stripped on forward by default; also stripped on excluded URLs so the token can't leak into health/metrics downstream logs. - Optional RFC 7662 introspection via existing requireTokenIntrospection. Introspection-endpoint failure returns 503 (distinguishes infra from token rejection). - 401s use RFC 6750 WWW-Authenticate hints (toggleable). Failure reason is logged at debug; raw tokens are never logged. Implementation - principal.go: pure-data principal type and buildPrincipalFromSession. - bearer_auth.go: alg/kid pin, classifier, identifier sanitization, multi-aud azp gate, iat age check, per-IP failure tracker, handleBearerRequest, buildPrincipalFromBearerToken. - token_manager.go: VerifyToken now wraps a new verifyTokenWithOpts that accepts internal-only verifyOpts. Existing callers, the TokenVerifier interface, and all mocks unchanged. - middleware.go: extracted forwardAuthorized from processAuthorizedRequest; wired bearer detection after init wait + after bypass; excluded-URL Authorization strip when bearer enabled. - settings.go: ten new config fields with defaults applied in CreateConfig. - main.go: startup validation for audience + identifier-claim guard; bearer failure tracker init. Tests - bearer_auth_test.go: table-driven helper tests for every new component (parseBearerJOSEHeader, sanitizeBearerIdentifier, resolveBearerIdentifier, enforceMultiAudienceAzp, enforceIatAge, bearerFailureTracker, detectBearerToken). Integration tests through ServeHTTP covering happy path, ID-token rejection, alg=none rejection, oversized kid, multi-aud with/without azp, iat-too-old, bidi identifier, replay (100x reuse), 429 throttle trip, excluded-URL strip, roles gate, cookie-wins precedence, BearerOverridesCookie, oversized token, malformed JWT, feature-off pass-through. Startup validation for audience- required and email-identifier-rejected. - All existing tests pass unmodified (cookie-path regression). - go vet clean. golangci-lint clean (0 issues). Race detector clean on bearer tests. Documentation - README.md: bearer auth section with security highlights and config snippet; doc link in the index. - .traefik.yml: commented config block exposing every bearer knob. - docs/CONFIGURATION.md: new subsection with full parameter table. - docs/BEARER_AUTH.md: threat model, hardening matrix, failure response table, operational guidance, known follow-ups. - docs/superpowers/specs/2026-05-18-bearer-token-auth-design.md: design spec + security-review hardening history. * fix(cache): redact raw cache keys in debug logs (CodeQL go/clear-text-logging) CodeQL flagged 9 high-severity alerts (go/clear-text-logging) where the in-memory cache and the hybrid L1+L2 backend printed `key=%s` at debug. Cache callers (token cache, blacklist, introspection cache) pass raw access / refresh / id tokens as cache keys, so any debug-enabled deployment would write them to log streams. Pre-existing issue. CodeQL started flagging it on this PR because the new bearer-auth path adds a data-flow source (req.Header.Get("Authorization")) that reaches the existing logging sinks via the same cache. The cookie path had the same risk but wasn't tracked as taint by CodeQL. Fix: hash the key (SHA-256[:8] hex) before printing. Same approach the bearer-auth logger uses for principal identifiers (spec §13). Doesn't change cache semantics — same key still produces the same hash, so debug correlation across log lines is preserved without exposing the raw value. Touches both affected packages: - internal/cache/cache.go (2 sites: Set + LRU eviction) - internal/cache/backends/hybrid.go (12 sites: L1/L2 read/write/fallback) New helper `redactKey` colocated with each package (unexported, package-local) keeps the change blast radius narrow. Tests green; lint clean. * docs(bearer): how to obtain bearer tokens from the OIDC provider Adds a section walking operators through the OAuth 2.0 client_credentials flow (RFC 6749 §4.4) and the JWT bearer assertion alternative (RFC 7523), with a worked Auth0-shape curl example, a per-provider quick reference (Auth0, Okta, Keycloak, Entra v2, Cognito, GitLab, Google), operational notes (token TTL, caching, JWKS rotation, revocation, scope vs audience, secret hygiene), and a three-line validation loop. Most common operator confusion: "I enabled the feature but tokens get 401'd" — almost always missing or wrong audience. The new section makes the audience-matching requirement loud, with per-provider parameter names so people don't have to dig through IdP docs. Locations: - docs/BEARER_AUTH.md — full section under "Quick start" - README.md — short snippet + deep link
21 KiB
Configuration Reference
Complete reference for all Traefik OIDC middleware configuration options.
Table of Contents
- Required Parameters
- Client Authentication
- Optional Parameters
- Security Options
- Session Management
- Access Control
- Headers Configuration
- Security Headers
- Scope Configuration
- Advanced Options
Required Parameters
| Parameter | Type | Description | Example |
|---|---|---|---|
providerURL |
string | Base URL of the OIDC provider | https://accounts.google.com |
clientID |
string | OAuth 2.0 client identifier | 1234567890.apps.googleusercontent.com |
clientSecret |
string | OAuth 2.0 client secret. Required when clientAuthMethod is unset, client_secret_post, or client_secret_basic. Optional when clientAuthMethod: private_key_jwt. |
your-client-secret |
sessionEncryptionKey |
string | Key for encrypting session data (min 32 bytes) | your-32-byte-encryption-key-here |
callbackURL |
string | Path where provider redirects after authentication | /oauth2/callback |
Basic Configuration Example
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-auth
spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: your-client-id.apps.googleusercontent.com
clientSecret: your-client-secret
sessionEncryptionKey: your-32-byte-encryption-key-here
callbackURL: /oauth2/callback
Client Authentication
The middleware supports three client authentication methods at the token and
revocation endpoints. The default is client_secret_post (current behavior);
private_key_jwt is opt-in and backwards compatible.
| Method | Default | Description |
|---|---|---|
client_secret_post |
yes | client_id + client_secret in the request body. |
client_secret_basic |
no | RFC 6749 §2.3.1 — client_id + client_secret in the Authorization: Basic header (form-urlencoded then base64); not in the body. |
private_key_jwt |
no | RFC 7523 §2.2 — plugin signs a short-lived JWT with a private key and sends it as client_assertion. |
Select via clientAuthMethod:
clientAuthMethod: private_key_jwt
client_secret_post
Default. The plugin sends client_id and client_secret as form parameters
in the token / revocation request body. No additional configuration required.
private_key_jwt
Asymmetric client authentication per RFC 7523 §2.2. Use this when your IdP enforces short secret TTLs, when policy mandates secretless clients, or when you want to avoid distributing a shared secret to the proxy.
For each token / revocation request the plugin builds a JWS with:
iss=sub=clientIDaud= token endpoint URLiat= now,exp= now + 60sjti= random hex per requestkidheader =clientAssertionKeyID
Required fields:
| Parameter | Type | Default | Description |
|---|---|---|---|
clientAuthMethod |
string | client_secret_post |
Set to private_key_jwt. |
clientAssertionPrivateKey |
string | none | Inline PEM private key. Mutually exclusive with clientAssertionKeyPath. PKCS#8, PKCS#1, and SEC1 formats accepted. |
clientAssertionKeyPath |
string | none | Path to PEM private key on disk. Mutually exclusive with clientAssertionPrivateKey. |
clientAssertionKeyID |
string | none | kid header inserted in the JWS. Must match the public key registered with the IdP. |
clientAssertionAlg |
string | RS256 |
One of RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512. |
When clientAuthMethod: private_key_jwt, clientSecret is optional.
Example — inline PEM:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-auth
spec:
plugin:
traefikoidc:
providerURL: https://idp.example.com
clientID: my-client-id
sessionEncryptionKey: your-32-byte-encryption-key-here
callbackURL: /oauth2/callback
clientAuthMethod: private_key_jwt
clientAssertionKeyID: key-2026-01
clientAssertionAlg: RS256
clientAssertionPrivateKey: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7VJTUt9Us8cKj
MZj4ev7QnMa1mYV3Kx1jRkH5YwXQ7N2J2j8K5pP6h0oZmXq1yQv4r8wZb3sH9D2k
... (truncated) ...
-----END PRIVATE KEY-----
Example — key on disk:
clientAuthMethod: private_key_jwt
clientAssertionKeyPath: /etc/traefik/oidc/client-key.pem
clientAssertionKeyID: key-2026-01
clientAssertionAlg: RS256
Generating an RS256 key with OpenSSL:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
-out client-key.pem
openssl rsa -in client-key.pem -pubout -out client-pub.pem
Register client-pub.pem (or its JWK form) with your IdP under the same
kid you set in clientAssertionKeyID.
Notes:
- The private key is parsed once at plugin startup. Key rotation requires a Traefik reload.
- Assertion lifetime is fixed at 60 seconds.
- A fresh random
jtiis generated per request. - The
audclaim is the token endpoint URL (from discovery). - Tracking issue: #135.
client_secret_basic
Per RFC 6749 §2.3.1, the plugin sends the client credentials
in an Authorization: Basic header instead of the body. Both halves
(client_id, client_secret) are form-urlencoded individually, joined with
a colon, then base64-encoded. Use this when your IdP requires Basic auth at
the token endpoint and rejects credentials in the body.
clientAuthMethod: client_secret_basic
clientID: your-client-id
clientSecret: your-client-secret
Optional Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
logoutURL |
string | callbackURL + "/logout" |
Path for logout requests |
postLogoutRedirectURI |
string | / |
Redirect URL after logout |
logLevel |
string | info |
Logging verbosity (debug, info, error) |
forceHTTPS |
bool | true |
Force HTTPS for redirect URIs (set false only for plaintext HTTP local dev) |
rateLimit |
int | 100 |
Maximum requests per second |
excludedURLs |
[]string | none | Paths that bypass authentication |
revocationURL |
string | auto-discovered | Token revocation endpoint |
oidcEndSessionURL |
string | auto-discovered | Provider's end session endpoint |
enablePKCE |
bool | false |
Enable PKCE for authorization code flow |
minimalHeaders |
bool | false |
Reduce forwarded headers |
clientAuthMethod |
string | client_secret_post |
Client authentication method at token/revocation endpoints. One of client_secret_post, client_secret_basic, private_key_jwt. See Client Authentication. |
clientAssertionPrivateKey |
string | none | Inline PEM private key for private_key_jwt. Mutually exclusive with clientAssertionKeyPath. PKCS#8 / PKCS#1 / SEC1. |
clientAssertionKeyPath |
string | none | Path to PEM private key on disk for private_key_jwt. Mutually exclusive with clientAssertionPrivateKey. |
clientAssertionKeyID |
string | none | kid header for private_key_jwt assertions. Required when clientAuthMethod: private_key_jwt. |
clientAssertionAlg |
string | RS256 |
Signing algorithm for private_key_jwt. One of RS256/384/512, PS256/384/512, ES256/384/512. |
TLS Termination at Load Balancer
forceHTTPS defaults to true, so redirect URIs always use https://. This is
the correct default behind any TLS-terminating load balancer (AWS ALB, Google
Cloud LB, Azure App Gateway) — X-Forwarded-Proto cannot be trusted (ALB may
overwrite it).
Set forceHTTPS: false only when you serve OIDC over plaintext HTTP (local
dev). Otherwise leave it at default.
Streaming Endpoints (SSE and WebSocket)
The middleware automatically bypasses the OIDC redirect for two request kinds that browsers cannot follow a 302 on:
| Bypass | Triggered by |
|---|---|
| Server-Sent Events (SSE) | Accept: text/event-stream |
| WebSocket upgrade | Upgrade: websocket + Connection: upgrade (RFC 6455) |
These requests do not require any explicit configuration — they are handled implicitly. However, the bypass is not unauthenticated:
- A valid, encrypted session cookie is required. Requests without one are rejected (the connection cannot proceed to the backend).
- The session cookie is sealed with
sessionEncryptionKey, so theauthenticatedflag cannot be forged. - Validation is cookie-only — no JWK fetch / signature verification — so streaming endpoints keep working when the OIDC provider is briefly unavailable.
- The user identifier from the session is forwarded as
X-Forwarded-User(andX-Auth-Request-UserunlessminimalHeaders: true).
For browser clients, the user must complete the normal OIDC flow on a regular HTTP page first; the resulting session cookie is then reused on the SSE / WebSocket connection.
Security Options
Audience Validation
| Parameter | Type | Default | Description |
|---|---|---|---|
audience |
string | clientID |
Expected audience for access token validation |
strictAudienceValidation |
bool | false |
Reject sessions with audience mismatch |
allowOpaqueTokens |
bool | false |
Enable opaque token support via RFC 7662 |
requireTokenIntrospection |
bool | false |
Require introspection for opaque tokens |
Production Security Configuration
audience: "https://my-api.example.com"
strictAudienceValidation: true
Opaque Token Support
allowOpaqueTokens: true
requireTokenIntrospection: true
strictAudienceValidation: true
Other Security Options
| Parameter | Type | Default | Description |
|---|---|---|---|
disableReplayDetection |
bool | false |
Disable JTI-based replay attack detection |
allowPrivateIPAddresses |
bool | false |
Allow private IPs in provider URLs |
Bearer-token (M2M) authentication
Opt-in path that accepts Authorization: Bearer <jwt> instead of the cookie
session flow. M2M-only, default off, audience-mandatory. See
docs/BEARER_AUTH.md for the threat model and operational
guidance.
| Parameter | Type | Default | Description |
|---|---|---|---|
enableBearerAuth |
bool | false |
Master switch. Startup fails if true with empty audience or with bearerIdentifierClaim=email. |
bearerIdentifierClaim |
string | "sub" |
JWT claim used as the principal identifier. "email" is rejected at startup. |
stripAuthorizationHeader |
bool | true |
Strip Authorization from forwarded requests after successful bearer auth. |
bearerEmitWWWAuthenticate |
bool | true |
Emit RFC 6750 WWW-Authenticate: Bearer error="..." hints on 401. |
bearerOverridesCookie |
bool | false |
Cookie wins when both bearer and cookie are present (default). Set true for bearer-wins. |
maxTokenAgeSeconds |
int64 | 86400 |
Upper bound on iat claim age (24h). 0 disables the check. |
maxIdentifierLength |
int | 256 |
Length cap on the sanitised principal identifier. |
bearerFailureThreshold |
int | 20 |
Consecutive 401s from one source IP that trip the throttle. |
bearerFailureWindowSeconds |
int | 60 |
Rolling window for counting 401s. |
bearerFailurePenaltySeconds |
int | 60 |
429 + Retry-After duration after the threshold trips. |
Session Management
| Parameter | Type | Default | Description |
|---|---|---|---|
sessionMaxAge |
int | 86400 (24h) |
Maximum session age in seconds |
refreshGracePeriodSeconds |
int | 60 |
Seconds before expiry to attempt refresh |
maxRefreshTokenAgeSeconds |
int | 21600 |
Heuristic max age (in seconds) of a stored refresh token. Once exceeded, requests treat the RT as expired up front (returns 401 to AJAX, triggers full re-auth on navigations) instead of grant-spamming the IdP with invalid_grant retries. IdPs do not advertise RT TTL on the wire, so this is intentionally a conservative heuristic — tune to match your provider. Set 0 to disable. Default 21600 (6h). |
cookieDomain |
string | auto-detected | Domain for session cookies |
cookiePrefix |
string | _oidc_raczylo_ |
Prefix for cookie names |
Multi-Subdomain Setup
cookieDomain: .example.com # Share cookies across subdomains
Multiple Middleware Instances
When running multiple middleware instances with different authorization requirements, use unique prefixes:
# User authentication middleware
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-userauth
spec:
plugin:
traefikoidc:
cookiePrefix: "_oidc_userauth_"
sessionEncryptionKey: user-encryption-key-min-32-bytes
# ... other config
---
# Admin authentication middleware
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-adminauth
spec:
plugin:
traefikoidc:
cookiePrefix: "_oidc_adminauth_"
sessionEncryptionKey: admin-encryption-key-min-32-bytes
allowedUsers:
- admin@example.com
# ... other config
Extended Session Duration
sessionMaxAge: 604800 # 7 days
# Common values:
# 3600 - 1 hour (high security)
# 86400 - 1 day (default)
# 259200 - 3 days
# 604800 - 7 days
# 2592000 - 30 days
Access Control
User Restrictions
| Parameter | Type | Description |
|---|---|---|
allowedUserDomains |
[]string | Restrict to specific email domains |
allowedUsers |
[]string | Specific email addresses allowed |
allowedRolesAndGroups |
[]string | Required roles or groups |
roleClaimName |
string | JWT claim for roles (default: roles) |
groupClaimName |
string | JWT claim for groups (default: groups) |
userIdentifierClaim |
string | Claim for user ID (default: email) |
Domain Restriction
allowedUserDomains:
- company.com
- subsidiary.com
Specific User Access
allowedUsers:
- user@example.com
- contractor@external.org
Role-Based Access Control
allowedRolesAndGroups:
- admin
- developer
roleClaimName: "https://myapp.com/roles" # For namespaced claims (Auth0)
Access Control Logic
- If only
allowedUsersis set: Only specified emails can access - If only
allowedUserDomainsis set: Only specified domains can access - If both are set: Access granted if email is in
allowedUsersOR domain is inallowedUserDomains - If neither is set: Any authenticated user can access
Users Without Email (Azure AD)
For Azure AD service accounts or users without email:
userIdentifierClaim: sub # Options: sub, oid, upn, preferred_username
allowedUsers:
- "abc12345-6789-0abc-def0-123456789abc" # User object ID
Headers Configuration
Default Headers
The middleware sets these headers for downstream services:
| Header | Description |
|---|---|
X-Forwarded-User |
User's email address |
X-User-Groups |
Comma-separated user groups |
X-User-Roles |
Comma-separated user roles |
X-Auth-Request-Redirect |
Original request URI |
X-Auth-Request-User |
User's email address |
X-Auth-Request-Token |
User's ID token |
Minimal Headers Mode
For "431 Request Header Fields Too Large" errors:
minimalHeaders: true # Only forwards X-Forwarded-User
Custom Templated Headers
headers:
- name: "X-User-Email"
value: "{{{{.Claims.email}}}}"
- name: "X-User-ID"
value: "{{{{.Claims.sub}}}}"
- name: "Authorization"
value: "Bearer {{{{.AccessToken}}}}"
- name: "X-User-Roles"
value: "{{{{range $i, $e := .Claims.roles}}}}{{{{if $i}}}},{{{{end}}}}{{{{$e}}}}{{{{end}}}}"
Template Variables:
{{.Claims.field}}- ID token claims{{.AccessToken}}- Raw access token{{.IdToken}}- Raw ID token{{.RefreshToken}}- Raw refresh token
Important: Use double curly braces ({{{{ and }}}}) to escape templates in YAML.
Security Headers
Security Profiles
| Profile | Use Case | Security Level |
|---|---|---|
default |
Standard web apps | High |
strict |
Maximum security | Very High |
development |
Local development | Medium |
api |
API endpoints | High |
custom |
Custom requirements | Configurable |
Basic Configuration
securityHeaders:
enabled: true
profile: "default"
API with CORS
securityHeaders:
enabled: true
profile: "api"
corsEnabled: true
corsAllowedOrigins:
- "https://your-frontend.com"
- "https://*.example.com"
corsAllowCredentials: true
Custom Security Configuration
securityHeaders:
enabled: true
profile: "custom"
# Content Security Policy
contentSecurityPolicy: "default-src 'self'; script-src 'self'"
# HSTS
strictTransportSecurity: true
strictTransportSecurityMaxAge: 31536000
strictTransportSecuritySubdomains: true
strictTransportSecurityPreload: true
# Frame and Content Protection
frameOptions: "DENY"
contentTypeOptions: "nosniff"
xssProtection: "1; mode=block"
referrerPolicy: "strict-origin-when-cross-origin"
# CORS
corsEnabled: true
corsAllowedOrigins: ["https://app.example.com"]
corsAllowedMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
corsAllowedHeaders: ["Authorization", "Content-Type"]
corsAllowCredentials: true
corsMaxAge: 86400
# Custom Headers
customHeaders:
X-Custom-Header: "value"
# Server Identification
disableServerHeader: true
disablePoweredByHeader: true
CORS Origin Patterns
corsAllowedOrigins:
- "https://example.com" # Exact match
- "https://*.example.com" # Subdomain wildcard
- "http://localhost:*" # Port wildcard (development)
Scope Configuration
Default Behavior (Append Mode)
scopes:
- roles
- custom_scope
# Result: ["openid", "profile", "email", "roles", "custom_scope"]
Override Mode
overrideScopes: true
scopes:
- openid
- profile
- custom_scope
# Result: ["openid", "profile", "custom_scope"]
Advanced Options
Dynamic Client Registration (RFC 7591)
Dynamic Client Registration allows the middleware to automatically register itself with the OIDC provider, eliminating the need to manually create client credentials.
Basic Configuration (Single Instance):
dynamicClientRegistration:
enabled: true
initialAccessToken: "your-token" # Optional, if provider requires it
persistCredentials: true
credentialsFile: "/tmp/oidc-credentials.json"
clientMetadata:
redirect_uris:
- "https://your-app.com/oauth2/callback"
client_name: "My Application"
application_type: "web"
grant_types:
- "authorization_code"
- "refresh_token"
Multi-Replica Deployment (Kubernetes):
For Kubernetes deployments with multiple replicas, use Redis storage to share credentials across all instances and prevent registration race conditions:
dynamicClientRegistration:
enabled: true
persistCredentials: true
storageBackend: "redis" # Share credentials via Redis
redisKeyPrefix: "myapp:dcr:" # Optional custom prefix
clientMetadata:
redirect_uris:
- "https://your-app.com/oauth2/callback"
client_name: "My Application"
redis:
enabled: true
address: "redis:6379"
cacheMode: "redis"
Storage Backend Options:
| Backend | Description | Use Case |
|---|---|---|
file |
Store credentials in local file | Single instance deployments |
redis |
Store credentials in Redis | Multi-replica Kubernetes deployments |
auto |
Use Redis if available, fallback to file | Flexible deployments (default) |
Multi-Replica Deployment
Without Redis, disable replay detection:
disableReplayDetection: true
With Redis (recommended):
redis:
enabled: true
address: "redis:6379"
cacheMode: "hybrid"
See REDIS.md for complete Redis configuration.
Kubernetes Secrets
Reference secrets instead of hardcoding sensitive values:
providerURL: urn:k8s:secret:oidc-secret:ISSUER
clientID: urn:k8s:secret:oidc-secret:CLIENT_ID
clientSecret: urn:k8s:secret:oidc-secret:SECRET
Create the secret:
kubectl create secret generic oidc-secret \
--from-literal=ISSUER=https://accounts.google.com \
--from-literal=CLIENT_ID=your-client-id \
--from-literal=SECRET=your-client-secret \
-n traefik
Environment Variable Naming
Important: Avoid using "API" as a substring in environment variable names when using ${VAR} syntax in Traefik configuration. Traefik reserves TRAEFIK_API_* variables and the substring may cause conflicts.
# Bad - may cause issues
sessionEncryptionKey: ${OIDC_SECRET_API}
# Good
sessionEncryptionKey: ${OIDC_SECRET_SVC}