Authentik — Identity Provider & SSO
Project: goauthentik.io
Host: authentik / authentik.home.lab
IP: 10.69.20.68
VLAN: 20 (SERVERS)
LXC ID: 268
Web UI: https://auth.home.helix9.org (via Traefik)
Role path: roles/authentik/
Playbook: playbooks/authentik.yml
Overview
Authentik is the SSO and identity provider for the home network. Every service that needs more than IP-based gating delegates authentication to it via Traefik forward-auth. It also serves as an OIDC / SAML provider for backends that speak those protocols natively.
Two interaction patterns are in active use:
- Forward-auth via Traefik — every
*.home.helix9.orghost that has theauthentikmiddleware in Traefik gets its requests inspected by an Authentik outpost. See Traefik — Authentik Integration for the proxy-side view. - Direct OIDC / SAML — services with built-in OIDC support (OneDev, future additions) can talk to Authentik directly using a per-application provider, no Traefik middleware required.
The two patterns coexist on the same Authentik instance.
Authentication Flow (Forward-Auth)
1. user → https://copyparty.home.helix9.org (browser)
2. Traefik → http://authentik:9000/.../auth/traefik (forward-auth probe, original Cookie + headers)
3a. session valid → 200 + X-authentik-* headers (Traefik continues to backend)
3b. no session → 302 /outpost.goauthentik.io/start?rd=<orig>
4. Authentik → login flow (password / WebAuthn / etc.)
5. session set (cookie scoped to .home.helix9.org)
6. redirect back to original URL
The authentik-outpost Traefik router (priority 1000, PathPrefix(/outpost.goauthentik.io/)) catches the redirect on whatever host the user was visiting and proxies to the same Authentik backend, so the login UI appears under the original hostname — cookies stay first-party.
Infrastructure
LXC Container
| Setting | Value |
|---|---|
| Node | pve02 |
| LXC ID | 268 |
| IP | 10.69.20.68/24 |
| Gateway | 10.69.20.1 |
| CPU | 2 cores |
| RAM | 2048 MB |
| Swap | 512 MB |
| Disk | 15 GB |
| Tags | servers, auth, authentik |
| Unprivileged | yes |
Provisioned via Terraform from inventory/host_vars/authentik/vars.yml. The container runs Podman as the container engine — Authentik itself is a four-container stack managed via systemd Quadlet units, not the host's package manager.
Ansible
| Item | Path |
|---|---|
| Role | roles/authentik/ |
| Playbook | playbooks/authentik.yml |
| Host vars | inventory/host_vars/authentik/vars.yml |
| Vault | inventory/host_vars/authentik/vault.yml (encrypted) |
| Required vault keys | vault_authentik_pg_pass, vault_authentik_secret_key |
Roles applied in order by playbooks/authentik.yml:
roles:
- common # base hardening
- ssh # SSH config
- users # ansible/admin users
- podman # podman + systemd integration
- authentik # Authentik-specific stack
Deploy:
ansible-playbook playbooks/authentik.yml
Container Topology
Authentik runs as four containers on a private Podman network (authentik-internal). Only the server container exposes ports outside the LXC.
┌──────────────────────┐
│ authentik-server │ 9000 → LXC :9000 (HTTP)
client ───► │ (web UI + API) │ 9443 → LXC :9443 (HTTPS, unused)
└──────┬───────────┬───┘
│ │
▼ ▼
┌────────────┐ ┌─────────┐
│ postgresql │ │ redis │
│ :5432 │ │ :6379 │
└────────────┘ └─────────┘
▲ ▲
│ │
┌──────┴───────────┴───┐
│ authentik-worker │ Podman socket mounted (blueprints)
│ (background jobs) │
└──────────────────────┘
Quadlet units
All four units live in /etc/containers/systemd/. Quadlet generates a corresponding <name>.service at boot.
| Unit | Image | Role |
|---|---|---|
authentik-postgresql.container | docker.io/library/postgres:16-alpine | Stores users, groups, applications, sessions |
authentik-redis.container | docker.io/library/redis:alpine | Session cache, task queue |
authentik-server.container | ghcr.io/goauthentik/server:2026.2.1 | HTTP frontend (port 9000) |
authentik-worker.container | ghcr.io/goauthentik/server:2026.2.1 (with Exec=worker) | Background tasks, blueprint reconciliation, outpost management |
Server and worker share the same image — only the entrypoint differs.
Network
authentik-internal.network is an empty Quadlet [Network] block — Podman creates a default-mode bridge. Postgres and Redis are reachable from server/worker by container name (authentik-postgresql, authentik-redis) but not from outside the LXC.
Persistent data
| Path | Container mount | Contents |
|---|---|---|
/srv/authentik/database | postgresql:/var/lib/postgresql/data | PostgreSQL data dir |
/srv/authentik/redis | redis:/data | Redis RDB snapshots (--save 60 1) |
/srv/authentik/media | server,worker:/media | User uploads, branding |
/srv/authentik/certs | worker:/certs | Cert material for outposts |
/srv/authentik/custom-templates | server,worker:/templates | Custom Authentik templates |
These directories are owned by root:root mode 0755. Container users inside Podman map to host UIDs that can read these paths because the LXC is unprivileged but the containers run rootless inside it.
Worker — Podman socket
The worker mounts the host Podman socket read-only:
Volume=/run/podman/podman.sock:/var/run/docker.sock:ro
This lets Authentik's blueprint engine and outpost manager observe / orchestrate sibling containers if needed (e.g. spawn an embedded outpost). Currently unused — outpost is the embedded one running inside the server container — but the mount stays for future flexibility.
Configuration
Environment file
Rendered to /srv/authentik/.env (mode 0600) by env.j2:
POSTGRES_DB=authentik
POSTGRES_USER=authentik
POSTGRES_PASSWORD=<from vault_authentik_pg_pass>
AUTHENTIK_POSTGRESQL__HOST=authentik-postgresql
AUTHENTIK_POSTGRESQL__NAME=authentik
AUTHENTIK_POSTGRESQL__USER=authentik
AUTHENTIK_POSTGRESQL__PASSWORD=<from vault_authentik_pg_pass>
AUTHENTIK_REDIS__HOST=authentik-redis
AUTHENTIK_SECRET_KEY=<from vault_authentik_secret_key>
AUTHENTIK_ERROR_REPORTING__ENABLED=false
The same file is loaded by all four containers via EnvironmentFile=.
Vault (sensitive)
inventory/host_vars/authentik/vault.yml (encrypted with ansible-vault):
| Key | Purpose |
|---|---|
vault_authentik_pg_pass | PostgreSQL password |
vault_authentik_secret_key | Authentik signing key (cookies, tokens). Rotating this invalidates every existing session and all OIDC tokens. |
Edit:
ansible-vault edit inventory/host_vars/authentik/vault.yml
Image updates
All four units have AutoUpdate=registry. To pull the latest tag of postgres/redis or the pinned Authentik image:
systemctl start podman-auto-update.service
To upgrade Authentik specifically, bump authentik_version in host_vars/authentik/vars.yml and re-run the playbook. Postgres migrations run automatically when the new server image starts.
Operations
Service start order
Postgres and Redis must be healthy before the server starts. The role enforces this via:
- Enable + start postgres and redis.
podman healthcheck run authentik-postgresql— retried up to 15 times at 10 s intervals.- Flush stale
netavarkDNAT rules (workaround, see Gotchas). - Enable server and worker.
- Start them async (
async: 360, poll: 0) — first image pull can take minutes.
The systemd units themselves also enforce ordering via Requires= / After=.
Health checks
# Container-level
podman healthcheck run authentik-postgresql
podman healthcheck run authentik-redis
# Authentik HTTP
curl -fsS http://localhost:9000/-/health/live/ # liveness
curl -fsS http://localhost:9000/-/health/ready/ # readiness (DB + Redis up)
# From outside the LXC
curl -fsS https://auth.home.helix9.org/-/health/ready/
The /ready/ endpoint is what Traefik should be hitting if a healthcheck is configured for the Authentik backend (currently not configured).
Logs
journalctl -u authentik-server -f
journalctl -u authentik-worker -f
journalctl -u authentik-postgresql -f
podman logs authentik-server
Restart
systemctl restart authentik-server authentik-worker
A full stack restart (Postgres included) is rarely needed — running it at scale takes ~60 s and drops every active session.
Initial bootstrap
After a clean install, the first-run setup URL is printed in the server logs:
journalctl -u authentik-server | grep -i "initial setup"
Visit that URL once to create the akadmin user and set its password. After that, all admin actions go through https://auth.home.helix9.org/if/admin/.
Application Setup (Traefik Forward-Auth)
For each service that needs SSO via Traefik:
1. In Authentik (Admin UI)
- Provider — create a Proxy Provider
- Type:
Forward auth (single application)for hostname-bound services, orForward auth (domain level)for shared cookies across multiple subdomains - External host:
https://copyparty.home.helix9.org(or whichever) - Internal host: leave empty — the actual backend is reached via Traefik, not Authentik
- Skip authentication: typically leave default
- Type:
- Application — create an Application referencing the provider above. Slug
copyparty, nameCopyparty. Bind a policy (e.g. require group membership) here. - Outpost — assign the Application to the embedded outpost. Authentik reconciles the outpost config automatically; no separate container needed.
2. In Traefik
Add the authentik middleware to the router (or the local-or-auth chain for LAN-bypass-with-fallback). See Traefik — Adding a New Service.
3. Verify
# From outside the LAN (mobile hotspot)
curl -I -L https://copyparty.home.helix9.org
# Expect 302 → /outpost.goauthentik.io/start?rd=...
If the redirect URL is missing or points to a different host, the outpost router priority in Traefik is wrong (must be >= everything else, currently 1000).
Currently registered applications
The applications below have a Traefik router with the authentik middleware. Check the actual Authentik admin UI for the live state — this list may drift.
| Application | Traefik host | Provider type |
|---|---|---|
| Copyparty | copyparty.home.helix9.org | Forward auth |
| Jellyfin (external) | jellyfin.home.helix9.org | Forward auth |
| Proxmox PVE01 | pve01.home.helix9.org | Forward auth |
| Proxmox PVE02 | pve02.home.helix9.org | Forward auth |
| OpenClaw | openclaw.helix9.org | Forward auth |
Mediastack services (Sonarr, Radarr, Sabnzbd, Seerr) are LAN-only and do not pass through Authentik — they rely on Traefik's IP allowlist instead.
OIDC / Direct Integration
For services that speak OIDC natively (OneDev planned, others future):
- In Authentik: create an OAuth2/OpenID Provider. Note the client ID, client secret, and the issuer URL (
https://auth.home.helix9.org/application/o/<slug>/). - In the application: configure OIDC with those values. Common pitfall: the redirect URI must exactly match what the application sends, including the scheme.
- No Traefik middleware needed — the application handles its own auth state, just enforce TLS via Traefik.
Backup
The Authentik state is in three places:
- PostgreSQL — users, groups, applications, providers, policies. Back up via
pg_dump:podman exec authentik-postgresql pg_dump -U authentik authentik | \gzip > /srv/authentik/backup/authentik-$(date +%Y%m%d).sql.gz /srv/authentik/media— uploaded branding, custom flow images.- Vault —
vault_authentik_secret_key. Without this, even a perfect DB restore won't validate existing sessions or signed tokens.
A scheduled pg_dump cron job is not yet configured — currently relying on PBS snapshots of the LXC volume. This is a gap; restoring an LXC snapshot mid-session is acceptable for personal use but loses any data written between the snapshot and the failure.
Known Gotchas
nft flush chain inet netavark nv_ae05fe19_..._dnat—tasks/main.ymlflushes a netavark DNAT chain before startingauthentik-server, and the server unit has the same asExecStartPre=. This works around a netavark bug where stale DNAT rules from a previous container instance prevent the new one from claiming:9000. The chain name (nv_ae05fe19_10_89_0_0_nm24_dnat) is deterministic from the network's subnet — if you ever change the Podman network CIDR, the chain name will drift and this command will silently no-op (failed_when: false). Symptom of the bug:authentik-serverstarts butcurl :9000returns connection refused.RuntimeMaxSec=7d— the server unit kills itself weekly. Restart-as-a-cure for a slow memory leak in long-running Authentik deployments. Sessions survive (state is in Postgres + Redis); only in-flight HTTP connections drop.unmaskstep — services can end up masked after a failed install or asystemctl maskfor safe boot. The role unmasks them on every run (failed_when: false) so a re-run isn't blocked.- Image pull at first start —
async: 360, poll: 0means the playbook returns before the pull completes. The systemd start status will showactivatingfor several minutes. Tailjournalctl -u authentik-serverto watch progress. - Outpost router priority — covered in Traefik docs. The
authentik-outpostTraefik router must outrank every per-service router (currently priority 1000) or login flows break silently — users get a 404 on the post-login redirect. AUTHENTIK_SECRET_KEYrotation — invalidates every active session and every issued OIDC/SAML token. Treat as a hard cutover, not a maintenance task. Rotate only on suspected compromise.- First-time setup URL — only valid until the first admin user is created, then the URL 404s. If you miss it, you can recreate via
ak create_admin_groupinside the server container. - Postgres major upgrades — going from
postgres:16topostgres:17is not a drop-in. Postgres refuses to start on a data dir from a different major version. Either runpg_upgrade(manual, fiddly) orpg_dump→ drop volume →pg_restore. Pin the major in the Quadlet (currently16-alpine) to avoid surprises fromAutoUpdate=registry. - Worker Podman socket — read-only, but still grants visibility into all sibling containers in the LXC. Anyone who pops the worker container has list-pods on the host. Acceptable trade-off for blueprint orchestration; flag if threat model changes.
Related Docs
- Traefik — proxy-side forward-auth integration, outpost routing
- Home Router — Static Host Mappings —
auth.home.helix9.orgresolution - Ansible Setup — vault structure, role conventions
- New Host — LXC provisioning workflow