oauth2-proxy is the dependable way to put SSO in front of any app — internal tools, dashboards, third-party software that doesn’t speak SAML/OIDC natively. Wired to Traefik via forwardAuth, every protected route makes a sub-request to oauth2-proxy on each request; if the user has a valid session, the request flows through and identity headers are forwarded to the backend; if not, the user is redirected to the IdP. This article walks through a working install end-to-end — Google as the IdP (substitute any OIDC provider), oauth2-proxy as the sidecar, and Traefik as the front door.
How to verify
# An anonymous request to a protected route should redirect to oauth2-proxy's /oauth2/start
curl -sI https://app.example.com/ | head -5
# oauth2-proxy must be reachable
curl -s http://oauth2-proxy.internal:4180/ping
# After login, the cookie cuts the round-trip
curl -sI -b "_oauth2_proxy=valid-session-cookie" https://app.example.com/ | head -5
# Backend sees the identity headers
docker logs app 2>&1 | grep -i 'x-forwarded-user' | tail
What’s happening
oauth2-proxy is a small Go service that holds the OAuth2/OIDC client credentials, manages the sign-in flow with the IdP, and issues an opaque session cookie to the browser. It exposes three URL groups: /oauth2/start redirects to the IdP; /oauth2/callback receives the IdP’s response and sets the cookie; /oauth2/auth is the forward-auth check endpoint — it returns 202 if the cookie is valid, 401 if not.
Traefik’s forwardAuth middleware calls /oauth2/auth on every request to a protected route. When 401 comes back, Traefik returns a 302 to the user pointing at /oauth2/start?rd=<original-url>, which kicks off the OAuth flow. After successful login the user lands back on <original-url> with the cookie set, and the next call to /oauth2/auth returns 202.
The identity gets to the backend via response headers from /oauth2/auth: X-Forwarded-User, X-Forwarded-Email, X-Forwarded-Groups. Traefik copies the named headers from the auth response onto the original request before sending it upstream. The backend trusts these headers because the only path they can be set is via the Traefik+oauth2-proxy chain.
The cookie domain matters. Setting cookie_domain=.example.com lets the same session work across app.example.com, tools.example.com, etc. Don’t set cookie_domain if you want strict per-host isolation.
The procedure
-
Register the OAuth client with the IdP. For Google, create an OAuth client in Google Cloud Console — type “Web application”, redirect URI
https://auth.example.com/oauth2/callback. -
Deploy oauth2-proxy. The full config below covers Google as IdP, cookie config, Redis-backed sessions (so a single oauth2-proxy can scale).
# /opt/oauth2-proxy/config.yaml provider: google client_id: "REPLACE_CLIENT_ID.apps.googleusercontent.com" client_secret: "REPLACE_CLIENT_SECRET" email_domains: - "example.com" cookie_domains: - ".example.com" cookie_secret: "REPLACE_32_BYTE_BASE64" cookie_secure: true cookie_httponly: true cookie_samesite: lax cookie_expire: "168h" # 7 days reverse_proxy: true skip_provider_button: false set_xauthrequest: true set_authorization_header: false pass_authorization_header: false pass_access_token: false pass_user_headers: true session_store: type: redis redis: connection_url: "redis://redis.internal:6379/0"# docker-compose section services: oauth2-proxy: image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0 command: --config=/etc/oauth2-proxy/config.yaml volumes: - /opt/oauth2-proxy/config.yaml:/etc/oauth2-proxy/config.yaml:ro networks: - edge labels: traefik.enable: "true" traefik.http.routers.oauth.rule: "Host(`auth.example.com`)" traefik.http.routers.oauth.entrypoints: "websecure" traefik.http.routers.oauth.tls.certresolver: "letsencrypt" traefik.http.services.oauth.loadbalancer.server.port: "4180" -
Wire the Traefik forwardAuth middleware.
http: middlewares: sso: forwardAuth: address: "http://oauth2-proxy:4180/oauth2/auth" trustForwardHeader: true authResponseHeaders: - X-Auth-Request-User - X-Auth-Request-Email - X-Auth-Request-Groups sso-error: errors: status: - "401" service: oauth-redirect@docker query: "/oauth2/start?rd={url}"The
errorsmiddleware converts a 401 from forwardAuth into a 302 to the OAuth start URL. Without it, anonymous users see a 401 page instead of being redirected. -
Apply the middleware chain on protected routes.
http: routers: app: rule: "Host(`app.example.com`)" entryPoints: [websecure] service: app-backend middlewares: [sso-error, sso, secure-headers] tls: certResolver: letsencrypt -
Confirm the backend reads the identity headers. Most apps that support reverse-proxy auth honor
X-Forwarded-UserandX-Forwarded-Emailnatively; for those that don’t, your application code reads them. -
Kubernetes flavour — same chain via CRDs.
apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: sso namespace: platform spec: forwardAuth: address: "http://oauth2-proxy.platform.svc.cluster.local:4180/oauth2/auth" trustForwardHeader: true authResponseHeaders: - X-Auth-Request-User - X-Auth-Request-Email
Common pitfalls
cookie_secretnot 16/24/32 bytes after base64-decoding — oauth2-proxy refuses to start with a cryptic error. Generate withopenssl rand -base64 32.- Forgetting
set_xauthrequest: true— the auth response has no identity headers and your backend sees anonymous users. - Single-instance oauth2-proxy with in-memory sessions — restarts log everyone out. Use Redis-backed sessions in production.
- Wrong
cookie_domain— too narrow and SSO doesn’t span subdomains; too wide and the cookie leaks to apps that should not see it. - The forwardAuth address pointing at the public hostname — at request time oauth2-proxy must be reachable from inside the Traefik network, not via the public URL.
A working oauth2-proxy + Traefik setup is the gateway to running internal tooling securely with no per-app login screens. The pattern composes with the rest of the middleware stack — rate limits, IP allow-lists, security headers — and is part of the managed operations playbook we run for clients. For the lighter-weight JWT-only path see traefik-jwt-auth-middleware.