mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
1e33bb0a4d
revocation endpoints, joining the existing client_secret_post default. Both are opt-in via the new clientAuthMethod config field. Closes #135. private_key_jwt (RFC 7523 §2.2 / OpenID Connect Core §9) ======================================================== Plugin signs a short-lived JWT with a configured private key and presents it as client_assertion. Use when the IdP enforces short secret TTLs or requires secretless client auth (Microsoft Entra ID / Azure AD, Okta, Auth0, Keycloak). New Config fields: clientAuthMethod (default: client_secret_post) clientAssertionPrivateKey (inline PEM) clientAssertionKeyPath (PEM file path; mutually exclusive) clientAssertionKeyID (JWS kid header — required) clientAssertionAlg (default: RS256; RS/PS/ES 256–512 supported) PEM forms accepted: PKCS#8, PKCS#1, SEC1. Assertion claims: iss=sub=clientID, aud=tokenURL, iat=now, exp=now+60s, random 16-byte hex jti per request. ECDSA signatures are raw r||s per RFC 7515 (not ASN.1). client_secret_basic (RFC 6749 §2.3.1) ===================================== Sends credentials in the Authorization: Basic header instead of the body. Both halves are form-urlencoded individually before base64 — that encoding step is required by the spec and is NOT what stdlib's http.Request.SetBasicAuth does, so the plugin uses its own helper. The form body omits client_id and client_secret on this path. Wire-up ======= Both methods are dispatched at the same two call sites: helpers.go:exchangeTokens — auth_code + refresh_token grants token_manager.go:RevokeTokenWithProvider — RFC 7009 revocation Existing clientSecret deployments are unaffected — empty clientAuthMethod maps to the historical client_secret_post behavior, and clientAssertion remains nil unless the new fields are set. Yaegi compatibility =================== All required crypto/rsa, crypto/ecdsa, crypto/x509, encoding/pem and crypto/sha256/384/512 symbols are exposed by the traefik/yaegi stdlib symbol tables (RSA SignPKCS1v15 + SignPSS, ECDSA Sign, ParsePKCS8/1PrivateKey, ParseECPrivateKey). Tests (16 new) ============== Algorithm-family coverage: TestIssue135_SignerRSAFamily — RS256/384/512 + PS256/384/512 TestIssue135_SignerECDSAFamily — ES256/384/512, raw r||s shape TestIssue135_SignerRejectsAlgKeyMismatch TestIssue135_SignerJTIUniqueness — 50 sigs, all jti distinct TestIssue135_SignerPEMVariants — PKCS#8, PKCS#1, SEC1 Config validation: TestIssue135_ConfigValidation — full Validate() matrix TestIssue135_ConfigKeyPathLoadsFile Wire-up: TestIssue135_AuthCodeExchangeUsesAssertion TestIssue135_RefreshTokenUsesAssertion TestIssue135_BackcompatClientSecretPath TestIssue135_RevocationUsesAssertion TestIssue135_BuildSignerFromInlineConfig TestIssue135_BuildSignerDefaultsToRS256 TestIssue135_ClientSecretBasicAuth — Authorization header, no body creds TestIssue135_ClientSecretBasicURLEncodesReservedChars — :, +, /, @, =, & TestIssue135_ClientSecretBasicRevocation — revocation parity Documentation ============= README.md — required-row note + 5 optional rows + dedicated section docs/CONFIGURATION.md — new Client Authentication section with three method subsections, OpenSSL keygen snippet, RFC links docs/index.html — 5 new config-table rows + Private Key JWT explainer card .traefik.yml + examples/complete-traefik-config.yaml — commented opt-in example Out of scope (deferred) ======================= mTLS / tls_client_auth (RFC 8705) — separate change; requires per-call http.Client with tls.Config.Certificates and conflicts with the current pooled HTTP client architecture.
647 lines
20 KiB
Markdown
647 lines
20 KiB
Markdown
# Configuration Reference
|
|
|
|
Complete reference for all Traefik OIDC middleware configuration options.
|
|
|
|
## Table of Contents
|
|
|
|
- [Required Parameters](#required-parameters)
|
|
- [Client Authentication](#client-authentication)
|
|
- [Optional Parameters](#optional-parameters)
|
|
- [Security Options](#security-options)
|
|
- [Session Management](#session-management)
|
|
- [Access Control](#access-control)
|
|
- [Headers Configuration](#headers-configuration)
|
|
- [Security Headers](#security-headers)
|
|
- [Scope Configuration](#scope-configuration)
|
|
- [Advanced Options](#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
|
|
|
|
```yaml
|
|
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`:
|
|
|
|
```yaml
|
|
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](https://www.rfc-editor.org/rfc/rfc7523). 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` = `clientID`
|
|
- `aud` = token endpoint URL
|
|
- `iat` = now, `exp` = now + 60s
|
|
- `jti` = random hex per request
|
|
- `kid` header = `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:**
|
|
|
|
```yaml
|
|
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:**
|
|
|
|
```yaml
|
|
clientAuthMethod: private_key_jwt
|
|
clientAssertionKeyPath: /etc/traefik/oidc/client-key.pem
|
|
clientAssertionKeyID: key-2026-01
|
|
clientAssertionAlg: RS256
|
|
```
|
|
|
|
**Generating an RS256 key with OpenSSL:**
|
|
|
|
```bash
|
|
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 `jti` is generated per request.
|
|
- The `aud` claim is the token endpoint URL (from discovery).
|
|
- Tracking issue:
|
|
[#135](https://github.com/lukaszraczylo/traefikoidc/issues/135).
|
|
|
|
### client_secret_basic
|
|
|
|
Per [RFC 6749 §2.3.1][rfc6749-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.
|
|
|
|
```yaml
|
|
clientAuthMethod: client_secret_basic
|
|
clientID: your-client-id
|
|
clientSecret: your-client-secret
|
|
```
|
|
|
|
[rfc6749-2-3-1]: https://www.rfc-editor.org/rfc/rfc6749#section-2.3.1
|
|
|
|
---
|
|
|
|
## 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](#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 the
|
|
`authenticated` flag 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`
|
|
(and `X-Auth-Request-User` unless `minimalHeaders: 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
|
|
|
|
```yaml
|
|
audience: "https://my-api.example.com"
|
|
strictAudienceValidation: true
|
|
```
|
|
|
|
#### Opaque Token Support
|
|
|
|
```yaml
|
|
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
|
|
|
|
```yaml
|
|
cookieDomain: .example.com # Share cookies across subdomains
|
|
```
|
|
|
|
### Multiple Middleware Instances
|
|
|
|
When running multiple middleware instances with different authorization requirements, use unique prefixes:
|
|
|
|
```yaml
|
|
# 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
|
|
|
|
```yaml
|
|
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
|
|
|
|
```yaml
|
|
allowedUserDomains:
|
|
- company.com
|
|
- subsidiary.com
|
|
```
|
|
|
|
### Specific User Access
|
|
|
|
```yaml
|
|
allowedUsers:
|
|
- user@example.com
|
|
- contractor@external.org
|
|
```
|
|
|
|
### Role-Based Access Control
|
|
|
|
```yaml
|
|
allowedRolesAndGroups:
|
|
- admin
|
|
- developer
|
|
roleClaimName: "https://myapp.com/roles" # For namespaced claims (Auth0)
|
|
```
|
|
|
|
### Access Control Logic
|
|
|
|
- If only `allowedUsers` is set: Only specified emails can access
|
|
- If only `allowedUserDomains` is set: Only specified domains can access
|
|
- If both are set: Access granted if email is in `allowedUsers` OR domain is in `allowedUserDomains`
|
|
- If neither is set: Any authenticated user can access
|
|
|
|
### Users Without Email (Azure AD)
|
|
|
|
For Azure AD service accounts or users without email:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
minimalHeaders: true # Only forwards X-Forwarded-User
|
|
```
|
|
|
|
### Custom Templated Headers
|
|
|
|
```yaml
|
|
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
|
|
|
|
```yaml
|
|
securityHeaders:
|
|
enabled: true
|
|
profile: "default"
|
|
```
|
|
|
|
### API with CORS
|
|
|
|
```yaml
|
|
securityHeaders:
|
|
enabled: true
|
|
profile: "api"
|
|
corsEnabled: true
|
|
corsAllowedOrigins:
|
|
- "https://your-frontend.com"
|
|
- "https://*.example.com"
|
|
corsAllowCredentials: true
|
|
```
|
|
|
|
### Custom Security Configuration
|
|
|
|
```yaml
|
|
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
|
|
|
|
```yaml
|
|
corsAllowedOrigins:
|
|
- "https://example.com" # Exact match
|
|
- "https://*.example.com" # Subdomain wildcard
|
|
- "http://localhost:*" # Port wildcard (development)
|
|
```
|
|
|
|
---
|
|
|
|
## Scope Configuration
|
|
|
|
### Default Behavior (Append Mode)
|
|
|
|
```yaml
|
|
scopes:
|
|
- roles
|
|
- custom_scope
|
|
# Result: ["openid", "profile", "email", "roles", "custom_scope"]
|
|
```
|
|
|
|
### Override Mode
|
|
|
|
```yaml
|
|
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):**
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```yaml
|
|
disableReplayDetection: true
|
|
```
|
|
|
|
With Redis (recommended):
|
|
|
|
```yaml
|
|
redis:
|
|
enabled: true
|
|
address: "redis:6379"
|
|
cacheMode: "hybrid"
|
|
```
|
|
|
|
See [REDIS.md](REDIS.md) for complete Redis configuration.
|
|
|
|
---
|
|
|
|
## Kubernetes Secrets
|
|
|
|
Reference secrets instead of hardcoding sensitive values:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```bash
|
|
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.
|
|
|
|
```yaml
|
|
# Bad - may cause issues
|
|
sessionEncryptionKey: ${OIDC_SECRET_API}
|
|
|
|
# Good
|
|
sessionEncryptionKey: ${OIDC_SECRET_SVC}
|
|
```
|