Skip to main content

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:

  1. Forward-auth via Traefik — every *.home.helix9.org host that has the authentik middleware in Traefik gets its requests inspected by an Authentik outpost. See Traefik — Authentik Integration for the proxy-side view.
  2. 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

SettingValue
Nodepve02
LXC ID268
IP10.69.20.68/24
Gateway10.69.20.1
CPU2 cores
RAM2048 MB
Swap512 MB
Disk15 GB
Tagsservers, auth, authentik
Unprivilegedyes

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

ItemPath
Roleroles/authentik/
Playbookplaybooks/authentik.yml
Host varsinventory/host_vars/authentik/vars.yml
Vaultinventory/host_vars/authentik/vault.yml (encrypted)
Required vault keysvault_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.

UnitImageRole
authentik-postgresql.containerdocker.io/library/postgres:16-alpineStores users, groups, applications, sessions
authentik-redis.containerdocker.io/library/redis:alpineSession cache, task queue
authentik-server.containerghcr.io/goauthentik/server:2026.2.1HTTP frontend (port 9000)
authentik-worker.containerghcr.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

PathContainer mountContents
/srv/authentik/databasepostgresql:/var/lib/postgresql/dataPostgreSQL data dir
/srv/authentik/redisredis:/dataRedis RDB snapshots (--save 60 1)
/srv/authentik/mediaserver,worker:/mediaUser uploads, branding
/srv/authentik/certsworker:/certsCert material for outposts
/srv/authentik/custom-templatesserver,worker:/templatesCustom 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):

KeyPurpose
vault_authentik_pg_passPostgreSQL password
vault_authentik_secret_keyAuthentik 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:

  1. Enable + start postgres and redis.
  2. podman healthcheck run authentik-postgresql — retried up to 15 times at 10 s intervals.
  3. Flush stale netavark DNAT rules (workaround, see Gotchas).
  4. Enable server and worker.
  5. 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, or Forward 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
  • Application — create an Application referencing the provider above. Slug copyparty, name Copyparty. 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.

ApplicationTraefik hostProvider type
Copypartycopyparty.home.helix9.orgForward auth
Jellyfin (external)jellyfin.home.helix9.orgForward auth
Proxmox PVE01pve01.home.helix9.orgForward auth
Proxmox PVE02pve02.home.helix9.orgForward auth
OpenClawopenclaw.helix9.orgForward 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):

  1. 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>/).
  2. In the application: configure OIDC with those values. Common pitfall: the redirect URI must exactly match what the application sends, including the scheme.
  3. No Traefik middleware needed — the application handles its own auth state, just enforce TLS via Traefik.

Backup

The Authentik state is in three places:

  1. 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
  2. /srv/authentik/media — uploaded branding, custom flow images.
  3. Vaultvault_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_..._dnattasks/main.yml flushes a netavark DNAT chain before starting authentik-server, and the server unit has the same as ExecStartPre=. 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-server starts but curl :9000 returns 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.
  • unmask step — services can end up masked after a failed install or a systemctl mask for safe boot. The role unmasks them on every run (failed_when: false) so a re-run isn't blocked.
  • Image pull at first startasync: 360, poll: 0 means the playbook returns before the pull completes. The systemd start status will show activating for several minutes. Tail journalctl -u authentik-server to watch progress.
  • Outpost router priority — covered in Traefik docs. The authentik-outpost Traefik 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_KEY rotation — 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_group inside the server container.
  • Postgres major upgrades — going from postgres:16 to postgres:17 is not a drop-in. Postgres refuses to start on a data dir from a different major version. Either run pg_upgrade (manual, fiddly) or pg_dump → drop volume → pg_restore. Pin the major in the Quadlet (currently 16-alpine) to avoid surprises from AutoUpdate=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.