Skip to content
James Brooks // thatnerdknows
all writing
// writing

SSO for any app with Authentik + Traefik

June 5, 2026 · #authentik #traefik #sso #homelab

What you get: put a single sign-on login screen in front of any web app — even ones with no auth of their own — using Authentik forward-auth and Traefik. Add a middleware to the router and the app is instantly SSO- protected; you can gate it to specific users or groups (including federated AD/LDAP groups). No app changes.

This is one of the most useful patterns in a homelab — and it has three gotchas that will waste an evening if you don’t know them. All three are here.


The pattern (per app)

1. Traefik — add the authentik forward-auth middleware to the router, and add an outpost route so the /outpost.goauthentik.io/ callback reaches Authentik:

# middleware (once)
http:
  middlewares:
    authentik:
      forwardAuth:
        address: "http://AUTHENTIK_IP:9000/outpost.goauthentik.io/auth/traefik"
        trustForwardHeader: true
        authResponseHeaders: ["X-authentik-username","X-authentik-groups","X-authentik-email"]
# per app: the app router + its outpost route
    rt-myapp:
      rule: "Host(`myapp.example.com`)"
      middlewares: ["lan-only","authentik"]     # <- gated
      service: svc-myapp
    rt-myapp-outpost:
      rule: "Host(`myapp.example.com`) && PathPrefix(`/outpost.goauthentik.io/`)"
      service: authentik                         # <- points at the Authentik outpost service
      priority: 100

2. Authentik — create a Proxy Provider in forward_single mode, an Application bound to it, and gate it to a group (Application → Policy/Group binding). Attach the provider to your embedded outpost. Done — hit myapp.example.com and you get the Authentik login; only members of that group get through, and the app receives X-authentik-username/-groups headers.

Bonus: federate Active Directory / LDAP into Authentik as a source, and you can gate apps by AD group membership — users log in with their existing AD password.


Gotcha 1 — login redirects to the internal IP (cert warning)

Unauthenticated requests bounce to http://AUTHENTIK_IP:9000/application/o/authorize/... instead of your public https://auth.example.com → browser cert warning / broken flow. The embedded outpost builds that redirect from its authentik_host, which defaults to the internal http://IP:9000.

Fix: set both authentik_host and authentik_host_browser to your public URL and restart the outpost.

# PATCH the embedded outpost — GET the config first and MERGE (a partial 'config' clobbers the rest):
#   authentik_host         = https://auth.example.com
#   authentik_host_browser = https://auth.example.com
# then restart server + worker (a PATCH alone isn't enough — it builds the redirect at runtime):
docker compose restart server worker

Setting only authentik_host_browser is not enough — the forward-auth Location: is built from authentik_host itself. Set both, restart, and the redirect goes to https://auth.example.com.

Gotcha 2 — expired session = “dead buttons” (POST returns 401)

After the proxy session expires, GET requests 302 to the login page (fine), but POST requests get a bare 401 — Authentik can’t redirect a request body. A single-page app then fails silently: the page loads but every action button “does nothing.” Classic and maddening.

Fix (two parts):

  • Raise the proxy provider’s access_token_validity (default is often ~1 hour → bump to hours=12 or days=1 so re-auth is ~daily, not hourly). (PATCH gotcha: include mode: forward_single or you get 400 "Internal host cannot be empty".)
  • App-side, reload-to-re-login on a 401 so the SPA recovers itself instead of freezing.

Gotcha 3 — a bypass PathPrefix swallows sibling routes

If you exclude an API path from auth (SDKs/CI that authenticate by token), don’t use a bare string prefix. PathPrefix(/api/handoff) also matches /api/handoffs/... — so those app routes silently bypass the gate and lose their X-authentik-* headers.

Fix: use a bounded regex: PathRegexp(^/api/(handoff|ingest)(/|$)) — exact endpoints with a /-or-end boundary.


Gotchas recap

  1. Redirect to internal IP → set authentik_host (+ _browser) to the public URL and restart the outpost.
  2. POST-401 dead buttons on session expiry → raise access_token_validity + app-side reload-on-401.
  3. Prefix bypass over-matches → use PathRegexp with boundaries, not PathPrefix.
  4. Gate by group (incl. federated AD groups), and always keep the app LAN-only unless it’s meant to be public.

Written from a working setup: Authentik (docker-compose) forward-auth gating several LAN apps via Traefik, gated to an admin group, with AD/LDAP federation for group-based access. Adjust IPs/domains to your environment.


Written by James Brooks — I run ThatNerdKnows (IT support + websites for small businesses). This is the deep end; if you’d rather just have it handled, that’s the day job.