This guide documents the auth-host mode pattern using thomseddon/traefik-forward-auth:2 (TFA). It replaces the more complex oauth2-proxy approach that required a dual-router hack and an errors middleware to handle redirects in Traefik v3.
Why traefik-forward-auth over oauth2-proxy?
With Traefik v3, oauth2-proxy’s redirect pattern broke because:
authRequestRedirectwas removed from the ForwardAuth middlewaresigninurlwas also removed- The only workaround was chaining an Errors middleware to catch 401s and redirect — fragile and verbose
traefik-forward-auth issues a real HTTP 307 redirect directly from the ForwardAuth response, which Traefik v3 honours natively. No errors middleware, no dual routers per service.
Architecture
Browser → traefik (websecure) │ ▼ ForwardAuth check → tfa-personal:4181 │ Not authenticated? │ TFA returns 307 → https://auth.yourdomain.com/_oauth │ TFA 302 → Google OAuth consent │ Google callback → auth.yourdomain.com/_oauth │ Cookie set on .yourdomain.com (shared across all subdomains) │ Redirect to original protected URL ✓
The key difference from oauth2-proxy: TFA encodes the original URL into the OAuth state parameter, so after Google login it always returns the user to where they started — not to the auth subdomain.
Step 1 — Google OAuth Credentials
- Go to Google Cloud Console → APIs & Services → Credentials
- Create an OAuth 2.0 Client ID (Web application)
- Add the authorised redirect URI for each auth domain:
https://auth.yourdomain.com/_oauth
Note the path is /_oauth, not /oauth2/callback as with oauth2-proxy. 4. Save your Client ID and Client Secret
Step 2 — Whitelist Config File
TFA supports a config file for email whitelists. Create one per instance on the host:
# /docker/tfa/data/personal-tfa.conf
whitelist = you@gmail.com
Or to allow an entire domain:
domain = yourdomain.com
Both instances can share the same file if the allowed users are the same, or use separate files to scope access differently per domain group.
Step 3 — Deploy traefik-forward-auth
This is a single shared stack. Deploy once. The middleware names (auth-personal, auth-company) are defined here and referenced by every protected service via the @swarm suffix.
# tfa-stack.yml version: "3.8" services: # ── Protects *.yourdomain.com ───────────────────────────────────────────── tfa-personal: image: thomseddon/traefik-forward-auth:2 environment: PROVIDERS_GOOGLE_CLIENT_ID: "your-google-client-id" PROVIDERS_GOOGLE_CLIENT_SECRET: "your-google-client-secret" SECRET: "your-random-32-char-hex-secret" # openssl rand -hex 16 DEFAULT_PROVIDER: "google" AUTH_HOST: "auth.yourdomain.com" # dedicated auth subdomain COOKIE_DOMAIN: "yourdomain.com" # no leading dot needed here COOKIE_NAME: "_tfa_personal" URL_PATH: "/_oauth" # Google redirect URI path LIFETIME: "2592000" # session lifetime in seconds (30 days) LOG_LEVEL: "info" CONFIG: "/etc/tfa/config" volumes: - /docker/tfa/data/personal-tfa.conf:/etc/tfa/config:ro networks: - lb-net deploy: replicas: 1 labels: - "traefik.enable=true" - "traefik.swarm.network=lb-net" # ── Router for the auth subdomain itself ────────────────────────── # IMPORTANT: this router MUST also carry the forwardauth middleware. # TFA needs to receive X-Forwarded-Host/Uri/Proto on the auth-host # request so it can store the *original* protected URL in the OAuth # state parameter. Without this, after Google login the user is # redirected back to auth.yourdomain.com instead of the service they # were trying to reach. - "traefik.http.routers.tfa-personal.rule=Host(`auth.yourdomain.com`)" - "traefik.http.routers.tfa-personal.entrypoints=websecure" - "traefik.http.routers.tfa-personal.tls=true" - "traefik.http.routers.tfa-personal.service=tfa-personal" - "traefik.http.routers.tfa-personal.middlewares=auth-personal" - "traefik.http.services.tfa-personal.loadbalancer.server.port=4181" # ── Middleware definition (used by all protected services) ──────── - "traefik.http.middlewares.auth-personal.forwardauth.address=http://tfa-personal:4181" - "traefik.http.middlewares.auth-personal.forwardauth.trustForwardHeader=true" - "traefik.http.middlewares.auth-personal.forwardauth.authResponseHeaders=X-Forwarded-User" networks: lb-net: external: true name: lb-net
Generate the SECRET value:
openssl rand -hex 16
Deploy:
docker stack deploy -c tfa-stack.yml google-auth
Step 4 — Protecting a Service
This is the complete, minimal set of labels needed on any service. No dual routers, no errors middleware.
# myservice-stack.yml version: "3.8" services: myservice: image: your-image networks: - lb-net deploy: replicas: 1 labels: - "traefik.enable=true" - "traefik.swarm.network=lb-net" - "traefik.http.routers.myservice.rule=Host(`myservice.yourdomain.com`)" - "traefik.http.routers.myservice.entrypoints=websecure" - "traefik.http.routers.myservice.tls=true" - "traefik.http.routers.myservice.service=myservice" - "traefik.http.routers.myservice.middlewares=auth-personal@swarm" # ← one line - "traefik.http.services.myservice.loadbalancer.server.port=80" networks: lb-net: external: true name: lb-net
That single middlewares=auth-personal@swarm label is all that’s needed. TFA handles the full OAuth dance transparently.
Running Multiple Auth Domains
If you manage services across two separate domains (e.g. a personal domain and a company domain), run two TFA instances in the same stack — one per domain. Each gets its own:
AUTH_HOSTsubdomainCOOKIE_DOMAIN(so cookies are scoped to the right domain)COOKIE_NAME(avoids cookie name collisions between domains)SECRET(independent session signing)- Middleware name (
auth-personal/auth-company)
Services on *.yourdomain.com use auth-personal@swarm. Services on *.companydomain.com use auth-company@swarm. One Google OAuth app can cover both as long as both callback URIs are registered in Google Cloud Console.
The Critical Label Non-Obvious Detail
The auth-host router must apply the forwardauth middleware to itself:
- "traefik.http.routers.tfa-personal.middlewares=auth-personal"
This is counterintuitive — you’re applying the middleware to the TFA service’s own router. The reason: when a protected service redirects an unauthenticated browser to auth.yourdomain.com/_oauth, Traefik needs to forward the X-Forwarded-Host, X-Forwarded-Uri, and X-Forwarded-Proto headers to TFA on that request. TFA reads those headers to reconstruct the original URL and encode it into the OAuth state. Without this, after Google login the user lands on auth.yourdomain.com instead of the service they came from.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
After login, redirected to auth.yourdomain.com not original service | middlewares=auth-personal missing from the TFA router itself | Add it as shown above |
Google returns redirect_uri_mismatch | Callback URI in Google Console doesn’t match AUTH_HOST + URL_PATH | Register https://auth.yourdomain.com/_oauth in Google Console |
| Cookie not shared — must log in per subdomain | COOKIE_DOMAIN missing or wrong | Set to bare domain without leading dot: yourdomain.com |
| TFA container exits immediately | Bad SECRET value | Use openssl rand -hex 16 output |
unauthorized on every request | Whitelist/domain config not matching your Google account email | Check the mounted config file path and content |
Verification
# Watch TFA auth decisions live docker service logs -f google-auth_tfa-personal # Successful auth flow shows: # Starting auth flow → Google redirect # Completing auth flow → cookie set, redirect to original URL # Authenticated user: you@gmail.com
Clear browser cookies for the domain before each test run to avoid stale session interference.
Leave a Reply