/Traefik OIDC

Traefik OIDC

184
v0.8.25

Traefik OIDC Middleware

OpenID Connect authentication middleware for Traefik. Replaces forward-auth + oauth2-proxy. Auto-detects all major OIDC providers, validates ID tokens, manages sessions, and forwards user identity to downstream services.

Documentation

Provider support

ProviderOIDCRefreshAuto-detected by
GoogleFullYesaccounts.google.com
Azure ADFullYeslogin.microsoftonline.com, sts.windows.net
Auth0FullYes*.auth0.com
OktaFullYes*.okta.com, *.oktapreview.com, *.okta-emea.com
KeycloakFullYeshost containing keycloak, or /realms/ in path (covers KC <17 /auth/realms/ and 17+ /realms/)
AWS CognitoFullYescognito-idp.*.amazonaws.com
GitLabFullYesgitlab.com
GitHubOAuth 2.0 only — no ID token, no refreshNogithub.com
GenericFullYesany RFC-compliant .well-known/openid-configuration

Authentication and claim extraction use the ID token. Ensure your provider includes required claims (email, roles, groups) in the ID token, not just the access token or UserInfo endpoint.

Install

Enable the plugin in Traefik's static configuration:

# traefik.yml
experimental:
plugins:
traefikoidc:
moduleName: github.com/lukaszraczylo/traefikoidc
version: v0.7.10

Then attach the middleware in your dynamic configuration (see Quickstart below).

This middleware tracks the current Traefik helm chart release. If it fails to load, update Traefik first.

Verify release signatures

Release checksums are signed with cosign keyless signing:

cosign verify-blob \
--certificate-identity-regexp "https://github.com/lukaszraczylo/traefikoidc/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--bundle "traefikoidc_v<version>_checksums.txt.sigstore.json" \
traefikoidc_v<version>_checksums.txt

Quickstart

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-auth
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: https://accounts.google.com
clientID: 1234567890.apps.googleusercontent.com
clientSecret: urn:k8s:secret:traefik-oidc:CLIENT_SECRET
sessionEncryptionKey: urn:k8s:secret:traefik-oidc:SESSION_KEY
callbackURL: /oauth2/callback
logoutURL: /oauth2/logout
postLogoutRedirectURI: /
# forceHTTPS defaults to true (secure-by-default). Only set false if you
# serve OIDC over plaintext HTTP for local dev.
allowedUserDomains: [company.com]
allowedRolesAndGroups: [admin, developer]
excludedURLs: [/health, /metrics]

More example configs in examples/.

Required parameters

ParameterDescription
providerURLIssuer URL (used for OIDC discovery).
clientIDOAuth 2.0 client ID.
clientSecretOAuth 2.0 client secret. Supports urn:k8s:secret:ns:name:key.
sessionEncryptionKeyCookie encryption key, min 32 bytes.
callbackURLCallback path, e.g. /oauth2/callback.

Common optional parameters

Full reference in docs/CONFIGURATION.md.

ParameterDefaultPurpose
forceHTTPStrueForces https:// in redirect URIs. Leave at default behind any TLS-terminating LB (AWS ALB, GCP LB, Azure App Gateway). Set false only for plaintext HTTP local dev.
logoutURLcallbackURL + "/logout"RP-initiated logout path.
postLogoutRedirectURI/Where to send users after logout.
scopesappended to openid profile emailExtra OAuth scopes. Set overrideScopes: true to replace defaults.
excludedURLsnonePrefix-matched paths that bypass auth.
allowedUserDomainsnoneRestrict to email domains.
allowedUsersnoneRestrict to specific addresses (or claim values when userIdentifierClaim != email).
allowedRolesAndGroupsnoneRequire any of these roles/groups from ID-token claims.
roleClaimName / groupClaimNameroles / groupsFor namespaced claims (Auth0).
userIdentifierClaimemailUse sub, oid, upn, or preferred_username for users without email.
enablePKCEfalsePKCE on the auth code flow.
cookieDomainautoSet explicitly for multi-subdomain setups (.example.com).
cookiePrefix_oidc_raczylo_Unique prefix per middleware instance to isolate sessions.
sessionMaxAge86400Session lifetime in seconds.
refreshGracePeriodSeconds60Proactively refresh tokens this many seconds before expiry.
rateLimit100Requests/sec. Min 10.
logLevelinfodebug, info, error.
audienceclientIDCustom access-token audience (Auth0 custom APIs).
strictAudienceValidationfalseReject mismatched audiences. Set true in production.
allowOpaqueTokens / requireTokenIntrospectionfalseAccept opaque access tokens via RFC 7662.
disableReplayDetectionfalseDisable JTI cache. Use Redis instead for multi-replica.
allowPrivateIPAddressesfalsePermit private-IP providerURL (internal Keycloak, etc.).
minimalHeadersfalseReduce forwarded headers (mitigates HTTP 431).
stripAuthCookiesfalseStrip OIDC cookies from backend hop (mitigates HTTP 431).
caCertPath / caCertPEMnoneTrust an internal CA for the provider's TLS.
insecureSkipVerifyfalseLocal dev only. Disables TLS verification, logs a security warning.
enableBackchannelLogout / backchannelLogoutURLfalse / noneOIDC Back-Channel Logout (server-to-server).
enableFrontchannelLogout / frontchannelLogoutURLfalse / noneOIDC Front-Channel Logout (iframe).
redisdisabledSee docs/REDIS.md.
dynamicClientRegistrationdisabledSee docs/DCR.md.

