Files
serverAppConfigs/serverAppConfigs-docs/Infrastructure/Authentik.md
T
Frank ef07e7d01a fix(caddy,authentik): auth guarded vhosts by proxying the whole request through authentik
The forward_auth target /outpost.goauthentik.io/auth/caddy does not exist
in authentik 2026.8 (verified live: 404 for every header combination; the
URL tree has no outpost auth prefix at all — the outpost worker dials back
to core over ws/outpost/<uuid>/ instead). Unauthenticated subrequests got
404, Caddy treated >=400 as 'not authenticated' and redirected browsers to
the dead path — users saw authentik's own Not-Found page.

Fix: guarded vhosts now proxy the whole request through authentik-server:9000
with X-authentik-meta-outpost/-provider/-app headers (the exact header set
this build's Traefik controller emits, adapted for Caddy). Unauthenticated
clients get the login flow served in place; once a session exists,
authentik proxies on to the provider's internal_host — now
http://caddy:8017, a new post-auth internal listener that routes straight to
the app. The chain terminates; nothing dials back into authentik.

Live DB changes applied alongside (postgres is the source of truth):
provider pk=1 authentication_flow=default-authentication-flow (was NULL —
the actual cause of the 404s) and internal_host=http://caddy:8017 (was
self-referential http://authentik-server:9000).
2026-08-28 22:28:32 -07:00

7.1 KiB
Raw Blame History

Authentik — the homelab's SSO / OIDC provider

Authentik is the single sign-on (SSO) and OpenID Connect (OIDC) provider for the homelab. It fronts the Caddy vhosts that do not carry their own application-level authentication. Apps that have their own login (Grafana, Gitea, OpenWebUI, Jellyfin, the *arr stack, Pi-hole) are not wrapped in forward_auth — they handle their own session.

  • Version: 2026.8.0 (pinned in dankclaw/compose.yaml)
  • Host: dankclaw (192.168.0.143)
  • Name: auth.franksdank.cloud
  • Stack: 3 compose services — authentik-postgres, authentik-server, authentik-worker (see Pi-hole for the LAN-DNS pattern)

Why Authentik

The comparison that chose it (against Authelia, Keycloak, Zitadel, Ory) is in .docaudit/research/auth.md. In short: full OIDC with single-logout, a web GUI for all configuration, a native Caddy forward_auth outpost, and a footprint that fits on a 3-user homelab (~1–1.5 GB idle). Redis was removed in 2025.10, so the stack is 3 services, not 4.

Architecture

graph LR
    U[Browser] -->|1. GET /search?q=x| A[Anubis :8923<br/>bot challenge]
    A -->|2. verified, dials internal listener| C[Caddy :8016<br/>auth decision]
    C -->|3. whole request + meta headers| S[authentik-server :9000<br/>proxy provider 1]
    S -->|4. no session: login flow served in place| U
    S -->|5. session valid: proxies on to internal_host| P[Caddy :8017<br/>post-auth routing]
    P -->|6. app response back up the chain| U
    S --> PG[(postgres)]

In authentik 2026.8 the proxy provider is an OAuth2 provider and the old /outpost.goauthentik.io/auth/caddy forward-auth endpoint no longer exists — every request that reaches it returns 404 (verified live, 2026-08-29). The chain therefore does not forward-auth to a path: guarded vhosts proxy the whole request through authentik with X-authentik-meta-outpost, X-authentik-meta-provider and X-authentik-meta-app headers. The provider's authentication_flow (default-authentication-flow) decides: no session → the login flow is served in place; session valid → the policy passes and authentik proxies the request on to the provider's internal_host (http://caddy:8017, Caddy's post-auth internal listener), which routes it straight to the app. The chain terminates — nothing dials back into Authentik from :8017.

The three compose services

Service Role Image
authentik-postgres PostgreSQL 16 (users, flows, sessions, events) docker.io/library/postgres:16-alpine
authentik-server Core + admin API + the embedded Caddy outpost (port 9000) ghcr.io/goauthentik/server:2026.8.0
authentik-worker Background tasks (event processing, provider sync, flows) ghcr.io/goauthentik/server:2026.8.0
  • Postgres is the only stateful store. Sessions, caching, and WebSockets moved to PostgreSQL in 2025.10 (Redis removed).
  • Server and worker run the same image; the command selects the role (server / worker).
  • The worker runs as 0:0 (rootless: container root maps to the host user that owns the data dir).
  • Data persists under /home/dank/authentik/ (bind mounts, repo policy — no named volumes).

Caddy integration

Guarded vhosts proxy the whole request through Authentik on Caddy's internal listener — there is no forward_auth anywhere: in 2026.8 the auth endpoint the old config pointed at (/outpost.goauthentik.io/auth/caddy) does not exist in the URL tree, so every unauthenticated subrequest got a 404 and browsers were served authentik's own Not-Found page (verified live, 2026-08-29). Pattern (one example; the other guarded vhosts are identical):

http://:8016 {
	@search host search.franksdank.cloud
	handle @search {
		# Unauthenticated: the login flow is served in place.
		# Authenticated: Authentik proxies on to internal_host (:8017).
		reverse_proxy authentik-server:9000 {
			header_up X-authentik-meta-outpost 7e39655b-58d3-4fe6-a80a-513f31051217
			header_up X-authentik-meta-provider 1
			header_up X-authentik-meta-app name=caddy
		}
	}
}

The meta headers tell authentik which outpost, provider and application are answering (the values come from the DB: outpost pk = the UUID embedded in AUTHENTIK_OUTPOST_TOKEN, provider pk 1 = embedded-outpost, app slug caddy). The provider's internal_host is http://caddy:8017, so authenticated traffic is handed back to Caddy's post-auth listener, which routes it straight to the app — the chain terminates there.

Guarded vhosts: the homepage (franksdank.cloud, www.franksdank.cloud), SearXNG, Hermes, Prometheus, RomM, Copyparty. The auth.franksdank.cloud vhost proxies to the server directly — it is the login, and it must never sit behind a challenge.

Configuration (secrets)

Two secrets live in the host .env (git-ignored; placeholders in .env.example):

  • AUTHENTIK_PG_PASS — the PostgreSQL password. Generate: openssl rand -base64 32.
  • AUTHENTIK_SECRET_KEY — the server's signing key. Generate: openssl rand -base64 48.

First-time setup

  1. ./scripts/deploy.sh dankclaw — pulls, starts the 3 services (postgres health-gated), reloads Caddy.
  2. Open https://auth.franksdank.cloud — the initial setup wizard runs. It creates the superuser (email + password) and generates the DB schema.
  3. In the admin interface: Directory → Applications → add a Proxy application (or an OAuth2/OpenID Connect provider) for each vhost you protect. The Caddy outpost reads the application's policy.
  4. For apps with native OIDC (Grafana, Gitea, OpenWebUI): add an OAuth2/OpenID Connect provider, create the app with the correct redirect URI, and paste the client_id / client_secret into the app's SSO settings.

Operations

  • Health: https://auth.franksdank.cloud/-/health/ (the server healthcheck probes this).
  • Logs: podman logs authentik-server / podman logs authentik-worker.
  • Backups: pg_dump the authentik-postgres database (the single source of truth). The /home/dank/authentik/ dir holds media + static files.
  • Upgrades: bump the image tag in dankclaw/compose.yaml, run ./scripts/deploy.sh dankclaw. Authentik migrates the schema on startup.

Failure modes

Symptom Cause Fix
Every vhost 502s at the outpost authentik-server down podman logs authentik-server; check postgres health
Login loops (302 → login → 302) trusted_proxies / X-Forwarded-* mismatch Confirm Caddy sets X-Forwarded-For; the outpost trusts private_ranges
"Invalid signature" on the outpost AUTHENTIK_SECRET_KEY differs between server and worker Both services read the same .env var; re-deploy
Postgres "password authentication failed" AUTHENTIK_PG_PASS changed after first init Reset via pg_ctl or re-init the data dir

Cross-references

  • Caddy — the reverse proxy that carries the forward_auth directives
  • Pi-hole — the LAN DNS that resolves auth.franksdank.cloud
  • Network Architecture — the flat naming scheme
  • Research: .docaudit/research/auth.md — the full provider comparison