The redis.enableTLS / redis.tlsSkipVerify settings were accepted by the config layer but silently dropped before reaching the connection pool, so the plugin always dialed Redis in plaintext. This blocked TLS-only Redis deployments such as AWS ElastiCache with in-transit encryption. - Add EnableTLS, TLSSkipVerify, TLSServerName to backends.Config and PoolConfig and forward them through universal_cache_singleton -> backends.Config -> PoolConfig. - In the connection pool, dial via tls.Dialer.DialContext (TLS 1.2 minimum) with SNI defaulting to the host part of the configured Address when TLSServerName is empty, so ElastiCache cluster endpoints validate out of the box. Plain dial path now also propagates ctx. - Add regression tests covering successful TLS negotiation with skip- verify, rejection of self-signed certs without skip-verify, rejection of plain TCP servers when EnableTLS=true, and unaffected plaintext behavior. - Document maxRefreshTokenAgeSeconds (added in1b6c861) and the implicit SSE / WebSocket auth bypass (added in684a990) in README.md, docs/CONFIGURATION.md and docs/index.html. - Add the missing redis.tlsSkipVerify row to docs/index.html and clarify the redis.enableTLS description. patch-release
14 KiB
Configuration Reference
Complete reference for all Traefik OIDC middleware configuration options.
Table of Contents
- Required Parameters
- 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 | 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
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 |
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 |
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}