Protecting Services with Google OAuth on Traefik v3 — Docker Swarm (traefik-forward-auth)

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:

  • authRequestRedirect was removed from the ForwardAuth middleware
  • signinurl was 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

  1. Go to Google Cloud Console → APIs & Services → Credentials
  2. Create an OAuth 2.0 Client ID (Web application)
  3. 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_HOST subdomain
  • COOKIE_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

SymptomCauseFix
After login, redirected to auth.yourdomain.com not original servicemiddlewares=auth-personal missing from the TFA router itselfAdd it as shown above
Google returns redirect_uri_mismatchCallback URI in Google Console doesn’t match AUTH_HOST + URL_PATHRegister https://auth.yourdomain.com/_oauth in Google Console
Cookie not shared — must log in per subdomainCOOKIE_DOMAIN missing or wrongSet to bare domain without leading dot: yourdomain.com
TFA container exits immediatelyBad SECRET valueUse openssl rand -hex 16 output
unauthorized on every requestWhitelist/domain config not matching your Google account emailCheck 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

Your email address will not be published. Required fields are marked *