Traefik's ForwardAuth feature sends every incoming request to an authentication service before forwarding it to the actual backend. If GateKeeper says the request is authenticated (HTTP 200), Traefik passes it through. If not (HTTP 401), Traefik redirects the browser to the login page.
How it works
- A browser sends a request to
app.example.com. - Traefik intercepts it and sends a
GET /auth/verifyrequest to GateKeeper, including the session cookie. - GateKeeper checks the session. If valid, it responds
200withX-Auth-UserandX-Auth-Emailheaders. - Traefik forwards the original request to the app with those headers attached.
- If the session is invalid, Traefik returns a 401, the browser redirects to
/login?redirect_uri=<original_url>, and after login the user is sent back.
Step 1 - define the middleware
Create one file in your Traefik dynamic config directory that defines the gk-auth middleware. You only need to do this once.
# traefik/dynamic/middlewares-gk-auth.yml
http:
middlewares:
gk-auth:
forwardAuth:
address: "http://gatekeeper:8282/auth/verify"
authResponseHeaders:
- X-Auth-User
- X-Auth-Email
- X-Auth-GroupsPoint address directly at the GateKeeper container on its public port (8282) - use the Docker service name (gatekeeper:8282), a private IP, or a Tailscale address. Do not point it at your public BASE_URL (https://auth.example.com/auth/verify).
The reason: a ForwardAuth request routed back through the auth.example.com router gets its X-Forwarded-Host header rewritten to auth.example.com. GateKeeper then builds the post-login redirect back to itself, and users land on their profile page instead of the app they were trying to reach. Hitting the container directly preserves the real app host. GateKeeper still reads X-Forwarded-Proto: https (set by Traefik) to build correct https:// redirects, so a plain http:// address to the container is fine on a private network.
Traefik hot-reloads the dynamic config directory, so no restart is needed after adding this file.
Step 2 - apply it to a service
In each service's route file, add gk-auth@file to the router's middleware list.
# traefik/dynamic/myapp.yml
http:
routers:
myapp:
rule: "Host(`app.example.com`)"
entryPoints:
- https
middlewares:
- gk-auth@file
tls:
certResolver: cloudflare
service: myapp-service
services:
myapp-service:
loadBalancer:
servers:
- url: "http://100.0.0.1:8080"The @file suffix tells Traefik the middleware comes from the file provider. If your middleware and router are in the same file you can omit it, but @file is explicit and always safe.
Identity headers
When authentication succeeds, GateKeeper sets headers that Traefik forwards to your app:
X-Auth-User- the user's internal UUIDX-Auth-Email- the user's email addressX-Auth-Groups- comma-separated list of group names the user belongs to (omitted if the user has no groups)
Your app can read these to identify who is logged in and what roles they have, without any SDK or API call.
Credential injection
For apps that require a username and password rather than header-based auth, you can store credentials on the policy and have GateKeeper inject them automatically. Add Authorization to authResponseHeaders and Traefik will forward the Authorization: Basic header to the upstream:
http:
middlewares:
sonarr-auth:
forwardAuth:
address: "http://gatekeeper:8282/auth/verify?policy=sonarr"
authResponseHeaders:
- X-Auth-User
- X-Auth-Email
- X-Auth-Groups
- AuthorizationSet the app credentials on the policy detail page under Credential injection. See Access policies for the full setup guide.
# Flask example
@app.route("/")
def index():
email = request.headers.get("X-Auth-Email", "anonymous")
return f"Hello, {email}"// Go example
func handler(w http.ResponseWriter, r *http.Request) {
email := r.Header.Get("X-Auth-Email")
fmt.Fprintf(w, "Hello, %s", email)
}Logout
Add a logout link in your app that posts to GateKeeper's logout endpoint:
<form method="POST" action="https://auth.example.com/logout">
<button type="submit">Sign out</button>
</form>Protecting GateKeeper itself
Do not apply the gk-auth middleware to GateKeeper's own route. Doing so creates a loop where the auth server requires authentication to serve the login page.
Troubleshooting: login redirects back to the auth domain
If, after signing in, you land on the GateKeeper profile page instead of the app you were trying to reach, the ForwardAuth request is being routed through the auth.example.com router, which rewrites the X-Forwarded-Host header. GateKeeper then builds the post-login redirect back to itself.
The fix is to point the middleware address directly at the GateKeeper container (http://gatekeeper:8282/auth/verify), never at the public BASE_URL.
To confirm this is the cause, set LOG_LEVEL=debug on GateKeeper and watch the logs while you reproduce it:
docker compose logs -f gatekeeper | grep forwardauthEach verify request logs the headers it received:
{"msg":"forwardauth verify","x_forwarded_host":"auth.example.com","x_forwarded_uri":""}If x_forwarded_host shows your auth domain (and x_forwarded_uri is empty) instead of the app's host, the request is going through the wrong route - switch the middleware address to the direct container address. When it is correct, x_forwarded_host shows the app's hostname. Set LOG_LEVEL back to info when you are done.