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/traefik is 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.systems and vault.k3s.internal.zolty.systems don’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-ingress and use it from grafana, 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.systems hostnames — swap them for your own DNS and it works on managed k8s exactly the same way.