Traefik OAuth2-Proxy Integration#
OAuth2 Proxy integrates with Traefik via the ForwardAuth middleware. There are two primary patterns, each with different tradeoffs. --reverse-proxy=true is required for both so that oauth2-proxy accepts X-Forwarded-* headers from Traefik .
Full official documentation: Traefik | OAuth2 Proxy
Pattern 1 β ForwardAuth + errors Middleware (401 Redirect)#
forwardAuth points to /oauth2/auth, which returns 202 (authenticated) or 401 (unauthenticated) without proxying the request. The errors middleware catches 401β403 and serves the sign-in page .
Critical: add statusRewrites: "401": 302 to the errors middleware. Without it, the redirect from oauth2-proxy is served inside the 401 context, causing browsers to display a bare "Found." link instead of following the redirect automatically .
Pass ?rd={url} in the errors middleware query so the proxy knows where to redirect the user after login:
oauth-errors:
errors:
status: ["401-403"]
service: oauth-backend
query: "/oauth2/sign_in?rd={url}"
statusRewrites:
"401": 302
Middleware order matters: the errors middleware must be listed before forwardAuth in the chain .
Pattern 2 β ForwardAuth to / with static://202 Upstream#
Point forwardAuth directly at oauth2-proxy's root (/) instead of /oauth2/auth. Configure oauth2-proxy with --upstream=static://202. Unauthenticated requests are redirected to the sign-in flow internally β no separate errors middleware needed .
oauth-auth-redirect: # auto-redirects to sign-in
forwardAuth:
address: https://oauth.example.com/
trustForwardHeader: true
oauth-auth-wo-redirect: # returns 401 silently
forwardAuth:
address: https://oauth.example.com/oauth2/auth
trustForwardHeader: true
This pattern avoids the CSRF race condition described below and simplifies the configuration, but requires a dedicated oauth2-proxy hostname. The /oauth2/ route must still be accessible from all protected service domains for static asset resolution (see Sign-In Page Asset Path Resolution below).
Known Issue: CSRF Token Race Condition (Pattern 1 + skip_provider_button=true)#
Root cause: When skip_provider_button = true, /oauth2/sign_in immediately calls doOAuthStart and sets a CSRF cookie . When Traefik's errors middleware intercepts a 401, the browser may fire multiple concurrent subresource requests (JS, CSS, service workers), each triggering a separate server-side call to /oauth2/sign_in. Each call writes a new _oauth2_proxy_csrf cookie, overwriting the previous one. The OAuth callback then fails with 403 CSRF token mismatch because the browser's cookie no longer matches the state parameter.
Example log pattern:
GET /oauth2/sign_in?rd=.../ β 302 + Set-Cookie _oauth2_proxy_csrf=AAA
GET /oauth2/sign_in?rd=.../sw.js β 302 + Set-Cookie _oauth2_proxy_csrf=BBB (overwrites AAA)
GET /oauth2/sign_in?rd=.../workbox.js β 302 + Set-Cookie _oauth2_proxy_csrf=CCC
GET /oauth2/callback?state=.../ β 403 CSRF token mismatch
Fix β cookie_csrf_per_request = true: Enabled via PR #1708, this makes each authentication attempt write a unique CSRF cookie keyed by the state nonce, so concurrent requests each carry distinct, non-overwriting cookies .
Alternative: set skip_provider_button = false. The sign-in page becomes a static HTML page that sets no cookies; CSRF state is only generated when the user clicks the "Sign in" button, which triggers a single navigation to /oauth2/start .
Related cookie flags :
| Flag | Purpose |
|---|---|
--cookie-csrf-expire | Lifetime of CSRF cookies (default 15m) |
--cookie-csrf-samesite | SameSite policy for CSRF cookies |
--cookie-csrf-per-request-limit | Caps concurrent CSRF cookies to avoid HTTP 431 |
Sign-In Page Asset Path Resolution (Cross-Domain)#
The sign-in page template loads CSS via a path relative to ProxyPrefix :
<link rel="stylesheet" href="{{.ProxyPrefix}}/static/css/bulma.min.css">
ProxyPrefix defaults to /oauth2 . When the protected service and oauth2-proxy are on different domains (e.g., app.example.com vs oauth.example.com), Traefik's errors middleware serves the sign-in page body from oauth2-proxy but the asset URL resolves relative to the protected service's domain. The browser requests app.example.com/oauth2/static/css/bulma.min.css, which returns 404.
Solutions:
- Route
/oauth2/on every protected domain to the oauth2-proxy backend β the recommended approach in the official example config. - Use Pattern 2 (static upstream + ForwardAuth to
/), where the user's browser is redirected to the oauth2-proxy domain before the sign-in page is rendered, so assets load from the correct origin.
The rd field in the sign-in form is populated server-side as a hidden input and client-side for hash fragments . When Traefik's errors middleware does not carry the original URL into the rd parameter, post-login redirects land on the domain root instead of the intended subpath β the ?rd={url} query in the errors.query config (see Pattern 1 above) is the fix .
Header Forwarding#
| Flag | Header Set | Notes |
|---|---|---|
--set-xauthrequest | X-Auth-Request-User, X-Auth-Request-Email, X-Auth-Request-Groups | Identity info to upstream |
--pass-authorization-header | Authorization: Bearer <id_token> | OIDC ID token |
--pass-access-token | X-Forwarded-Access-Token (+ X-Auth-Request-Access-Token with --set-xauthrequest) | OAuth access token |
Traefik's authResponseHeaders list in forwardAuth must explicitly enumerate any headers you want forwarded to the upstream service .
Gotcha β pass_host_header: defaults to true. Setting it to false rewrites the Host header to the upstream URL, breaking host-based routing inside Kubernetes .