docs(vault): fleet-wide fact re-verification pass (67 pages)
The docs-fact census wave re-read the live hosts and re-keyed every page it could: runtime/payload identities, ports, sizes, ledger figures, unit state, and the retirement banners that drifted as the fleet moved. The wave was interrupted mid-run, so this commit carries only what passed the integrity gates: every touched page parses (front matter or H1+status convention intact, balanced fences, no truncated tables, trailing newline), the docs_completeness and link-integrity seams are green, and bats is 384/384. Numbers that this session proved on live hosts are keyed to the measured value, not the page's earlier claim.
This commit is contained in:
1 parent
466fd477d8
commit
e7f0d93c86
67 files changed
+2619
-1152
No files matched your search
@@ -11,27 +11,43 @@ timestamp: 2026-09-16
|
||||
|
||||
Authentik is the single sign-on (SSO) and OpenID Connect (OIDC) provider for the
|
||||
homelab. It protects the Caddy vhosts that do not carry their own
|
||||
application-level authentication with its embedded proxy outpost. Apps that
|
||||
have their own login (Grafana, Gitea, OpenWebUI, Jellyfin, the *arr stack,
|
||||
Pi-hole) are not wrapped — they handle their own session.
|
||||
application-level authentication with its proxy outpost. Not wrapped: Gitea,
|
||||
OpenWebUI, Jellyfin (LDAP outpost), the *arr stack, Pi-hole (basicauth edge) —
|
||||
they handle their own session. Grafana is the exception that is both: it is a
|
||||
guarded vhost (`grafana-proxy`, pk 8, locked to Authentik SSO) *and* an OAuth2
|
||||
relying party (`grafana`, pk 9) (`dankclaw/Caddyfile:122-132`,
|
||||
`dankclaw/Caddyfile:374-377`; live provider table below).
|
||||
|
||||
- **Version:** `2026.8.3` (pinned in `dankclaw/authentik/quadlet/*.container`)
|
||||
- **Version:** `2026.8.3` (pinned in `dankclaw/authentik/quadlet/*.container`;
|
||||
the outpost's boot log reads `"Booting authentik" … "version": "2026.8.3"`,
|
||||
2026-10-18)
|
||||
- **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)
|
||||
- **LDAP outpost:** a 4th, quadlet-deployed unit (`authentik-outpost-ldap`)
|
||||
serves LDAP for Jellyfin — see the section below.
|
||||
- **Stack: five quadlet units, no compose.** `authentik-postgres`,
|
||||
`authentik-server`, `authentik-worker` (`deploy_authentik`,
|
||||
`scripts/deploy-remote.sh:980-1004`) plus two outposts — `authentik-outpost`
|
||||
(the Rust **proxy** outpost that actually serves `/outpost.goauthentik.io/*`,
|
||||
`deploy_authentik_outpost`, `scripts/deploy-remote.sh:1006-1018`) and
|
||||
`authentik-outpost-ldap` (`deploy_authentik_outpost_ldap`,
|
||||
`scripts/deploy-remote.sh:1020-1032`). All five report `generated` +
|
||||
`active`, `Restart=always`, `WantedBy=default.target`, with linger on
|
||||
(`systemctl --user`, 2026-10-18). The "3 compose services" wording is the
|
||||
pre-quadlet era, retired.
|
||||
- **LDAP outpost:** serves LDAP for Jellyfin — see the section below.
|
||||
|
||||
## 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, an outpost that terminates auth for reverse-proxied
|
||||
apps, 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. (The integration
|
||||
mechanism changed in 2026.x — see Architecture; the research file predates
|
||||
the pin.)
|
||||
The comparison that chose it (against Authelia, Keycloak, Zitadel, Ory) was
|
||||
recorded in `.docaudit/research/auth.md`, which no longer exists in the repo or
|
||||
in the host checkout (`ls`, 2026-10-18) *(unverified 2026-10-18: the
|
||||
comparison itself)*. In short: full OIDC with single-logout, a web GUI for all
|
||||
configuration, an outpost that terminates auth for reverse-proxied apps, and a
|
||||
footprint that fits on a 3-user homelab — measured idle today
|
||||
`podman stats --no-stream`: postgres 233.8 MB + server 739.1 MB + worker
|
||||
292.4 MB + outpost 294.3 MB + ldap outpost 46.6 MB ≈ **1.6 GB** across five
|
||||
containers (the older "~1–1.5 GB" figure was the three-service stack). Redis
|
||||
was removed in 2025.10; the integration mechanism changed again in 2026.x (see
|
||||
Architecture).
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -46,6 +62,25 @@ graph LR
|
||||
S --> PG[(postgres)]
|
||||
```
|
||||
|
||||
Who answers was re-measured 2026-10-18 from inside the `caddy` container with a
|
||||
tenant-matching `Host: search.franksdank.cloud`:
|
||||
`http://authentik-server:9000/outpost.goauthentik.io/start` → **302 into the
|
||||
authentication flow**, while `http://authentik-outpost:9000/…` answers **404**
|
||||
for `/outpost.goauthentik.io/*` *and* for `/if/flow/default-authentication-flow/`.
|
||||
So the **server process** still terminates the proxy-provider decision, exactly
|
||||
as Caddy's `:8016 → authentik-server:9000` hop assumes
|
||||
(`dankclaw/Caddyfile:93-98`). The `authentik-outpost` unit is installed,
|
||||
enabled (`generated`) and active, and it is the DB's `authentik Embedded
|
||||
Outpost` (`7e39655b-…`, nine providers attached) holding the websocket channel
|
||||
to core — but it serves no HTTP surface on the paths the chain needs. The unit
|
||||
header's stronger claim that the URL space "is served by the Rust worker
|
||||
binary, not the server"
|
||||
(`dankclaw/authentik/quadlet/authentik-outpost.container:6-14`) is **not** what
|
||||
this probe shows; do not point a new listener at `authentik-outpost:9000`
|
||||
without re-measuring. Note also that a probe without a tenant `Host` (e.g.
|
||||
`curl http://127.0.0.1:9000/…`) 404s on paths that work — that is the
|
||||
Host-routing rule below, not an outage.
|
||||
|
||||
Two mechanisms were disproved live (2026-08-29/30) and one is the actual
|
||||
contract:
|
||||
|
||||
@@ -97,7 +132,7 @@ http://:8016 {
|
||||
}
|
||||
```
|
||||
|
||||
The embedded outpost inside the server process decides by `Host`:
|
||||
The outpost decides by `Host`:
|
||||
no session → the provider's `authentication_flow` is served in place;
|
||||
session valid → the request is forwarded to the provider's `internal_host`
|
||||
(`http://caddy:8017`), where per-vhost `handle` blocks reach the apps.
|
||||
@@ -107,10 +142,15 @@ routing table, and the Authentik proxy-provider list must be the same set of
|
||||
hosts. A guarded host with no provider 404s at login time; a provider with no
|
||||
DNS record breaks off-LAN clients.
|
||||
|
||||
Live provider inventory (verified 2026-09-02), all attached to outpost
|
||||
`7e39655b-…` ("authentik Embedded Outpost"), all with
|
||||
`internal_host = http://caddy:8017`, all three flows set, and each linked to
|
||||
an Application (see the provisioning rules below):
|
||||
Live provider inventory (read from the `authentik-postgres` container with
|
||||
read-only `SELECT`s, 2026-10-18). Nine proxy providers, all attached to outpost
|
||||
`7e39655b-58d3-4fe6-a80a-513f31051217` ("authentik Embedded Outpost", 9
|
||||
members in `authentik_outposts_outpost_providers`), all with
|
||||
`internal_host = http://caddy:8017`, all carrying the **same three flow
|
||||
UUIDs** (`default-authentication-flow`,
|
||||
`default-provider-authorization-implicit-consent`,
|
||||
`default-provider-invalidation-flow`), and each backed by an Application
|
||||
(the `slug` column below):
|
||||
|
||||
| Provider pk | name | external_host | app slug |
|
||||
|---|---|---|---|
|
||||
@@ -122,13 +162,21 @@ an Application (see the provisioning rules below):
|
||||
| 7 | loki-proxy | `https://loki.franksdank.cloud/` | loki |
|
||||
| 8 | grafana-proxy | `https://grafana.franksdank.cloud/` | grafana-guard |
|
||||
| 10 | seerr-proxy | `https://seerr.franksdank.cloud/` | seerr-guard |
|
||||
| 13 | marreta-proxy | `https://marreta.franksdank.cloud` | marreta |
|
||||
|
||||
Provider 3 (`hermes-proxy`) still exists but is dormant — its vhost was
|
||||
decommissioned from Caddy on 2026-08-31. Provider 9 (`grafana`) and 12
|
||||
(`Seerr OIDC`) are OAuth2 relying-party providers, not proxy providers; the
|
||||
outpost does not deserialize them. Trailing slashes on `external_host` (the
|
||||
runbook-recipe style) and bare origins both intercept correctly — the outpost
|
||||
matches on hostname.
|
||||
That is exactly the Caddyfile's guarded set (`dankclaw/Caddyfile:283-423`), so
|
||||
the lockstep rule holds today. Two corrections against the 2026-09-02 table:
|
||||
**pk 13 (`marreta-proxy`) was added** when Marreta went onto the chain, and
|
||||
**pk 3 (`hermes-proxy`) no longer exists at all** — `select … where id=3`
|
||||
returns zero rows, so it is deleted, not "dormant"; its vhost survived but is
|
||||
exempt-by-construction (`dankclaw/Caddyfile:179-191`). OAuth2 relying parties,
|
||||
not proxy providers, so the proxy outpost does not deserialize them: pk 9
|
||||
`grafana` (`grant_types = {authorization_code}`), pk 12 `Seerr OIDC`
|
||||
(`grant_types = {}` — empty, i.e. still in the state the token-endpoint rule
|
||||
below rejects), and pk 14 `Open WebUI`
|
||||
(`grant_types = {authorization_code,refresh_token}`). Trailing slashes on
|
||||
`external_host` (the runbook-recipe style) and bare origins both intercept
|
||||
correctly — the outpost matches on hostname.
|
||||
|
||||
Two provisioning rules, each verified live by an outage:
|
||||
|
||||
@@ -188,47 +236,65 @@ implementation diverges from upstream in ways that bit us; all verified live:
|
||||
`{sub}` only — email/groups/entitlements claims cannot be delivered by this
|
||||
build until the evaluator is patched or upgraded. Clients consuming this
|
||||
provider must key identity off `sub`.
|
||||
- Scope mappings for this provider exist (`grafana-email-scope`,
|
||||
`grafana-profile-scope`, Python-syntax expressions) and will start emitting
|
||||
claims the moment the evaluator bug is fixed — no config change needed then.
|
||||
- Scope mappings for this provider exist — `grafana-email-scope` and
|
||||
`grafana-profile-scope` are both rows in `authentik_core_propertymapping`
|
||||
(read-only `SELECT`, 2026-10-18) — and will start emitting claims the moment
|
||||
the evaluator bug is fixed; no config change is needed then.
|
||||
|
||||
## LDAP outpost (Jellyfin auth, live 2026-09-01)
|
||||
|
||||
A fourth unit, `dankclaw/authentik/quadlet/authentik-outpost-ldap.container`
|
||||
→ `authentik-outpost-ldap.service`, serves LDAP for Jellyfin's LDAP-Auth
|
||||
plugin. The image is pinned by its **amd64 single-platform digest**
|
||||
(`ghcr.io/goauthentik/ldap@sha256:fa33bc8f522113830e1662f4c034352f9eb08e0a7d3da2ddadad9a4b33b4cbf4`):
|
||||
podman 5.7.0 refuses the
|
||||
`ghcr.io/goauthentik/ldap:2026.8.0` tag because the tag resolves to a
|
||||
*nested* manifest list (index → per-platform manifest lists) —
|
||||
`unexpectedly received a manifest list instead of a manifest for a single
|
||||
image`. Resolve once with `podman manifest inspect <image> | jq -r
|
||||
'.manifests[] | select(.platform.architecture=="amd64") | .digest'` and pin
|
||||
that digest.
|
||||
The second outpost — `dankclaw/authentik/quadlet/authentik-outpost-ldap.container`
|
||||
→ `authentik-outpost-ldap.service`, one of the five units in this stack —
|
||||
serves LDAP for Jellyfin's LDAP-Auth plugin (`enabled`=generated, `active`,
|
||||
`Restart=always`, container up, 2026-10-18). The image is pinned by its **amd64
|
||||
single-platform digest**
|
||||
`ghcr.io/goauthentik/ldap@sha256:2e7b259eb8c37faac916946ddf48ec681ee9d3a9f724fe8521bf161973f13cf9`
|
||||
(`dankclaw/authentik/quadlet/authentik-outpost-ldap.container:26`, matching the
|
||||
running image in `podman ps`, 2026-10-18). The digest in this page before 2026-10-18
|
||||
(`…fa33bc8f…`) is superseded. The reason for pinning a digest at all: podman
|
||||
5.7.0 refuses the plain tag because it resolves to a *nested* manifest list
|
||||
(index → per-platform manifest lists) — `unexpectedly received a manifest list
|
||||
instead of a manifest for a single image` — first hit on `:2026.8.0`, still the
|
||||
rule at the current `2026.8.3` pin. Resolve once with `podman manifest inspect
|
||||
<image> | jq -r '.manifests[] | select(.platform.architecture=="amd64") |
|
||||
.digest'` and pin that digest.
|
||||
|
||||
- Ports **389** (ldap) and **636** (ldaps) publish on the LAN; Jellyfin on
|
||||
Dankserver binds `192.168.0.143:389`. The outpost token travels in
|
||||
`AUTHENTIK_LDAP_OUTPOST_TOKEN` from the host `.env` (git-ignored).
|
||||
- Ports **389** (ldap) and **636** (ldaps) publish on the LAN
|
||||
(`PublishPort=389:3389`, `636:6636`,
|
||||
`dankclaw/authentik/quadlet/authentik-outpost-ldap.container:30-31`; `ss -ltnp`
|
||||
shows `0.0.0.0:389` and `0.0.0.0:636`, 2026-10-18) and Dankserver can reach
|
||||
them — a bare TCP connect from Dankserver to `192.168.0.143:389` succeeds
|
||||
(2026-10-18). The outpost token travels in `AUTHENTIK_LDAP_OUTPOST_TOKEN`
|
||||
from the host `dankclaw/.env` (git-ignored).
|
||||
|
||||
### Authentik-side objects (provisioned via `ak shell` ORM)
|
||||
|
||||
Read from the live database (read-only `SELECT`s against the
|
||||
`authentik-postgres` container, 2026-10-18):
|
||||
|
||||
| Object | Value |
|
||||
|---|---|
|
||||
| LDAP provider | `Jellyfin LDAP` (pk 11), base DN `dc=franksdank,dc=cloud` |
|
||||
| Bind DN | `cn=svc-jellyfin-ldap,ou=users,dc=franksdank,dc=cloud` |
|
||||
| Service account | `svc-jellyfin-ldap` — passwordless/unusable; authenticates by token |
|
||||
| Bind token | intent **`verification`**, non-expiring — its key is the LDAP bind password |
|
||||
| RBAC role | `svc-jellyfin-ldap-search` carrying object-level permission `authentik_providers_ldap.search_full_directory` on the provider |
|
||||
| Groups | `jellyfin_users`, `jellyfin_admins` |
|
||||
| Application | `jellyfin` (slug `jellyfin`) |
|
||||
| LDAP provider | `Jellyfin LDAP` (pk 11), base DN `dc=franksdank,dc=cloud`, `search_mode=direct`, `bind_mode=direct` |
|
||||
| Bind DN | `cn=svc-jellyfin-ldap,ou=users,dc=franksdank,dc=cloud` *(unverified 2026-10-18: base DN confirmed in the DB, the `ou=users` path was not probed — an LDAP bind needs the bind secret)* |
|
||||
| Service account | `svc-jellyfin-ldap` (user pk 8). **It is not passwordless**: its password field is a real hash, not Django's `!`-prefixed unusable marker (`starts_with(password,'!')` → `f`) — the page's earlier "passwordless/unusable" claim is wrong, which matters because an account with a usable password can sign in through the ordinary password flow *(unverified 2026-10-18: whether anyone holds that password)* |
|
||||
| Bind token | exactly one row for pk 8, **`intent=app_password`**, non-expiring — the page previously recorded `intent=verification`; the live value is `app_password` |
|
||||
| RBAC role | `svc-jellyfin-ldap-search`, carrying exactly one object-level permission `authentik_providers_ldap.search_full_directory` (verified via `guardian_roleobjectpermission`) |
|
||||
| Groups | `jellyfin_users`, `jellyfin_admins` (both exist in `authentik_core_group`) |
|
||||
| Application | name `Jellyfin`, **slug `jellyfin-ldap`** (the page previously said slug `jellyfin`, which does not exist) |
|
||||
|
||||
Hard-won rules, each verified live:
|
||||
Hard-won rules (the first is now contested by the live database):
|
||||
|
||||
- **Token intent is load-bearing.** A bind token with `intent=app_password`
|
||||
is rejected by the flow's password stage with `Invalid password`;
|
||||
`intent=verification` passes. The outpost's direct binder executes the
|
||||
provider's authorization flow, and that password stage only accepts
|
||||
verification-intent tokens as static passwords.
|
||||
- **Token intent: contested, do not trust either value blindly.** This page
|
||||
long asserted that a bind token with `intent=app_password` is rejected by the
|
||||
flow's password stage with `Invalid password` and only `intent=verification`
|
||||
passes. The live token for `svc-jellyfin-ldap` is `app_password`
|
||||
(`authentik_core_token`, 2026-10-18), so either the rule is wrong or the
|
||||
current Jellyfin bind path is not that token. Nothing observable settles it
|
||||
today: the LDAP outpost's retained log holds only its 5-minute
|
||||
`initialized direct binder` / `Update providers` config cycles and no bind
|
||||
activity *(unverified 2026-10-18: no bind was performed — reproducing it
|
||||
requires the bind secret)*. Re-verify with a real bind before changing the
|
||||
token's intent either way.
|
||||
- **Search needs `search_full_directory`.** Without the RBAC grant the
|
||||
outpost returns only the bind user itself — a `(memberOf=…)` filter then
|
||||
matches zero entries and the plugin logs `Found no users matching X`.
|
||||
@@ -236,19 +302,38 @@ Hard-won rules, each verified live:
|
||||
and retest.
|
||||
- `ak shell` import path for the provider model in 2026.8:
|
||||
`from authentik.providers.ldap.models import LDAPProvider`.
|
||||
*(unverified 2026-10-18: importing it outside a configured Django shell fails
|
||||
with `ImproperlyConfigured`, and no `ak shell` was driven against the live
|
||||
auth service for this check)*
|
||||
|
||||
## Configuration (secrets)
|
||||
|
||||
Two secrets live in the host `.env` (git-ignored; placeholders in
|
||||
`.env.example`):
|
||||
**Four** variables live in the host env file the units read,
|
||||
`/home/dank/serverAppConfigs/dankclaw/.env` (git-ignored; the quadlets expand
|
||||
`${…}` from it via `EnvironmentFile`):
|
||||
|
||||
- `AUTHENTIK_PG_PASS` — the PostgreSQL password. Generate: `openssl rand -base64 32`.
|
||||
- `AUTHENTIK_SECRET_KEY` — the server's signing key. Generate: `openssl rand -base64 48`.
|
||||
- `AUTHENTIK_OUTPOST_TOKEN` — proxy outpost token
|
||||
(`dankclaw/authentik/quadlet/authentik-outpost.container:34`).
|
||||
- `AUTHENTIK_LDAP_OUTPOST_TOKEN` — LDAP outpost token
|
||||
(`…/authentik-outpost-ldap.container:29`).
|
||||
|
||||
Only the first two have placeholders in `dankclaw/.env.example`; the two outpost
|
||||
tokens have no example entry (there is no repository-root `.env.example` —
|
||||
`find`, 2026-10-18). Rotating `AUTHENTIK_SECRET_KEY` fleet-wide invalidates
|
||||
every outpost session (see the ORM playbook).
|
||||
|
||||
## First-time setup
|
||||
|
||||
1. `./scripts/deploy.sh dankclaw` — pulls, starts the 3 services (postgres
|
||||
health-gated), reloads Caddy.
|
||||
1. `./scripts/deploy.sh dankclaw --service authentik-server` (any of
|
||||
`authentik-postgres|authentik-server|authentik-worker` runs
|
||||
`deploy_authentik`, `scripts/deploy-remote.sh:980-1004`) — installs the three
|
||||
core quadlets, starts postgres health-gated (`wait_container_healthy
|
||||
authentik-postgres 120`), then the server (300 s health wait) and the worker.
|
||||
The outposts are separate handlers: `--service authentik-outpost` and
|
||||
`--service authentik-outpost-ldap` (`scripts/deploy-remote.sh:1006-1032`,
|
||||
dispatch at `:1579-1588`).
|
||||
2. Open `https://auth.franksdank.cloud` — the initial setup wizard runs. It
|
||||
creates the superuser (email + password) and generates the DB schema.
|
||||
3. For every guarded vhost, in the admin interface: **Directory →
|
||||
@@ -257,20 +342,36 @@ Two secrets live in the host `.env` (git-ignored; placeholders in
|
||||
(`https://<host>`), `internal_host = http://caddy:8017`, and **all three**
|
||||
flows set (see the provisioning pitfall above). Attach the provider to the
|
||||
embedded outpost.
|
||||
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.
|
||||
4. For apps with native OIDC: 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. The relying parties that exist
|
||||
today are `grafana` (pk 9), `Seerr OIDC` (pk 12) and `Open WebUI` (pk 14);
|
||||
**Gitea has no provider row at all** (live provider table, 2026-10-18) — it
|
||||
authenticates itself and stays off the chain by design
|
||||
(`dankclaw/Caddyfile:110-120`).
|
||||
|
||||
## Operations
|
||||
|
||||
- **Health:** `https://auth.franksdank.cloud/-/health/` (the server healthcheck
|
||||
probes this).
|
||||
- **Health:** there is **no** HTTP health path on this build —
|
||||
`/-/health/`, `/-/live/` and `/api/-/health/` all answer **404**, on
|
||||
`127.0.0.1:9000` and through `https://auth.franksdank.cloud` alike
|
||||
(`curl`, 2026-10-18). The container's own probe is Django-side:
|
||||
`HealthCmd=python /manage.py check --database default`
|
||||
(`dankclaw/authentik/quadlet/authentik-server.container:36`), and
|
||||
`podman inspect authentik-server` reports `healthy`. A usable liveness URL is
|
||||
a flow page: `https://auth.franksdank.cloud/if/flow/default-user-settings-flow/`
|
||||
→ 200 (2026-10-18). The earlier "the server healthcheck probes `/-/health/`"
|
||||
line was wrong in both halves.
|
||||
- **Logs:** `podman logs authentik-server` / `podman logs authentik-worker`.
|
||||
- **Backups:** `pg_dump` the `authentik-postgres` database (the single source of
|
||||
truth). The `/home/dank/Containers/authentik/data` dir holds media + static files.
|
||||
- **Upgrades:** bump the image tag in the three units under
|
||||
`dankclaw/authentik/quadlet/`, run `./scripts/deploy.sh dankclaw`. Authentik
|
||||
migrates the schema on startup.
|
||||
truth). The `/home/dank/Containers/authentik/data` dir holds media + static
|
||||
files (mounted by all three app units).
|
||||
- **Upgrades:** bump the tag in **all three app units** under
|
||||
`dankclaw/authentik/quadlet/` and re-deploy; `authentik-outpost` carries the
|
||||
same tag (it runs the same image, `Exec=worker`) and `authentik-outpost-ldap`
|
||||
is **digest-pinned**, so its pin must be re-resolved for the new version (see
|
||||
the LDAP section) — bumping only the tag would leave the LDAP outpost behind.
|
||||
Authentik migrates the schema on startup.
|
||||
|
||||
## Self-serve user-settings flow (password change)
|
||||
|
||||
@@ -374,8 +475,11 @@ client → TLS edge → Anubis :8923 → outpost decision → internal
|
||||
`podman exec caddy sh -c "grep -n -A3 <handler> /etc/caddy/Caddyfile"` —
|
||||
after the deploy prints its reload line; confirm the reload actually ran,
|
||||
don't trust exit 0.
|
||||
- Outpost reachability: `podman exec authentik-server getent hosts caddy`
|
||||
resolves the compose DNS alias (10.89.1.x) that `internal_host` dials.
|
||||
- Outpost reachability: `podman exec authentik-server getent hosts caddy` →
|
||||
`10.89.1.8 caddy.dns.podman` (2026-10-18) — that is Caddy's address on the
|
||||
`dankclaw_default` podman network, the alias `internal_host` dials. It is a
|
||||
netavark allocation, not a pinned address and not a compose artifact (the
|
||||
compose stack is gone); never hard-code it.
|
||||
|
||||
**Pitfalls:** probing `http://localhost:8017/` from the host returns HTTP
|
||||
000 (connection refused) — the post-auth listener lives in the caddy
|
||||
@@ -403,22 +507,36 @@ EOF
|
||||
- The heredoc IS the program: `ak shell` reads all of stdin and execs it
|
||||
(heredoc-through-ssh quoting breaks; the raw-stdin pattern is the proven
|
||||
one).
|
||||
- Bootstrap prints JSON log lines, then
|
||||
`### authentik shell (2026.8.3)` and `229 objects imported automatically…`
|
||||
— pipe through `sed -n '/229 objects/,$p'` to drop the noise.
|
||||
- 229 models are pre-imported. Import paths (each wrong guess costs a failed
|
||||
exec): `User`, `Group`, `Application`, `ApplicationEntitlement`,
|
||||
`PropertyMapping` → `authentik.core.models`; `ProxyProvider` →
|
||||
- Bootstrap prints JSON log lines, then a shell banner and an
|
||||
"`NNN objects imported automatically…`" line — pipe through
|
||||
`sed -n '/objects imported/,$p'` to drop the noise *(unverified 2026-10-18:
|
||||
the exact banner text and the 229-object count — no `ak shell` was driven
|
||||
against the live auth service for this audit)*.
|
||||
- Import paths (each wrong guess costs a failed exec): `User`, `Group`,
|
||||
`Application`, `ApplicationEntitlement`, `PropertyMapping` →
|
||||
`authentik.core.models`; `ProxyProvider` →
|
||||
`authentik.providers.proxy.models` (NOT `authentik.outposts.models`);
|
||||
`LDAPProvider` → `authentik.providers.ldap.models`;
|
||||
`authentik.core.models.application` does NOT exist as an import path; there
|
||||
is no `authentik.policies.group` module in this build.
|
||||
- For live DB truth prefer the postgres container
|
||||
(`podman exec authentik-postgres psql -U authentik -d authentik -At -c
|
||||
"select * from …"`) — **query with `select *` and stop guessing column
|
||||
names**: 2026.x renamed columns (`Provider.objects.filter(type=…)` raises
|
||||
FieldError — `type` moved to subclass tables; the outpost `enabled` column
|
||||
may not exist under that name; provider pk is `oauth2provider_ptr_id`).
|
||||
*(unverified 2026-10-18: these module paths were not re-executed — importing
|
||||
outside a configured Django shell fails with `ImproperlyConfigured`)*
|
||||
- For live DB truth prefer the postgres container, and it works without any
|
||||
credential — the local socket is trusted, so a read-only audit is just
|
||||
`podman exec -i authentik-postgres psql -U authentik -d authentik -At -f -`
|
||||
with SQL on stdin (quoting SQL through ssh+zsh otherwise fights you). Verify
|
||||
before guessing: 2026.x renamed both tables and columns, and the models are
|
||||
MTI all the way down — `authentik_core_provider` holds `id`, `name` and the
|
||||
three `*_flow_id` columns; the proxy table is
|
||||
`authentik_providers_proxy_proxyprovider` (doubled `proxy`) and its PK column
|
||||
really is **`oauth2provider_ptr_id`** (proxy providers inherit the OAuth2
|
||||
table in 2026.8), the LDAP table is
|
||||
`authentik_providers_ldap_ldapprovider` with PK `provider_ptr_id`;
|
||||
`authentik_core_application` attaches to a provider through `provider_id`
|
||||
(its PK is `policybindingmodel_ptr_id`, not `id`), and `authentik_outposts_outpost`
|
||||
is keyed by `uuid`. So `Provider.objects.filter(type=…)` raising `FieldError`
|
||||
is the same story from the ORM side: `type` lives in the subclass tables.
|
||||
Never select secret columns (`client_secret`, `key`, `password`) into a log.
|
||||
|
||||
## User provisioning playbook (user-level account)
|
||||
|
||||
@@ -435,33 +553,42 @@ for g in ("Non-Admin Users", "jellyfin_users", "openwebui_users"):
|
||||
u.groups.add(Group.objects.get(name=g))
|
||||
```
|
||||
|
||||
`path` defaults to `users` (regular). The access map (verified live
|
||||
2026-10-04): there are NO per-app policy bindings on the proxy providers
|
||||
(all use the implicit-consent "Authorize Application" flow), so guarded-vhost
|
||||
access is decided entirely by GROUP MEMBERSHIP consumed downstream —
|
||||
`path` defaults to `users` (regular). The access map (re-checked against the
|
||||
live DB 2026-10-18): **every one of the nine proxy providers carries the same
|
||||
authorization flow UUID**, `default-provider-authorization-implicit-consent`
|
||||
(name "Authorize Application"), i.e. there are no per-app policy bindings to
|
||||
delta, so guarded-vhost access is decided entirely by GROUP MEMBERSHIP consumed
|
||||
downstream —
|
||||
|
||||
- `homepage-admins` → Caddy RBAC for homepage `/admin` (`X-Authentik-Groups`
|
||||
regex gate) and the homepage admin panel; regular users get only
|
||||
`category: user` panels from `services.json`.
|
||||
- `openwebui_users` / `openwebui_admins` → ApplicationEntitlements
|
||||
"Open WebUI Users"/"Open WebUI Admins" (bound by group), consumed by the
|
||||
"Open WebUI roles" property mapping → `roles` claim. Verify live with
|
||||
**"Open WebUI Users"** and **"Open WebUI Admins"**, both rows attached to the
|
||||
`Open WebUI` application (`authentik_core_applicationentitlement`,
|
||||
2026-10-18), consumed by the "Open WebUI roles" property mapping → `roles`
|
||||
claim *(unverified 2026-10-18: the group→entitlement binding and the mapping
|
||||
expression were not read; the mapping itself is not visible under that name
|
||||
in `authentik_core_propertymapping`)*. Verify live with
|
||||
`u.app_entitlements(app)`.
|
||||
- `jellyfin_users` / `jellyfin_admins` → Jellyfin via the LDAP outpost
|
||||
(filter `(memberOf=cn=jellyfin_users,ou=groups,dc=franksdank,dc=cloud)`).
|
||||
- `Non-Admin Users` = the plain-user marker group (precedents: qa-e2e, and
|
||||
every human account).
|
||||
- **The human-user template** (ridley, kassie, alex, artem, kirby, ry, nic,
|
||||
debo, freya, mitch, eric — mitch (pk 22) and eric (pk 23) created
|
||||
2026-10-15, kirby/ry/nic/debo/freya (pk 17–21) created 2026-10-09, kassie
|
||||
and alex created 2026-10-08, artem (pk 16) created 2026-10-07): `username` +
|
||||
`<user>@franksdank.cloud` + display name +
|
||||
`is_active=True` + the password set via `set_password`, membership in
|
||||
`Non-Admin Users` + `jellyfin_users` + `openwebui_users`. `alex` was
|
||||
recreated from scratch on 2026-10-08 into this shape (the old row had an
|
||||
empty email and no `openwebui_users`; audit first — the row carried no MFA
|
||||
device, token, entitlement, or proxy session, and events survive a user
|
||||
delete because `Event.user` is a denormalized JSON field, not a FK).
|
||||
debo, freya, mitch, eric). The pks in this page are confirmed against
|
||||
`authentik_core_user` on 2026-10-18: ridley 13, kassie 14, alex 15,
|
||||
artem 16, kirby 17, ry 18, nic 19, debo 20, freya 21, mitch 22, eric 23 —
|
||||
and every one carries the `<user>@franksdank.cloud` address the template
|
||||
prescribes. Creation dates (2026-10-07 → 2026-10-15) were not re-checked
|
||||
*(unverified 2026-10-18)*. Template: `username` +
|
||||
`<user>@franksdank.cloud` + display name + `is_active=True` + the password
|
||||
set via `set_password`, membership in `Non-Admin Users` + `jellyfin_users` +
|
||||
`openwebui_users`. `alex` was recreated from scratch on 2026-10-08 into this
|
||||
shape (the old row had an empty email and no `openwebui_users`; audit first —
|
||||
the row carried no MFA device, token, entitlement, or proxy session, and
|
||||
events survive a user delete because `Event.user` is a denormalized JSON
|
||||
field, not a FK).
|
||||
- EXCLUDE from a user-level account: `homepage-admins`, `jellyfin_admins`,
|
||||
`openwebui_admins`, `authentik Admins`, `authentik Read-only`.
|
||||
- Other user-category vhosts (SearXNG, Gitea, Calibre-Web, RomM, Copyparty,
|
||||
@@ -479,14 +606,24 @@ assert authenticate(request=None, username="ridley", password="nope") is None
|
||||
|
||||
Look flows up by slug, not name:
|
||||
`Flow.objects.get(slug="default-authentication-flow")` (its `.name` is
|
||||
"Welcome to authentik!" — `.get(name=…)` fails). Stage order:
|
||||
Identification(10) → Password(20) → AuthenticatorValidate(30; no-op with no
|
||||
enrolled device) → UserLogin(100). With no MFA enrolled, nothing else blocks.
|
||||
"Welcome to authentik!" — `.get(name=…)` fails; both confirmed from
|
||||
`authentik_flows_flow`, 2026-10-18). Stage order, read from
|
||||
`authentik_flows_flowstagebinding` the same day:
|
||||
`default-authentication-identification`(10) →
|
||||
`default-authentication-password`(20) →
|
||||
`default-authentication-mfa-validation`(30; no-op with no enrolled device) →
|
||||
`default-authentication-login`(100). With no MFA enrolled, nothing else blocks.
|
||||
|
||||
More field gotchas (each cost a failed exec): `ApplicationEntitlement.application`
|
||||
can be null (guard with `getattr`); `LDAPProvider` has
|
||||
`search_mode`/`bind_mode`/`base_dn`, lacks `search_base`/`of_groups`/
|
||||
`organization`; `OAuth2Provider` has no `.scopes`; `Outpost.config` is an
|
||||
`search_mode`/`bind_mode`/`base_dn` and lacks `search_base`/`of_groups`/
|
||||
`organization` — the backing table
|
||||
(`authentik_providers_ldap_ldapprovider`) has exactly `provider_ptr_id`,
|
||||
`base_dn`, `certificate_id`, `tls_server_name`, `gid_start_number`,
|
||||
`uid_start_number`, `search_mode`, `bind_mode`, `mfa_support`
|
||||
(`information_schema.columns`, 2026-10-18); `OAuth2Provider` has no `.scopes`
|
||||
(scopes are linked through `ProviderPropertyMapping`, which is why
|
||||
`filter(provider=…)` on a scope mapping is empty); `Outpost.config` is an
|
||||
`OutpostConfig` object, not a dict; `Policy` has no uniform `.policy_cls`
|
||||
(use `type(p).__name__`); `User` has no `.status`.
|
||||
|
||||
@@ -536,3 +673,5 @@ podman run --rm --network container:<container> --entrypoint /bin/busybox \
|
||||
- [Jellyfin](../Dankserver/Jellyfin.md) — consumes the LDAP outpost via its LDAP-Auth plugin
|
||||
- [Network Architecture](../Architecture/Network%20Architecture.md) — the flat naming scheme
|
||||
- Research: `.docaudit/research/auth.md` — the full provider comparison
|
||||
*(the file is gone from the repo and from the host checkout as of
|
||||
2026-10-18)*
|
||||
Reference in new issue
Block a user