# 124 — `authentik` Central Identity Provider for the lab. Also runs the [split-horizon dnsmasq](../infrastructure/dns.md) — ergo "the SSO and DNS box". ## At a glance - **Hostname:** `authentik` - **IP:** `192.168.8.180` (statically configured — the only LXC with a static IP) - **Privilege:** privileged - **Resources:** 2 cores / 4 GiB RAM / 20 GiB rootfs - **Mounts:** none from `/mnt/library` - **Public hostname:** [`auth.hubris.network`](../infrastructure/dns.md) → [caddy (121)](121-caddy.md) → `:9000` - **Container DNS (in `/etc/pve/lxc/124.conf`):** `192.168.8.1 1.1.1.1` (router DNS plus a fallback added 2026-04-21 because router DNS flakes intermittently — Authentik is the resolver itself for the *rest* of the LAN, but its own LXC uses upstream). ## Authentik stack (`/opt/authentik/`) Upstream `docker-compose.yml` + `.env`. Services: `postgresql` (16-alpine), `server`, `worker`. Authentik 2026.x dropped the Redis dependency. - `.env` mode 600, **untracked**, holds `AUTHENTIK_SECRET_KEY` and `PG_PASS`. - `AUTHENTIK_TAG=2026.2.2` — pinned. Don't let it drift to `:latest`. Telemetry / update-check / error-reporting disabled. - Ports: 9000 (http), 9443 (https) on the LXC. - Embedded outpost lives at `/outpost.goauthentik.io/*` on the Authentik host — the forward-auth endpoint Caddy points at. - Stack is **not** git-tracked yet. If/when wiring auto-deploy: mirror the `mule-image` pattern (webhook receiver outside the app repo at `/opt/authentik-deploy/`). Repo `dtoro/authentik-conf` is reserved but not created. ## Forward-auth pattern (every gated app) - **One Proxy Provider per app.** Authentik enforces a UNIQUE constraint `application.provider_id`, so one Provider = one Application. "Domain-level" only means they share the cookie domain. Each provider in "Forward auth (domain level)" mode, External host `https://auth.hubris.network`, Cookie domain `hubris.network`. First one was `hubris-forward-auth` (Paperless). - **Authentication flow:** MUST be `default-authentication-flow` (NOT `default-source-authentication` — that's for IdP federation; gives `FlowNonApplicableException` + 404 on the authorize endpoint). - **Authorization flow:** `default-provider-authorization-implicit-consent` (or explicit). - **Application Launch URL** MUST be the full public URL `https://.hubris.network/` — outpost matches incoming `X-Forwarded-Host` against it. - Each Application MUST have at least one **policy/group/user binding** — zero bindings = outpost returns 404 on access. - **Restart Authentik after binding new apps to the outpost:** ``` pct exec 124 -- docker compose -f /opt/authentik/docker-compose.yml restart server worker ``` Caddy snippet `(authentik)` lives at the top of `/etc/caddy/Caddyfile`. Points at `http://192.168.8.180:9000` directly (NOT `https://auth.hubris.network`) to avoid hairpin TLS round-trip stripping `X-Forwarded-Host`. Must explicitly set `header_up X-Forwarded-Host {host}` in the forward-auth block. Used by gated sites with `import authentik`. ### Per-app username override pattern (Authentik) Used when the app's local user ID doesn't match the user's Authentik username (e.g., Nextcloud's `admin` ≠ Authentik's `dtoro`). 1. On the Authentik user: add attribute `_uid: ` (YAML, Directory → Users → Edit → Attributes). 2. Customization → Property Mappings → Create → **Scope Mapping** (not SAML): - Name: `-uid-override`, Scope name: `-uid`, Expression: ```python return {"nc_uid": user.attributes.get("_uid", user.username)} ``` - **Use a custom claim key** (e.g. `nc_uid`), not `preferred_username` — the default `profile` scope mapping emits `preferred_username` and will overwrite yours depending on ordering. 3. Attach the new scope to the provider (Providers → app → Scopes). 4. On the app side, point its OIDC UID-mapping setting at the custom claim. For Nextcloud: ``` occ user_oidc:provider --mapping-uid=nc_uid occ user_oidc:provider --scope="openid profile email -uid" ``` ### Bypass forward-auth for API paths (mobile apps) If the app has its own token-based API auth and a mobile client, API paths must bypass forward-auth — mobile apps can't follow the browser login redirect. Pattern in the Caddyfile site block: ``` paperless.hubris.network { tls { dns ionos {env.IONOS_AUTH_API_TOKEN} } @api path /api/* handle @api { reverse_proxy 192.168.8.130:8000 } handle { import authentik reverse_proxy 192.168.8.130:8000 } } ``` API paths to bypass per app: - [Paperless](103-paperless.md): `/api/*` (Bearer) - [Sonarr / Radarr / Lidarr / etc.](122-arriman.md): `/api/*` (X-Api-Key) - [qBittorrent](122-arriman.md): `/api/*` (session cookie from `/api/v2/auth/login`) - [SABnzbd](122-arriman.md): `/api?*` (apikey query param) — match `/api*` for query-string APIs - Homarr: no mobile client - [Portainer](105-apps.md): mobile uses same session auth as web; no bypass typically needed ### Backend trust of Authentik headers (skip the app's own login after SSO) - [**Paperless**](103-paperless.md): `PAPERLESS_ENABLE_HTTP_REMOTE_USER=true` and `PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME=HTTP_X_AUTHENTIK_USERNAME` in `/opt/paperless/paperless.conf`. Restart `paperless-webserver paperless-task-queue paperless-scheduler paperless-consumer`. Django auto-creates matching users on first SSO login; promote to superuser via existing admin UI. - Apps without header-auth support: users log in twice (SSO + app login). Acceptable but degraded UX. ## Per-app integration map | App | Type | Notes | | ----------------------------------------- | ---------------- | ----- | | [Paperless (103)](103-paperless.md) | Forward-auth + REMOTE_USER | `/api/*` bypass | | [Nextcloud (114)](114-nextcloud.md) | Native OIDC | `nc_uid` override; local dnsmasq required (Guzzle bypasses `/etc/hosts`) | | [mulita (120)](120-mule-images.md) | Native OIDC | `extra_hosts` override in compose | | [Booklore (105)](105-apps.md) | Native OIDC | Redirect URI `/oauth2-callback`; `extra_hosts` | | [Portainer (105)](105-apps.md) | Native OAuth2 | `portainer_uid` custom claim; `--trusted-origins` flag | | [WriteFreely (105)](105-apps.md) | Native OIDC | `[oauth.generic]` block; `extra_hosts` | | [qBittorrent (122)](122-arriman.md) | Forward-auth via IP whitelist | Reverse-proxy support enabled in qBit | | [Artifacto (105)](105-apps.md) | Forward-auth + gateway-secret auto-login | Public `/p/*` paths bypass | | [Home Assistant VM (108)](../vms/108-haos.md) | HACS `christiaangoossens/hass-oidc-auth` | `automatic_user_linking: true`, `default_redirect: true`. Supervisor DNS via `ha dns options`. | ## Netbird IdP integration — DEFERRED The `netbirdio/netbird-server` combined image has no config knobs for external OIDC. Verified in `combined/cmd/config.go` on main (v0.69.0): `AuthConfig` only exposes issuer + redirect URIs; `ToManagementConfig()` hardcodes `AuthAudience="netbird-dashboard"`, `UserIDClaim="sub"`, always calls `buildEmbeddedIdPConfig()`. Code comment: `"embedded IdP is always enabled in combined server"`. To wire Authentik into Netbird login/user-sync, must migrate to the legacy split stack (`netbirdio/management` + `signal` + `dashboard` + coturn) with the richer `management.json` schema. Sqlite management data should migrate but needs verification; peers stay connected via wireguard keys. **Pre-work already in place (keep for re-use):** - Provider `Provider for Netbird` + App `netbird`. Client ID `xZwVTFCsxWdBM3uIGS15wAAcVvsJiTtWdxVCEela`. Redirect URIs for `https://netbird.hubris.network/{nb-auth,nb-silent-auth}` and `http://localhost:53000/`. Scopes: `openid profile email offline_access goauthentik.io/api`. - Service account `netbird-service` in `authentik Admins` group, non-expiring API token `netbird-service-api`. - Netbird mgmt host (`82.165.190.79`) is now a peer on its own mesh. See [mesh](../infrastructure/mesh.md). ## DNS responsibility dnsmasq runs alongside Authentik on this LXC, listening on `192.168.8.180:53` + `127.0.0.1:53`, serving every `*.hubris.network` subdomain → `192.168.8.175`. **There is no wildcard** — every site needs an explicit `address=` entry. See [DNS split-horizon](../infrastructure/dns.md). ## Related - [DNS split-horizon](../infrastructure/dns.md) - [Caddy (121)](121-caddy.md) - [Mesh migration](../infrastructure/mesh.md) - Every gated app under [containers/index](index.md) ## Changelog ### 2026-04-28 — wiki entry created Initial documentation. ### 2026-04-22 — Phase 6 (Netbird IdP swap) deferred Combined netbird-server image can't take an external IdP. Pre-work in Authentik kept for later. Netbird mgmt host instead joined its own mesh as a peer (`100.122.165.149`) for split-horizon DNS access. ### 2026-04-22 — Artifacto, mulita, WriteFreely, Portainer wired Native OIDC for mulita / WriteFreely / Portainer; gateway-secret auto-login pattern for Artifacto. ### 2026-04-21 — deployed; Phases 1–5 complete LXC 124 provisioned, stack at `/opt/authentik`, public URL via Caddy, Paperless + Booklore + Nextcloud + Home Assistant wired. dnsmasq for split-horizon DNS lives on the same LXC.