TL;DR
Every service I run — ComfyUI, Grafana, Vault, even the ancient app on a Mac across the network — lives behind a Traefik forward-auth middleware that hands off to Authentik. No per-service login page. One Authentik login shared across everything. The magic is a two-route IngressRoute pattern: a protected route with the middleware + an unprotected callback route for the OAuth flow itself. Adding a new service to the cluster takes five lines of YAML. Wiring a non-Kubernetes backend — like the Mac that runs ComfyUI and Ollama — takes a service-with-manual-endpoints proxy.
The problem I was avoiding
Before forward-auth, every service had its own authentication problem:
- Grafana has built-in OIDC support — fine for one service.
- ComfyUI has literally no auth at all — protected by network isolation alone.
- Vault I configured with Authentik OIDC too, but it’s its own configuration dance.
- A legacy service running on a Mac somewhere off-cluster — can’t run sidecar auth, can’t do anything clever.
Three logins for three services. A fourth service means a fourth login. And the offnet services? They were either locked by IP firewall (fragile) or unprotected behind TLS (security theater).
The solution is middleware. Put Traefik’s forward-auth middleware in front of everything, point it at Authentik’s embedded outpost, and let Traefik handle the redirect dance. Once a user logs in once, they’re logged in everywhere.
How forward-auth actually works
Request flow:
Browser → GET /dashboard HTTP/1.1 (no auth header)
↓
Traefik sees no auth header → triggers forward-auth middleware
↓
Traefik → Authentik: "can I let this request through?"
↓
Authentik (outpost) → Browser: "redirect to /auth (not logged in)"
↓
Browser → Authentik login page, user types password
↓
Authentik → Browser: redirect with session cookie + Set-Cookie
↓
Browser → GET /dashboard + cookie header
↓
Traefik → Authentik: "is this cookie valid?" (via forward-auth)
↓
Authentik → Traefik: 200 OK + auth response headers (username, groups, JWT)
↓
Traefik → Service (ComfyUI, etc.) + injected headers
↓
Service responds with content
The key insight: Authentik is not in the request path after the user is logged in. Traefik caches the auth headers, and subsequent requests hit Traefik → service directly. The only thing hitting Authentik again is the periodic re-auth check, and the logout URL.
The Traefik middleware definition
Define this once in your infrastructure namespace (I use public-ingress):
# kubernetes/apps/public-ingress/middleware-authentik.yaml
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: authentik-forward-auth
namespace: public-ingress
spec:
forwardAuth:
address: http://authentik-server.authentik.svc.cluster.local/outpost.goauthentik.io/auth/traefik
trustForwardHeader: true
authResponseHeaders:
- X-Authentik-Username
- X-Authentik-Groups
- X-Authentik-Email
- X-Authentik-Uid
- X-Authentik-Name
- Authorization
Key details:
- address points to Authentik’s embedded Traefik outpost service on port 80 (inside the cluster). The path
/outpost.goauthentik.io/auth/traefikis fixed — don’t change it. - trustForwardHeader tells the outpost to trust
X-Forwarded-*headers from Traefik (needed when Traefik is reverse-proxying the outpost itself). - authResponseHeaders lists the headers Authentik injects back. Any custom claims you want the downstream service to see go in here.
Install this middleware and it’s available cluster-wide via middleware: [public-ingress/authentik-forward-auth] from any namespace.
The two-route IngressRoute pattern
This is the part that trips people up. The OAuth callback route must not be protected.
# kubernetes/apps/comfyui/comfyui-ingress.yaml
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: comfyui
namespace: comfyui
spec:
entryPoints:
- websecure
tls:
certResolver: letsencrypt
routes:
# Route 1: the protected service
- match: Host(`comfyui.k3s.internal.zolty.systems`)
middlewares:
- name: authentik-forward-auth
namespace: public-ingress
services:
- name: comfyui
port: 8188
# Route 2: the auth callback — NO MIDDLEWARE
- match: Host(`comfyui.k3s.internal.zolty.systems`) && PathPrefix(`/outpost.goauthentik.io/`)
services:
- name: authentik-server
namespace: authentik
port: 80
Why two routes?
If /outpost.goauthentik.io/ went through the forward-auth middleware, Authentik would ask itself if it’s allowed to authenticate users. Infinite loop. The callback route bypasses auth entirely so the OAuth exchange can complete.
TLS and hostnames: Both routes share the same hostname and TLS certificate. The second route just carves out a path exception.
Once this is in place, visiting https://comfyui.k3s.internal.zolty.systems redirects to Authentik, you log in once, and Traefik proxies you through.
Adding a non-Kubernetes backend (proxy-only mode)
Some services live outside the cluster — a legacy app, a GPU box with ComfyUI running locally, whatever. You can still put them behind forward-auth by creating a Service with manual Endpoints.
Example: ComfyUI running on a Mac at 192.168.1.50:8188:
# kubernetes/apps/comfyui-offnet/service.yaml
---
apiVersion: v1
kind: Service
metadata:
name: comfyui-offnet
namespace: comfyui-offnet
spec:
type: ClusterIP
ports:
- port: 8188
targetPort: 8188
protocol: TCP
# No selector — we're providing endpoints manually
---
apiVersion: v1
kind: Endpoints
metadata:
name: comfyui-offnet
namespace: comfyui-offnet
subsets:
- addresses:
- ip: 192.168.1.50
ports:
- port: 8188
Then wire the IngressRoute the same way as any other service:
# kubernetes/apps/comfyui-offnet/ingress.yaml
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: comfyui-offnet
namespace: comfyui-offnet
spec:
entryPoints:
- websecure
tls:
certResolver: letsencrypt
routes:
- match: Host(`comfyui-offnet.k3s.internal.zolty.systems`)
middlewares:
- name: authentik-forward-auth
namespace: public-ingress
services:
- name: comfyui-offnet
port: 8188
- match: Host(`comfyui-offnet.k3s.internal.zolty.systems`) && PathPrefix(`/outpost.goauthentik.io/`)
services:
- name: authentik-server
namespace: authentik
port: 80
Traefik doesn’t care that the Service points to an external IP. It just proxies the connection the same way. The offnet service now lives behind Authentik SSO like everything else.
Persisting sessions and logging out
Sessions live in Authentik’s database, not in Traefik. A browser cookie from one service works everywhere because they all ask Authentik the same question: “is this cookie valid?”
Logout is handled by the /outpost.goauthentik.io/sign_out path, which you can wire to a logout link in the UI if the service supports it. Or just let browser-session clearing handle it.
One subtlety: Authentik’s session timeout is cluster-wide, not per-service. All services share the same session length, remember-me duration, and MFA requirements. This is actually a feature if you’re running 40 services — one policy for all of them.
The honest caveats
- API clients need a different auth path. Forward-auth works for browsers. Headless clients (CI, scripts, monitoring) don’t have a browser and can’t hit the login page. For those, use Authentik’s API tokens or JWT auth — they’re a different route and I’ll cover that separately.
- Callback carve-out is a sharp edge. If you forget the
/outpost.goauthentik.io/route and protect it with middleware, the user gets stuck in a redirect loop. The error is subtle — you just see “too many redirects” in the browser. I’ve done this twice. - A single IdP is a single point of failure. If Authentik crashes, everything fails to authenticate (though users already logged in keep working until their session expires). Run Authentik HA if authentication is critical.
- Session cookies are domain-scoped. Each subdomain gets its own session —
comfyui.k3s.internal.zolty.systemsandvault.k3s.internal.zolty.systemsdon’t share a browser cookie. They both work with Authentik (the outpost handles the redirects), but they don’t SSO to each other. You log in once per domain. Not a deal-breaker for internal services, but worth knowing.
Lessons
- Middleware is the lever. Don’t bolt auth onto each service. Let the reverse proxy handle it.
- Two routes, one IngressRoute. The callback must bypass the middleware. This pattern is copy-paste.
- The outpost is Authentik’s best feature for infrastructure. OIDC is fine for app-level auth. Forward-auth is how you protect everything at once.
- Cross-namespace middleware references work. You can define auth in
public-ingressand use it fromgrafana,vault,comfyui, etc. The namespace prefix does the routing. - Off-net services are just Services with manual Endpoints. No special wiring needed. Traefik proxies them the same way.
For deeper context on the OIDC side — human vs. workload auth, bootstrap secrets, the recovery token strategy — see Vault behind Authentik.
Running this without a homelab? A managed Kubernetes cluster like DigitalOcean’s can run Traefik Ingress and Authentik identically. The middleware and IngressRoute specs are cluster-agnostic; cert-manager gets the Let’s Encrypt DNS-01 challenge. The only piece that’s homelab-specific is the internal
.zolty.systemshostnames — swap them for your own DNS and it works on managed k8s exactly the same way.