Skip to content

Traefik + oauth2-proxy — SSO for any app via forwardAuth

Front any app with SSO using Traefik and oauth2-proxy — full forwardAuth flow, cookie handling, session token forwarding to backends.

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

  1. 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.

  2. 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"
  3. 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 errors middleware 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.

  4. 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
  5. Confirm the backend reads the identity headers. Most apps that support reverse-proxy auth honor X-Forwarded-User and X-Forwarded-Email natively; for those that don’t, your application code reads them.

  6. 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_secret not 16/24/32 bytes after base64-decoding — oauth2-proxy refuses to start with a cryptic error. Generate with openssl 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.