Authentik forward auth with Traefik
Put Authentik in front of any app with Traefik forwardAuth: proxy provider, outpost address, header pass-through, and the redirect loops explained.
What Authentik forward auth with Traefik does
Authentik forward auth with Traefik puts a login in front of an app that has no login of its own. Traefik stops every request for that app, asks Authentik whether the caller is signed in, and passes the request through only if the answer is yes. Authentik answers through an outpost, a small service that holds the proxy provider configuration and reads the Authentik session cookie.
The whole mechanism is one extra HTTP request. Traefik's forwardAuth middleware sends a copy of the incoming request's method, protocol, host, URI and client IP to an address you choose, as the headers X-Forwarded-Method, X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Uri and X-Forwarded-For. A 2XX answer means the request continues to the app. Any other answer is returned to the browser instead, which is how the login redirect reaches the user: the outpost replies with a 302 and the browser follows it to Authentik.
The app itself does not change. It keeps listening on its own port inside Docker and never learns that a check happened, unless you switch on the header pass-through covered further down.
What you need before starting
Two things must already work. Authentik must be reachable at its own hostname, and Traefik must be terminating TLS (transport layer security) for the domains you want to protect. If either is missing, build it first with a working Authentik install on a VPS and Traefik routing several apps from one Compose stack.
Every screen name and label below was configured against authentik 2026.8.0 and Traefik v3, in August 2026. Pin your authentik image tag, because the proxy provider screen moves between releases. Field wording changes, and settings move between the provider and the application. When your UI does not match the words here, look for the same four things: the provider mode, the external host, the outpost assignment and the token.
What forward auth protects, and what it does not
State the threat model out loud, because forward auth is easy to over-trust.
It authenticates users at the edge. A request that reaches the app has passed an Authentik flow, so it carries whatever you put in that flow: a password, MFA (multi-factor authentication), a policy that lets only one group through.
It does nothing for an app that is still reachable on its own published port. A line like ports: - "3000:3000" in the app's Compose file opens port 3000 on the server, so http://203.0.113.10:3000 reaches the app without touching Traefik or Authentik. Docker publishes ports by writing its own netfilter rules, so a ufw deny 3000 rule does not close that door either. Delete the published port and put the app on the Docker network Traefik joins. Traefik then reaches it on the container port with no host port involved, and sudo ss -ltnp | grep :3000 printing nothing is the check.
It does not make the app multi-tenant safe. Everyone who passes the policy arrives at the same app with the same rights that app gives an anonymous visitor. Forward auth decides who gets in. It does not decide what they may do once inside, so an app whose admin panel is open to any visitor stays open to every user you let through. Where an app supports real SSO (single sign-on) itself, that is the better option, and what vendors charge for that feature is worth checking before you design around the gate.
Identity headers are only as trustworthy as the path. If the app trusts X-authentik-username, then anything that can reach the app directly can set that header and become any user. Header based identity holds only while Traefik is the single way in.
Step 1: put Traefik, Authentik and the app on one Docker network
Traefik calls the outpost by container name, so they need a shared network. Create one external network and attach every stack to it. How Compose creates and names networks matters here, because the default one network per project behaviour is exactly what breaks name resolution between stacks.
docker network create proxyAuthentik's published Compose file calls its service server, which is a poor name to depend on once several stacks share a network. Add an override file beside it that joins the shared network under an alias you control.
# docker-compose.override.yml, in the authentik directory
services:
server:
networks:
default: {}
proxy:
aliases:
- authentik-server
networks:
proxy:
external: trueApply it with docker compose up -d in that directory, then confirm the alias resolves from another container on the same network. The Traefik image has no shell, so a throwaway container is the way to test what Traefik will see.
docker run --rm --network proxy curlimages/curl:8.21.0 \
-sS -o /dev/null -w '%{http_code}\n' \
http://authentik-server:9000/outpost.goauthentik.io/pingAny HTTP status at all proves that the name resolves and the port answers, and 204 is what a healthy embedded outpost returns for that path. curl: (6) Could not resolve host: authentik-server means the alias or the network is wrong. Fix that before touching the Authentik UI, because every later step depends on it.
Step 2: create the application and the proxy provider
In the Authentik admin interface open Applications, then Providers, then Create, and pick Proxy Provider.
- Name:
whoami-proxy - Authorization flow:
default-provider-authorization-implicit-consent, or the explicit consent flow if you want a confirmation screen on first access - Mode:
Forward auth (single application) - External host:
https://whoami.example.com
The external host is the field that gets typed wrong most often. It is the URL the browser uses for the app. The outpost matches an incoming forward auth request to a provider by that host, so a provider configured for http:// while users arrive over https://, or configured for the bare domain while the router serves a subdomain, matches nothing and the login never starts.
Now open Applications, then Applications, then Create. Give it a name and a slug, and set Provider to the one you just made. The application object is what policies bind to, so open its bindings tab and bind a group if only some users should pass. With no binding, every account that can log in to Authentik gets through.
Single application mode gives each app its own provider, its own bindings and its own consent behaviour. Domain level mode is the other choice: one provider for every app under a parent domain, configured with an Authentication URL and a Cookie domain. It saves repeated setup at the cost of per app authorisation, because one provider cannot apply different rules to different hosts. Start with single application mode. Move to domain level only when the app count makes the bookkeeping worse than the granularity you give up.
Step 3: attach the provider to an outpost
A provider does nothing until an outpost serves it. Authentik ships an embedded outpost inside the server container, and it is enabled on fresh installs. Open Applications, then Outposts, and edit authentik Embedded Outpost. Add your application to its list of applications and save.
Check the outpost configuration while you are there. The authentik_host value is the URL the outpost sends users to for login, so it must be your public Authentik URL, https://authentik.example.com/, not a container name. A wrong value here sends the browser to an address it cannot reach, and the symptom is a DNS error the moment the check fires.
The outpost list also shows whether the outpost is connected and which version it runs. Nothing you do in Traefik works while that column is unhealthy. One thing the embedded outpost does not do on Docker is add Traefik labels to the server container, so every label in the next two steps is yours to write.
A separate outpost container is the alternative. Pick it when Authentik runs on a different host from Traefik, or when you want the gate to restart independently of the Authentik server. Create the outpost in the UI, copy the token from its deployment info, and run it on the same shared network.
services:
authentik-proxy:
image: ghcr.io/goauthentik/proxy:2026.8.0
environment:
AUTHENTIK_HOST: https://authentik.example.com
AUTHENTIK_INSECURE: "false"
AUTHENTIK_TOKEN: <token from the outpost view>
networks:
- proxy
networks:
proxy:
external: trueIf the outpost reaches Authentik on an internal address that browsers cannot use, set AUTHENTIK_HOST to the internal URL and add AUTHENTIK_HOST_BROWSER with the public one. The outpost then talks to the API over the short path while still redirecting users to a URL that resolves for them.
Step 4: the forwardAuth middleware, pointed at the outpost
Define the middleware once. A file provider keeps it out of container labels and out of your way.
# /etc/traefik/dynamic/authentik.yml, loaded by Traefik's file provider
http:
middlewares:
authentik:
forwardAuth:
address: http://authentik-server:9000/outpost.goauthentik.io/auth/traefik
trustForwardHeader: true
maxResponseBodySize: 4194304
authResponseHeaders:
- X-authentik-username
- X-authentik-groups
- X-authentik-entitlements
- X-authentik-email
- X-authentik-name
- X-authentik-uid
- X-authentik-jwt
- X-authentik-meta-jwks
- X-authentik-meta-outpost
- X-authentik-meta-provider
- X-authentik-meta-app
- X-authentik-meta-versionEvery part of that address earns attention, because this is where most of the lost afternoons start.
- The host is the container alias,
authentik-server. Inside the Traefik container,localhostis Traefik. - The port is
9000, the container port. It is not the host port you published, and it stays 9000 even when you publish nothing. - The path ends in
/auth/traefik. There is a different suffix per reverse proxy,/auth/nginxfor nginx, and the wrong one answers with the wrong thing. - The scheme is
http, because the hop stays inside the Docker network. Adding TLS there gives you a certificate to validate for no gain.
For a standalone outpost, swap the host for that container's name, http://authentik-proxy:9000/outpost.goauthentik.io/auth/traefik. Nothing else changes.
trustForwardHeader: true tells Traefik to pass on the X-Forwarded-* headers it received instead of replacing them with its own view of the request. It matters when a load balancer or a CDN sits in front, because the outpost has to see the host the user typed. Traefik v3 marks the option deprecated in favour of forwardedHeaders.trustedIPs on the entrypoint, which decides by source IP which incoming X-Forwarded-* headers survive before any middleware sees them. maxResponseBodySize caps how much of the outpost's answer Traefik will read. Authentik's documented middleware sets it, so keep it.
Step 5: route /outpost.goauthentik.io to the outpost
Everything under /outpost.goauthentik.io/ on the app's own domain has to reach the outpost rather than the app, because the login flow returns the browser to that path on the app host to finish and set the proxy session cookie. In single application mode that path is the only thing on the app domain the outpost serves.
Put these labels on the Authentik server container, or on the standalone outpost container if you run one.
services:
server:
labels:
traefik.enable: "true"
traefik.http.routers.authentik-outpost.rule: PathPrefix(`/outpost.goauthentik.io/`) && (Host(`whoami.example.com`) || Host(`grafana.example.com`))
traefik.http.routers.authentik-outpost.entrypoints: websecure
traefik.http.routers.authentik-outpost.tls.certresolver: le
traefik.http.routers.authentik-outpost.priority: "20"
traefik.http.routers.authentik-outpost.service: authentik-outpost
traefik.http.services.authentik-outpost.loadbalancer.server.port: "9000"Add a Host() clause for each app you protect. Traefik's default router priority is the length of the rule, so this longer rule already beats the app's plain Host() rule, and setting priority explicitly means you do not depend on that.
Now the app router, carrying the middleware.
services:
whoami:
image: traefik/whoami:v1.12
networks:
- proxy
labels:
traefik.enable: "true"
traefik.http.routers.whoami.rule: Host(`whoami.example.com`)
traefik.http.routers.whoami.entrypoints: websecure
traefik.http.routers.whoami.tls.certresolver: le
traefik.http.routers.whoami.middlewares: authentik@file
traefik.http.services.whoami.loadbalancer.server.port: "80"
networks:
proxy:
external: truetraefik/whoami prints the headers it received, which makes it the easiest thing to test a gate with. Swap it for the real app once the flow works, and note that there is no ports: line: the only route in is the router above. Note the @file suffix too. A middleware defined in the file provider is authentik@file, and one defined in container labels is authentik@docker. Naming the wrong provider leaves the router in an error state in the Traefik dashboard, with a log line saying the middleware does not exist.
Run docker compose up -d, then open https://whoami.example.com in a private browser window. You should land on the Authentik login page and come back to whoami afterwards, with X-Authentik-Username among the headers it prints, in Go's canonical capitalisation. Confirm the second router with curl -v https://whoami.example.com/outpost.goauthentik.io/ping, which should return 204.
Why does Traefik answer 500 instead of showing a login page?
This is the first failure mode, and it is nearly always the middleware address. Traefik cannot reach the auth server, so it has no verdict, so it fails the request. The reason is in the Traefik log rather than in the browser, and it usually looks like dial tcp: lookup authentik-server on 127.0.0.11:53: no such host. That address is Docker's embedded DNS resolver, so the message means the name does not exist on any network this container has joined.
Work through the causes in this order.
- Traefik is not attached to the
proxynetwork.docker network inspect proxylists the containers on it. If Traefik is missing, add the network to its service and recreate the container. Restarting is not enough, because a container's networks are fixed when it is created. - The alias is missing, or the Authentik stack was never recreated after you added the override file.
- The address uses
localhostor127.0.0.1. Inside the Traefik container that is Traefik, and Traefik has nothing listening on 9000. - The address uses the public URL,
https://authentik.example.com/outpost.goauthentik.io/auth/traefik. This one often works, which is why it survives. It also sends every single request out of the Docker network, back in through Traefik and through TLS, and it fails whenever DNS or the certificate has a bad moment. Keep the hop internal.
A 404 in the browser instead of a 500 is a different problem with the same root. Traefik returns the auth server's non-2XX response to the client as is, so a 404 you see in the browser is the outpost's own 404, usually from a wrong path suffix. Test the exact address by hand, imitating what the middleware sends.
docker run --rm --network proxy curlimages/curl:8.21.0 -sS -i \
-H 'X-Forwarded-Proto: https' \
-H 'X-Forwarded-Host: whoami.example.com' \
-H 'X-Forwarded-Uri: /' \
http://authentik-server:9000/outpost.goauthentik.io/auth/traefikThere is no session cookie in that request, so a correctly wired outpost answers with a redirect whose Location points at your Authentik host. That is the answer Traefik would hand to a logged out browser. Anything else means the outpost did not match this host to a provider, which is the same failure you get from a wrong External host.
Why does the login loop back to the login page?
The second failure mode is the original host or the auth headers not surviving the hop. The symptom is a login that succeeds and then starts again, sometimes forever. There are four causes worth checking, each with its own tell.
/outpost.goauthentik.io/ on the app domain is not routed to the outpost. Authentik sends the browser back to that path on the app host to finish the login, the app receives a path it does not know, and it answers its own 404 or its own redirect. The user goes round again. curl -v https://whoami.example.com/outpost.goauthentik.io/ping returning anything other than 204 proves it. This one causes more lost afternoons than the rest together.
The outpost never sees the real host. Traefik fills X-Forwarded-Host from the request it received. If another proxy or a CDN sits in front and Traefik does not trust that proxy's headers, the outpost sees an internal host, matches no provider or the wrong one, and never issues the right redirect. Set forwardedHeaders.trustedIPs on the entrypoint to the front proxy's addresses.
The provider's External host does not match the browser's URL. Copy the URL out of the browser bar rather than typing it. A mismatch of scheme or subdomain is invisible on the screen and fatal to the match.
A cookie is being set and then discarded. The Authentik session cookie and the outpost's proxy cookie are both marked Secure, so a browser drops them on any page served over plain HTTP. TLS at the edge with http:// in the External host produces exactly that, and it looks identical to a loop.
Getting the user identity to the app
Traefik discards everything the auth server returns unless you name it. That is the whole job of authResponseHeaders: each listed header is copied off the outpost's answer and onto the request Traefik forwards to the app. Leave the list out and the gate still works, but the app receives an anonymous request and shows its own login form behind your login form.
The user facing headers are X-authentik-username, X-authentik-groups (pipe separated), X-authentik-email, X-authentik-name, X-authentik-uid (a hashed identifier) and X-authentik-entitlements. The X-authentik-meta-* headers describe the outpost, provider and app instead, and X-authentik-jwt carries a signed token for apps that would rather verify a signature than trust a header. If you prefer a pattern to a list, authResponseHeadersRegex: "^X-authentik-" copies them all, and the anchor matters because the match is partial.
Whether the app can use any of this is the app's decision. Apps with proxy header authentication read one of these headers as the username, each under its own setting name. Apps without it stay logged out behind a working gate, which is usually fine, since the gate was the point. The rule from the threat model still applies with full force here: an app that trusts a username header must not be reachable on any path that skips Traefik.
Forward auth or oauth2-proxy?
Same job, different moving parts, different running cost. Both stop the request at the edge and release it only after a login.
Authentik's proxy provider fits when Authentik is already your identity provider (IdP). The gate is configured in the same UI as the users and the policies, so adding an app is a provider, an application, and one line of labels. MFA, group policies, a session shared across every protected app, and one place to cut someone off all come with it. The cost is the Authentik stack, a database and a worker, plus upgrades that move the screens you configured against.
oauth2-proxy fits when the identity already lives somewhere else, or when you want the gate's configuration in the same repository as the Compose file. It is one small container with no database, and its configuration is environment variables you can read in a diff. The cost is thinner authorisation and more instances as the fleet grows, unless you share one cookie domain across apps. The oauth2-proxy version of this same setup walks the identical path with those pieces, which makes the comparison concrete.
Pick one and stay with it per stack. Switching later means new cookies, new sessions and a re-test of every protected app. If the identity provider itself is still open, that decision comes first, and the comparison of Keycloak, Authentik and Zitadel covers it.
FAQ
Why does Traefik return 500 when Authentik forward auth is enabled?
Traefik could not reach the outpost, so it had no verdict and failed the request. Read the Traefik log: a line like dial tcp: lookup authentik-server on 127.0.0.11:53: no such host means the name does not resolve on any network that container has joined. Confirm both containers are on the shared network with docker network inspect proxy, and remember that adding a network needs the container recreated, not restarted. Check the address uses the container name and the internal port 9000, not localhost and not the published host port.
Why does the Authentik login redirect loop back to the login page?
The most common cause is that /outpost.goauthentik.io/ on the app's own domain is not routed to the outpost, so the browser finishes the login against the app instead of against Authentik and starts over. Test it with curl -v https://whoami.example.com/outpost.goauthentik.io/ping, which should return 204. If that path is fine, check the provider's External host against the URL in the browser bar, and check authentik_host in the outpost configuration points at the public Authentik URL.
Do I still need to remove the app's published port?
Yes. Forward auth only guards the path through Traefik. A ports: entry publishes the app on the server itself, and Docker writes its own netfilter rules for that, so a ufw deny rule will not close it. Drop the published port, keep the app on the Docker network Traefik joins, and verify with sudo ss -ltnp that nothing on the host is listening on the app's port.
Should I use the embedded outpost or a separate outpost container?
Use the embedded outpost when Authentik and Traefik run on the same host. It needs no token and no second container, and it is enabled by default on new installs. Run a separate outpost when Authentik lives on another machine, or when you want the gate to restart on its own schedule. The separate one needs AUTHENTIK_HOST and AUTHENTIK_TOKEN, and it changes only the hostname in the middleware address.
Which Authentik version does this configuration match?
Everything here was configured against authentik 2026.8.0 with Traefik v3, in August 2026. The proxy provider screen has changed wording and layout across releases, so pin your image tag rather than tracking the latest tag. If your interface differs, the four settings to find are the provider mode, the external host, the outpost the provider is attached to, and the outpost's authentik_host.