# 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](Pi-hole.md) 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
```mermaid
graph LR
U[Browser] -->|1. GET /search?q=x| A[Anubis :8923
bot challenge]
A -->|2. verified, dials internal listener| C[Caddy :8016
auth decision]
C -->|3. whole request + meta headers| S[authentik-server :9000
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
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):
```caddyfile
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](Caddy.md) — the reverse proxy that carries the forward_auth directives
- [Pi-hole](Pi-hole.md) — the LAN DNS that resolves `auth.franksdank.cloud`
- [Network Architecture](../Architecture/Network%20Architecture.md) — the flat naming scheme
- Research: `.docaudit/research/auth.md` — the full provider comparison