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).
7.1 KiB
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 indankclaw/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
commandselects 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
./scripts/deploy.sh dankclaw— pulls, starts the 3 services (postgres health-gated), reloads Caddy.- Open
https://auth.franksdank.cloud— the initial setup wizard runs. It creates the superuser (email + password) and generates the DB schema. - 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.
- 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_secretinto 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_dumptheauthentik-postgresdatabase (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