Production gotchas

TLS termination at a load balancer

forceHTTPS defaults to true, so redirect URIs always use https://. This is the right default behind AWS ALB, GCP LB, Azure App Gateway, or any LB that terminates TLS — X-Forwarded-Proto is unreliable (ALB may overwrite it).

Only set forceHTTPS: false when you actually serve OIDC over plaintext HTTP (local dev). See issue #82.

Multi-replica deployments

Each replica keeps its own in-memory JTI cache → false positive "token replay detected" when the same token hits different replicas. Two options:

  1. Set disableReplayDetection: true (loses replay protection).
  2. Enable Redis for shared state (recommended) — see docs/REDIS.md.

For IdP-initiated logout (back/front-channel) in multi-replica setups, Redis is required so a logout on one instance invalidates sessions on the others.

Multiple middleware instances on the same host

Each instance must use a unique cookiePrefix and sessionEncryptionKey, otherwise a session minted by one instance can grant access through another. See issue #87.

HTTP 431 from backends

Either the ID token or the chunked OIDC cookies overflow your backend's header buffer. Combine these as needed:

minimalHeaders: true # drop X-Auth-Request-Token et al.
stripAuthCookies: true # strip _oidc_raczylo_* cookies on the backend hop

Cookies remain in the browser; only the Traefik→backend hop is affected. See #64, #122.

Internal CA for the provider

If the provider's TLS cert is signed by a private CA (self-hosted GitLab, internal Keycloak, ADFS):

caCertPath: /etc/ssl/certs/internal-ca.pem
# or, inline:
caCertPEM: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----

Both can be combined. An unparseable bundle fails the plugin at startup. See #125.

Environment variable names containing API

Traefik reserves TRAEFIK_API_*. User vars whose name contains API (e.g. OIDC_ENCRYPTION_SECRET_API) make the plugin fail with invalid handler type: <nil>. Rename to anything without the literal API substring. See #98.

Templated headers

Forward identity to backends via Go templates over ID-token claims and tokens:

headers:
- name: X-User-Email
value: "{{{{.Claims.email}}}}"
- name: Authorization
value: "Bearer {{{{.AccessToken}}}}"
- name: X-User-Roles
value: "{{{{range $i, $e := .Claims.roles}}}}{{{{if $i}}}},{{{{end}}}}{{{{$e}}}}{{{{end}}}}"

Available bindings: .Claims.<field>, .AccessToken, .IdToken, .RefreshToken. Names are case-sensitive (.Claims, not .claims).

Escape with quadruple braces. If you see can't evaluate field AccessToken in type bool, Traefik's YAML parser ate your {{ }}. The fix that actually works is {{{{ }}}} — the YAML pass turns it into {{ }} for the Go template engine. Other escaping tricks (literal blocks, single quotes) do not work reliably.

Default downstream headers

When a request is authenticated, the middleware sets:

HeaderNotes
X-Forwarded-UserUser's email (always).
X-User-GroupsComma-separated.
X-User-RolesComma-separated.
X-Auth-Request-UserUser's email.
X-Auth-Request-RedirectOriginal request URI.
X-Auth-Request-TokenFull ID token — the largest header; suppressed by minimalHeaders.

Plus security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy) controlled by the securityHeaders section — see docs/CONFIGURATION.md.

Common errors

SymptomCause
Token verification failedWrong/unreachable providerURL, or clock skew.
Session encryption key too shortsessionEncryptionKey is < 32 bytes.
No matching public key foundJWKS endpoint down, or kid mismatch.
Access denied: Your email domain is not allowedUser's domain not in allowedUserDomains.
Access denied: You do not have any of the allowed roles or groupsClaims missing or not in allowedRolesAndGroups.
can't evaluate field AccessToken in type boolTemplate not escaped — use {{{{ }}}}.
tls: failed to verify certificate: x509: certificate signed by unknown authorityInternal CA — set caCertPath / caCertPEM.
invalid handler type: <nil>Env var name contains API — rename it.
false positive replay detectedMulti-replica without Redis — see Multi-replica deployments.
Google sessions expire after ~1hConsent screen still in "Testing" mode. Do not add offline_access — Google rejects it; the middleware sets access_type=offline automatically.

Provider-specific issues (Keycloak mappers, Azure AD group overage, Auth0 namespaced claims, Cognito regions, GitLab self-hosted) live in docs/PROVIDERS.md.

Set logLevel: debug to surface detail.

License

See LICENSE.