docs(vault): one fact, one owner — repair the doubled pages, single-source the facts
Structural repair: - 13 pages were the same document twice (each exactly 2x its HEAD size), a second frontmatter block whose closing fence had been glued onto a body line by a frontmatter rewrite that re-read its own output. Un-doubled by cutting at the second frontmatter and keeping the copy whose body is a superset; the orphan census reports 0 lost tokens against HEAD. - log.md: one entry per date, bare `## YYYY-MM-DD` headings, newest first — 66 dated headings merged into 34, titles demoted to bold leads, every content line verified present. The file had been descending at the head and ascending at the tail. Ownership moves: - New `LLM/frank-medium.md` is the locator for the retired Dankserver endpoint: era windows quoted from each era page's own banner, no numbers restated, and the two unresolved date conflicts recorded instead of silently resolved. `frank-archon.md` links to it instead of restating the pre-rename window. - The four Pipeline-Parallelism stubs (frontmatter only, no body) are gone; `Multi-Node Runtimes.md` owns runtime construction, launch, mixed-vendor verdicts and cross-split spec decode, and the four links that pointed at the stubs were repointed at it or at the drafter page. - Authentik's three stub pages fold into `Authentik.md`, with the process detail and the outpost reference split out so the parent stays under the length hint. Verified corrections (host census + live probe, 2026-10-19): - The MXFP4 checkpoint two pages called the "on-disk rollback" is gone: /home/dank/models/ holds StillDeadcode, rad and tpl.jinja. Stated once, on the era page that owns that payload; the swap page now links to it. - frank-medium-radiance-mxfp4: the 4.24/7 accept rate belongs to the DFlash2 arm measured on the FP8 target, not to the MXFP4 era; the 3-family soak run census is kept once (155 requests) instead of twice (~120 and 155). - Fleet shapes re-confirmed against the live endpoints before touching any page: 3x200000 fp8 KV, 2x200704, 2x262144. Index rows are regenerated, not hand-carved. Net -1,849 vault lines.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
type: reference
|
||||
description: "Alertmanager receives firing rules from Prometheus, groups and dedupes them, and forwards each group as a webhook to the host-side Signal relay — which sends it through..."
|
||||
description: "Receives firing rules from Prometheus, groups and dedupes them, and forwards each group to the host-side Signal relay."
|
||||
tags:
|
||||
- infrastructure
|
||||
timestamp: 2026-09-17
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
---
|
||||
type: reference
|
||||
description: "Outpost, proxy-provider, OAuth2, LDAP and flow configuration for the homelab's Authentik SSO."
|
||||
tags:
|
||||
- infrastructure
|
||||
- authentik
|
||||
timestamp: 2026-10-19
|
||||
---
|
||||
|
||||
# Authentik Outpost Reference
|
||||
|
||||
Configuration lookup for the [[Authentik]] deployment: the live proxy-provider
|
||||
inventory behind the guarded vhosts, the provisioning rules the embedded outpost
|
||||
enforces, this fork's OAuth2 relying-party quirks, the LDAP provider objects, the
|
||||
self-serve user-settings flow, and the live database shape. What the service is,
|
||||
how it is deployed, wired and operated is [[Authentik]]; the three core
|
||||
containers are [[Authentik Processes]].
|
||||
|
||||
## Live proxy-provider inventory
|
||||
|
||||
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 |
|
||||
|---|---|---|---|
|
||||
| 1 | embedded-outpost | `https://www.franksdank.cloud` | caddy |
|
||||
| 2 | search-proxy | `https://search.franksdank.cloud` | search |
|
||||
| 4 | prometheus-proxy | `https://prometheus.franksdank.cloud` | prometheus |
|
||||
| 5 | romm-proxy | `https://romm.franksdank.cloud` | romm |
|
||||
| 6 | files-proxy | `https://files.franksdank.cloud` | files |
|
||||
| 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 |
|
||||
|
||||
That is exactly the Caddyfile's guarded set (`dankclaw/Caddyfile:283-423`), so
|
||||
the lockstep rule in [[Authentik]] 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.
|
||||
|
||||
## Provisioning rules for proxy providers
|
||||
|
||||
Two provisioning rules, each verified live by an outage:
|
||||
|
||||
1. **All three flows are required.** A ProxyProvider created via raw API/ORM
|
||||
with any of `authentication_flow`/`authorization_flow`/`invalidation_flow`
|
||||
null wedges the embedded outpost's config fetch: the Rust side rejects the
|
||||
whole config JSON (serde: `invalid type: null, expected a formatted UUID
|
||||
string`), logs `Failed to fetch outpost from API, retrying in 3 seconds`
|
||||
forever, and **everything behind the outpost dies at the next outpost
|
||||
reload** — including hosts whose providers were fine (verified 2026-09-02
|
||||
when three providers provisioned without any flows took down every guarded
|
||||
vhost after a server restart). Backfill all three flows (copy from pk 1:
|
||||
`default-authentication-flow`,
|
||||
`default-provider-authorization-implicit-consent` (the implicit-consent
|
||||
authorization flow, `default-invalidation-flow`) and the retry loop
|
||||
recovers on its next 3-second cycle. The silent killer: while the outpost
|
||||
process keeps its last good in-memory config, everything works — the
|
||||
outage only surfaces on restart/reload.
|
||||
2. **Every proxy provider needs an attached Application.** The outpost
|
||||
resolves the post-login redirect through the provider's application; a
|
||||
provider with no app completes the login (authentik session established)
|
||||
but bounces to authentik's own user UI (`/if/user/#/library`) instead of
|
||||
the app. No error is logged anywhere — the SSO "works" and the app never
|
||||
appears. Create the Application with `provider_id` set (the UI does this
|
||||
automatically; raw API/ORM drives must do it explicitly).
|
||||
|
||||
## OAuth2 relying-party quirks (this fork, verified live 2026-08-31)
|
||||
|
||||
The Grafana RP provider (`grafana`, pk 9, confidential client) was created and
|
||||
verified end-to-end via `ak shell` ORM drives. This fork's OAuth2 provider
|
||||
implementation diverges from upstream in ways that bit us; all verified live:
|
||||
|
||||
- **Application row required.** `OAuth2Provider.objects.create()` does not
|
||||
create the single-table-inheritance `Application` row; without it the flow
|
||||
executor can never complete an authorize request. Create both rows.
|
||||
- **`grant_types` defaults to empty.** Until the provider's `grant_types`
|
||||
contains `"authorization_code"` (enum value with underscore), the token
|
||||
endpoint rejects every exchange with `invalid_request`.
|
||||
- **Token endpoint is form-encoded + Basic-auth only.** A JSON body makes
|
||||
`parse_token_request` see no `grant_type` → `unsupported_grant_type`;
|
||||
POST-body client secrets are not accepted — authenticate the confidential
|
||||
client with `Authorization: Basic base64(client_id:client_secret)`.
|
||||
- **Provider resolution is by `client_id` query param** on the global
|
||||
`/application/o/authorize/` route; per-slug authorize paths do not exist in
|
||||
this build. Token/userinfo live at `/application/o/token/` and
|
||||
`/application/o/userinfo/?api=1`.
|
||||
- **Property-mapping evaluator is broken → userinfo returns `sub` only.**
|
||||
`PropertyMappingEvaluator.evaluate(*args, **kwargs)` forwards kwargs to
|
||||
`BaseEvaluator.evaluate(expression_source)`, so the first context kwarg
|
||||
becomes the *expression source* instead of the context; every call raises a
|
||||
swallowed `PropertyMappingExpressionException` and the claim is skipped.
|
||||
Scope-grant filtering has the same class of bug: `ScopeMapping` has no
|
||||
provider FK (linkage lives in the `ProviderPropertyMapping` through model),
|
||||
so `filter(provider=...)` always yields an empty queryset and tokens asked
|
||||
to carry scopes get zero. Net effect: userinfo/introspection answers with
|
||||
`{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` 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 provider objects (provisioned via `ak shell` ORM)
|
||||
|
||||
The LDAP outpost's own deployment — unit, image digest, published ports, token
|
||||
env var — is in [[Authentik]]. The objects it serves:
|
||||
|
||||
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`, `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`) — an earlier revision's "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 — an earlier revision 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`** (an earlier revision said slug `jellyfin`, which does not exist) |
|
||||
|
||||
Hard-won rules (the first is now contested by the live database):
|
||||
|
||||
- **Token intent: contested, do not trust either value blindly.** An earlier
|
||||
revision 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`.
|
||||
The permission is object-level on the provider; grant it via an RBAC role
|
||||
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)*
|
||||
|
||||
## Self-serve user-settings flow (password change)
|
||||
|
||||
Users change their own account details through the `default-user-settings-flow`
|
||||
(`https://auth.franksdank.cloud/if/flow/default-user-settings-flow/`) — the
|
||||
target of the homepage footer's "account" link. It is a `stage_configuration`
|
||||
designated flow, so it renders every bound `ConfigurableStage` for the logged-in
|
||||
user.
|
||||
|
||||
Stages, in execution order (the `FlowStageBinding.order` field, not the M2M
|
||||
insertion order):
|
||||
|
||||
| order | stage | type | what it does |
|
||||
|---|---|---|---|
|
||||
| 20 | `default-user-settings` | `PromptStage` | name / email / locale **+ password** |
|
||||
| 100 | `default-user-settings-write` | `UserWriteStage` | writes the prompt data to the user |
|
||||
|
||||
**Password change is a prompt field, not a stage.** The `UserWriteStage`
|
||||
writes whatever the preceding `PromptStage` collected: for the key `password`
|
||||
it calls `user.set_password(value)` (`stages/user_write/stage.py`). So the fix
|
||||
is a `password` field on the `default-user-settings` prompt
|
||||
(`field_key=password`, `type=password`, `required=False`, order 204) — NOT a
|
||||
`PasswordStage`. A `PasswordStage` is an *authentication* stage: its
|
||||
`validate_password` runs `authenticate()` against the backends to check the
|
||||
*current* password; it never sets a new one. (A `PasswordStage` was added on
|
||||
2026-09-25 and removed the same day for exactly this reason.)
|
||||
|
||||
Two ORM gotchas: `Flow.stages.add()` fails on this through model
|
||||
(`Can't bulk create a multi-table inherited model` — `FlowStageBinding` is
|
||||
MTI), so create the `FlowStageBinding` row directly with an explicit `order`;
|
||||
and a prompt field is a `Prompt` row linked through `PromptStageField`
|
||||
(`prompt_stage` + `prompt`; the order comes from `Prompt.order`).
|
||||
|
||||
**A blank password must not set an empty password.** `set_password("")` would
|
||||
make the account log in with no password, so a validation policy
|
||||
(`default-user-settings-no-empty-password`, bound to the prompt via
|
||||
`PromptStageValidationPolicy`) denies the form when `prompt_data["password"]`
|
||||
is `""` and allows it when set. Verified with the `PolicyEvaluator`
|
||||
(empty → deny, set → allow, absent → allow) and end-to-end by driving
|
||||
`UserWriteStageView.update_user` on a throwaway user (new password valid, old
|
||||
invalid).
|
||||
|
||||
## Live database shape
|
||||
|
||||
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.
|
||||
|
||||
## Cross-references
|
||||
|
||||
- [[Authentik]] — what the service is, its deployment, wiring, operations and
|
||||
failure modes
|
||||
- [[Authentik Processes]] — the postgres, server and worker containers
|
||||
- [[Caddy]] — the reverse proxy carrying the `:8016`/`:8017` auth-chain listeners
|
||||
- [[Jellyfin]] — consumes the LDAP outpost via its LDAP-Auth plugin
|
||||
- [[Grafana]] — guarded vhost *and* OAuth2 relying party of this provider
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
type: reference
|
||||
description: "The three core Authentik containers — database, server and worker — with their units, images, roles and probes."
|
||||
tags:
|
||||
- infrastructure
|
||||
- authentik
|
||||
- postgres
|
||||
timestamp: 2026-10-19
|
||||
---
|
||||
|
||||
# Authentik Processes
|
||||
|
||||
The three core containers of the [[Authentik]] stack on Dankclaw. The two
|
||||
outpost units are not covered here — they live on [[Authentik]] and
|
||||
[[Authentik Outpost Reference]]. Server and worker run the same image; the
|
||||
`command` selects the role (`server` / `worker`). Data persists under
|
||||
`/home/dank/Containers/authentik/` as bind mounts (repo policy — no named
|
||||
volumes), moved from `/home/dank/authentik/` in the 2026-09-01 quadlet
|
||||
standardization; the retired stubs this page replaces recorded the pre-move
|
||||
paths `/home/dank/authentik/data` (server and worker) and
|
||||
`/home/dank/authentik/postgres` (database).
|
||||
|
||||
## authentik-postgres
|
||||
|
||||
The PostgreSQL 16 database for Authentik — the only stateful store in the stack
|
||||
(users, flows, sessions, events). Sessions, caching, and WebSockets moved to
|
||||
PostgreSQL in 2025.10 (Redis removed).
|
||||
|
||||
- **Image:** `docker.io/library/postgres:16-alpine`
|
||||
- **Data:** `/home/dank/Containers/authentik/postgres` (bind mount; no named
|
||||
volumes)
|
||||
- **Health:** `pg_isready -d authentik -U authentik`
|
||||
- **Retired wording:** the stub called this a "Compose service … in
|
||||
`dankclaw/compose.yaml`"; the compose stack is gone and the unit is a quadlet.
|
||||
- The side-by-side with `gitea-postgres` (network, published ports, secret
|
||||
source, boot persistence) is owned by [[Postgres]].
|
||||
|
||||
## authentik-server
|
||||
|
||||
The Authentik server — the core, the admin API, and the embedded Caddy outpost.
|
||||
It also serves the login page at `https://auth.franksdank.cloud`.
|
||||
|
||||
- **Unit:** `dankclaw/authentik/quadlet/authentik-server.container`
|
||||
(`authentik-server.service`)
|
||||
- **Image:** `ghcr.io/goauthentik/server:2026.8.3` (`command: server`)
|
||||
- **Port:** `9000` (published) — the outpost, the admin API and the login UI all
|
||||
live here
|
||||
- **Data:** `/home/dank/Containers/authentik/data` (bind mount, shared with the
|
||||
worker)
|
||||
- **Health:** recorded by the retired stub as
|
||||
`http://localhost:9000/-/health/`; superseded — [[Authentik]] → Operations
|
||||
owns the live probe.
|
||||
- **Retired wording:** the stub described "the Caddy `forward_auth` vhosts"
|
||||
proxying here; the chain Caddy actually runs is [[Authentik]] → Caddy
|
||||
integration.
|
||||
|
||||
## authentik-worker
|
||||
|
||||
The Authentik worker — background tasks: event processing, provider
|
||||
synchronization, scheduled flows, and certificate renewal.
|
||||
|
||||
- **Unit:** `dankclaw/authentik/quadlet/authentik-worker.container`
|
||||
(`authentik-worker.service`)
|
||||
- **Image:** `ghcr.io/goauthentik/server:2026.8.3` (`command: worker`)
|
||||
- **Runs as:** `0:0` (rootless: container root maps to the host user that owns
|
||||
the data dir)
|
||||
- **Data:** `/home/dank/Containers/authentik/data` (bind mount, shared with the
|
||||
server)
|
||||
- **No port** — the worker has no HTTP endpoint.
|
||||
|
||||
## Cross-references
|
||||
|
||||
- [[Authentik]] — what the service is, its deployment, wiring, operations and
|
||||
failure modes
|
||||
- [[Authentik Outpost Reference]] — provider, flow and token configuration
|
||||
- [[Postgres]] — both PostgreSQL clusters on Dankclaw, side by side
|
||||
@@ -16,7 +16,7 @@ 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).
|
||||
`dankclaw/Caddyfile:374-377`; live provider table in [[Authentik Outpost Reference]]).
|
||||
|
||||
- **Version:** `2026.8.3` (pinned in `dankclaw/authentik/quadlet/*.container`;
|
||||
the outpost's boot log reads `"Booting authentik" … "version": "2026.8.3"`,
|
||||
@@ -35,6 +35,13 @@ relying party (`grafana`, pk 9) (`dankclaw/Caddyfile:122-132`,
|
||||
pre-quadlet era, retired.
|
||||
- **LDAP outpost:** serves LDAP for Jellyfin — see the section below.
|
||||
|
||||
**Split off from this page:** [[Authentik Processes]] owns the per-container
|
||||
facts for `authentik-postgres`, `authentik-server` and `authentik-worker`;
|
||||
[[Authentik Outpost Reference]] owns the provider/flow/token lookup material —
|
||||
the live proxy-provider inventory, the proxy-provider provisioning rules, the
|
||||
OAuth2 relying-party quirks, the LDAP provider objects, the user-settings flow
|
||||
design, and the live database shape.
|
||||
|
||||
## Why Authentik
|
||||
|
||||
The comparison that chose it (against Authelia, Keycloak, Zitadel, Ory) was
|
||||
@@ -100,23 +107,10 @@ contract:
|
||||
The chain terminates there — nothing dials back into Authentik from
|
||||
`:8017`.
|
||||
|
||||
## The three compose services
|
||||
## The stack's processes
|
||||
|
||||
| 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.3` |
|
||||
| `authentik-worker` | Background tasks (event processing, provider sync, flows) | `ghcr.io/goauthentik/server:2026.8.3` |
|
||||
|
||||
- **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/Containers/authentik/{data,postgres}`
|
||||
(bind mounts, repo policy — no named volumes; moved from `/home/dank/authentik/`
|
||||
in the 2026-09-01 quadlet standardization).
|
||||
Per-container facts — unit path, image and role, port, run-as, data directory,
|
||||
health probe — are owned by [[Authentik Processes]].
|
||||
|
||||
## Caddy integration
|
||||
|
||||
@@ -142,104 +136,9 @@ 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 (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 |
|
||||
|---|---|---|---|
|
||||
| 1 | embedded-outpost | `https://www.franksdank.cloud` | caddy |
|
||||
| 2 | search-proxy | `https://search.franksdank.cloud` | search |
|
||||
| 4 | prometheus-proxy | `https://prometheus.franksdank.cloud` | prometheus |
|
||||
| 5 | romm-proxy | `https://romm.franksdank.cloud` | romm |
|
||||
| 6 | files-proxy | `https://files.franksdank.cloud` | files |
|
||||
| 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 |
|
||||
|
||||
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:
|
||||
|
||||
1. **All three flows are required.** A ProxyProvider created via raw API/ORM
|
||||
with any of `authentication_flow`/`authorization_flow`/`invalidation_flow`
|
||||
null wedges the embedded outpost's config fetch: the Rust side rejects the
|
||||
whole config JSON (serde: `invalid type: null, expected a formatted UUID
|
||||
string`), logs `Failed to fetch outpost from API, retrying in 3 seconds`
|
||||
forever, and **everything behind the outpost dies at the next outpost
|
||||
reload** — including hosts whose providers were fine (verified 2026-09-02
|
||||
when three providers provisioned without any flows took down every guarded
|
||||
vhost after a server restart). Backfill all three flows (copy from pk 1:
|
||||
`default-authentication-flow`,
|
||||
`default-provider-authorization-implicit-consent` (the implicit-consent
|
||||
authorization flow, `default-invalidation-flow`) and the retry loop
|
||||
recovers on its next 3-second cycle. The silent killer: while the outpost
|
||||
process keeps its last good in-memory config, everything works — the
|
||||
outage only surfaces on restart/reload.
|
||||
2. **Every proxy provider needs an attached Application.** The outpost
|
||||
resolves the post-login redirect through the provider's application; a
|
||||
provider with no app completes the login (authentik session established)
|
||||
but bounces to authentik's own user UI (`/if/user/#/library`) instead of
|
||||
the app. No error is logged anywhere — the SSO "works" and the app never
|
||||
appears. Create the Application with `provider_id` set (the UI does this
|
||||
automatically; raw API/ORM drives must do it explicitly).
|
||||
|
||||
|
||||
## OAuth2 relying-party quirks (this fork, verified live 2026-08-31)
|
||||
|
||||
The Grafana RP provider (`grafana`, pk 9, confidential client) was created and
|
||||
verified end-to-end via `ak shell` ORM drives. This fork's OAuth2 provider
|
||||
implementation diverges from upstream in ways that bit us; all verified live:
|
||||
|
||||
- **Application row required.** `OAuth2Provider.objects.create()` does not
|
||||
create the single-table-inheritance `Application` row; without it the flow
|
||||
executor can never complete an authorize request. Create both rows.
|
||||
- **`grant_types` defaults to empty.** Until the provider's `grant_types`
|
||||
contains `"authorization_code"` (enum value with underscore), the token
|
||||
endpoint rejects every exchange with `invalid_request`.
|
||||
- **Token endpoint is form-encoded + Basic-auth only.** A JSON body makes
|
||||
`parse_token_request` see no `grant_type` → `unsupported_grant_type`;
|
||||
POST-body client secrets are not accepted — authenticate the confidential
|
||||
client with `Authorization: Basic base64(client_id:client_secret)`.
|
||||
- **Provider resolution is by `client_id` query param** on the global
|
||||
`/application/o/authorize/` route; per-slug authorize paths do not exist in
|
||||
this build. Token/userinfo live at `/application/o/token/` and
|
||||
`/application/o/userinfo/?api=1`.
|
||||
- **Property-mapping evaluator is broken → userinfo returns `sub` only.**
|
||||
`PropertyMappingEvaluator.evaluate(*args, **kwargs)` forwards kwargs to
|
||||
`BaseEvaluator.evaluate(expression_source)`, so the first context kwarg
|
||||
becomes the *expression source* instead of the context; every call raises a
|
||||
swallowed `PropertyMappingExpressionException` and the claim is skipped.
|
||||
Scope-grant filtering has the same class of bug: `ScopeMapping` has no
|
||||
provider FK (linkage lives in the `ProviderPropertyMapping` through model),
|
||||
so `filter(provider=...)` always yields an empty queryset and tokens asked
|
||||
to carry scopes get zero. Net effect: userinfo/introspection answers with
|
||||
`{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` 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.
|
||||
The live proxy-provider inventory (the per-vhost `external_host` → app mapping),
|
||||
the two provisioning rules the outpost enforces, and this fork's OAuth2
|
||||
relying-party quirks are reference lookup: [[Authentik Outpost Reference]].
|
||||
|
||||
## LDAP outpost (Jellyfin auth, live 2026-09-01)
|
||||
|
||||
@@ -267,44 +166,9 @@ rule at the current `2026.8.3` pin. Resolve once with `podman manifest inspect
|
||||
(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`, `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 (the first is now contested by the live database):
|
||||
|
||||
- **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`.
|
||||
The permission is object-level on the provider; grant it via an RBAC role
|
||||
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)*
|
||||
The objects behind it — LDAP provider, bind DN, service account and bind token,
|
||||
RBAC grant, groups, application slug — and the hard-won bind rules are in
|
||||
[[Authentik Outpost Reference]].
|
||||
|
||||
## Configuration (secrets)
|
||||
|
||||
@@ -340,7 +204,8 @@ every outpost session (see the ORM playbook).
|
||||
Applications** → add an application + a **Proxy** provider whose
|
||||
`external_host` is exactly the browser-facing origin
|
||||
(`https://<host>`), `internal_host = http://caddy:8017`, and **all three**
|
||||
flows set (see the provisioning pitfall above). Attach the provider to the
|
||||
flows set (see the provisioning rules in
|
||||
[[Authentik Outpost Reference]]). Attach the provider to the
|
||||
embedded outpost.
|
||||
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` /
|
||||
@@ -373,47 +238,6 @@ every outpost session (see the ORM playbook).
|
||||
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)
|
||||
|
||||
Users change their own account details through the `default-user-settings-flow`
|
||||
(`https://auth.franksdank.cloud/if/flow/default-user-settings-flow/`) — the
|
||||
target of the homepage footer's "account" link. It is a `stage_configuration`
|
||||
designated flow, so it renders every bound `ConfigurableStage` for the logged-in
|
||||
user.
|
||||
|
||||
Stages, in execution order (the `FlowStageBinding.order` field, not the M2M
|
||||
insertion order):
|
||||
|
||||
| order | stage | type | what it does |
|
||||
|---|---|---|---|
|
||||
| 20 | `default-user-settings` | `PromptStage` | name / email / locale **+ password** |
|
||||
| 100 | `default-user-settings-write` | `UserWriteStage` | writes the prompt data to the user |
|
||||
|
||||
**Password change is a prompt field, not a stage.** The `UserWriteStage`
|
||||
writes whatever the preceding `PromptStage` collected: for the key `password`
|
||||
it calls `user.set_password(value)` (`stages/user_write/stage.py`). So the fix
|
||||
is a `password` field on the `default-user-settings` prompt
|
||||
(`field_key=password`, `type=password`, `required=False`, order 204) — NOT a
|
||||
`PasswordStage`. A `PasswordStage` is an *authentication* stage: its
|
||||
`validate_password` runs `authenticate()` against the backends to check the
|
||||
*current* password; it never sets a new one. (A `PasswordStage` was added on
|
||||
2026-09-25 and removed the same day for exactly this reason.)
|
||||
|
||||
Two ORM gotchas: `Flow.stages.add()` fails on this through model
|
||||
(`Can't bulk create a multi-table inherited model` — `FlowStageBinding` is
|
||||
MTI), so create the `FlowStageBinding` row directly with an explicit `order`;
|
||||
and a prompt field is a `Prompt` row linked through `PromptStageField`
|
||||
(`prompt_stage` + `prompt`; the order comes from `Prompt.order`).
|
||||
|
||||
**A blank password must not set an empty password.** `set_password("")` would
|
||||
make the account log in with no password, so a validation policy
|
||||
(`default-user-settings-no-empty-password`, bound to the prompt via
|
||||
`PromptStageValidationPolicy`) denies the form when `prompt_data["password"]`
|
||||
is `""` and allows it when set. Verified with the `PolicyEvaluator`
|
||||
(empty → deny, set → allow, absent → allow) and end-to-end by driving
|
||||
`UserWriteStageView.update_user` on a throwaway user (new password valid, old
|
||||
invalid).
|
||||
|
||||
## Outpost claim cache (`proxysession`) — 403 after a group add
|
||||
|
||||
The embedded outpost does not re-evaluate group membership per request. Each
|
||||
@@ -457,7 +281,7 @@ client → TLS edge → Anubis :8923 → outpost decision → internal
|
||||
(the inventory table carries both bare origins and trailing slashes;
|
||||
routing matches on hostname, verified 2026-09-02).
|
||||
`internal_host = http://caddy:8017`, all three flows set, and an
|
||||
Application attached — the two provisioning rules above.
|
||||
Application attached — the two provisioning rules in [[Authentik Outpost Reference]].
|
||||
2. **Route flip in ONE commit/deploy**: the vhost becomes
|
||||
`reverse_proxy anubis:8923` with `header_up X-Real-Ip {remote_host}`, and
|
||||
inside the `http://:8017` listener add `@<svc> host <vhost>` +
|
||||
@@ -521,22 +345,9 @@ EOF
|
||||
is no `authentik.policies.group` module in this build.
|
||||
*(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.
|
||||
- The live database's table and column names (the 2026.x renames, the MTI parent
|
||||
keys, the read-only `psql` audit recipe) are reference lookup:
|
||||
[[Authentik Outpost Reference]].
|
||||
|
||||
## User provisioning playbook (user-level account)
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ Grafana responds on `/api/health` with 200 and `"database":"ok"`; measured 2026-
|
||||
client_id `grafana-21ca190cde75d60f`, global authorize/token/userinfo paths,
|
||||
Basic client auth, form-encoded token body, and the fork's userinfo-claims
|
||||
bug that leaves identity at the `sub` hash only) is in
|
||||
[[Authentik]] → "OAuth2 relying-party quirks". Consequences for Grafana:
|
||||
[[Authentik Outpost Reference]] → "OAuth2 relying-party quirks". Consequences for Grafana:
|
||||
The unit sets `ALLOW_SIGN_UP=false`, `AUTO_UPSERT=true`, `ALLOW_ASSIGN_ORG=true`,
|
||||
`ORG_ID=1` and **no** role-attribute mapping (`grafana.container:52-55`), so
|
||||
users land as Viewer with `login = <sub hex>`; no email/groups/role claims
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
type: reference
|
||||
title: Homepage Status Probes
|
||||
description: The homepage TUI status page — how probe targets are chosen (host-netns vantage), the three false-down shapes, triage procedure, local render check, deploy, and the e2e gate interactions (asleep Mac mini, raw-IP matrix).
|
||||
description: "How probe targets are chosen, the three false-down shapes, the triage procedure, and the e2e gate interactions."
|
||||
tags: [homepage, status, probes, e2e, homepage-status]
|
||||
timestamp: 2026-10-05T00:00:00-07:00
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
type: reference
|
||||
description: "A password-protected web site on Dankclaw lists the services that are up, with a short description of each and a link to both the service and its usage documentation."
|
||||
description: "The password-protected service list on Dankclaw: what each card shows and where its data comes from."
|
||||
tags:
|
||||
- infrastructure
|
||||
- homepage
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
type: reference
|
||||
title: Observability Provisioning
|
||||
description: How Prometheus config/rules and Grafana datasources/dashboards are provisioned as code in serverAppConfigs — the config triad, deploy copiers, planner mappings, seam tests, entry points for each change type, and API verification.
|
||||
description: "Monitoring config as code: the config triad, deploy copiers, planner mappings, and where each change type lands."
|
||||
tags: [observability, prometheus, grafana, provisioning, deployment]
|
||||
timestamp: 2026-10-05T00:00:00-07:00
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
type: reference
|
||||
title: "Observability on Dankclaw"
|
||||
description: "The Dankclaw observability stack — Prometheus, Grafana, Loki, Alloy, node-exporter and Alertmanager (Signal delivery): what runs, how it is deployed, how it is exposed, and what is still unconfigured."
|
||||
description: "The observability stack: what runs, how it is deployed, how it is exposed, and what is still unconfigured."
|
||||
tags: [observability, prometheus, grafana, loki, alloy, node-exporter, quadlet, dankclaw]
|
||||
timestamp: 2026-09-11
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
type: reference
|
||||
description: "Prometheus collects metrics from every service on Dankclaw by scraping their `/metrics` endpoints, and feeds the provisioned Grafana datasource. Down targets today are structural, not outages."
|
||||
description: "Scrapes each service's metrics endpoint and feeds the provisioned Grafana datasource; down targets are structural."
|
||||
tags:
|
||||
- infrastructure
|
||||
- prometheus
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
type: reference
|
||||
description: "The PostgreSQL 16 database for [Authentik](Authentik.md) — the homelab's SSO /"
|
||||
tags:
|
||||
- infrastructure
|
||||
- authentik
|
||||
- postgres
|
||||
timestamp: 2026-08-28
|
||||
---
|
||||
|
||||
# authentik-postgres
|
||||
|
||||
The PostgreSQL 16 database for [Authentik](Authentik.md) — the homelab's SSO /
|
||||
OIDC provider. It is the only stateful store in the stack (users, flows,
|
||||
sessions, events). Redis was removed in Authentik 2025.10; sessions and caching
|
||||
now live here.
|
||||
|
||||
- **Compose service:** `authentik-postgres` (in `dankclaw/compose.yaml`)
|
||||
- **Image:** `docker.io/library/postgres:16-alpine`
|
||||
- **Data:** `/home/dank/authentik/postgres` (bind mount; no named volumes)
|
||||
- **Health:** `pg_isready -d authentik -U authentik`
|
||||
|
||||
See [Authentik.md](Authentik.md) for the full architecture, setup, backups
|
||||
(`pg_dump`), and failure modes.
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
type: reference
|
||||
description: "The Authentik server — the core, the admin API, and the embedded Caddy outpost."
|
||||
tags:
|
||||
- infrastructure
|
||||
- authentik
|
||||
timestamp: 2026-09-16
|
||||
---
|
||||
|
||||
# authentik-server
|
||||
|
||||
The Authentik server — the core, the admin API, and the embedded Caddy outpost.
|
||||
This is the service the Caddy `forward_auth` vhosts proxy to
|
||||
(`authentik-server:9000`). It also serves the login page at
|
||||
`https://auth.franksdank.cloud`.
|
||||
|
||||
- **Unit:** `dankclaw/authentik/quadlet/authentik-server.container` (`authentik-server.service`)
|
||||
- **Image:** `ghcr.io/goauthentik/server:2026.8.3` (command: `server`)
|
||||
- **Port:** `9000` (published; the outpost + admin + login all live here)
|
||||
- **Data:** `/home/dank/authentik/data` (bind mount, shared with the worker)
|
||||
- **Health:** `http://localhost:9000/-/health/`
|
||||
|
||||
See [Authentik.md](Authentik.md) for the full architecture, the Caddy
|
||||
forward_auth pattern, first-time setup, and failure modes.
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
type: reference
|
||||
description: "The Authentik worker — background tasks: event processing, provider"
|
||||
tags:
|
||||
- infrastructure
|
||||
- authentik
|
||||
timestamp: 2026-09-16
|
||||
---
|
||||
|
||||
# authentik-worker
|
||||
|
||||
The Authentik worker — background tasks: event processing, provider
|
||||
synchronization, scheduled flows, and certificate renewal. It runs the same
|
||||
image as the server; the `command: worker` selects the role.
|
||||
|
||||
- **Unit:** `dankclaw/authentik/quadlet/authentik-worker.container` (`authentik-worker.service`)
|
||||
- **Image:** `ghcr.io/goauthentik/server:2026.8.3` (command: `worker`)
|
||||
- **Runs as:** `0:0` (rootless: container root maps to the host user that owns the data dir)
|
||||
- **Data:** `/home/dank/authentik/data` (bind mount, shared with the server)
|
||||
- **No port** — the worker has no HTTP endpoint.
|
||||
|
||||
See [Authentik.md](Authentik.md) for the full architecture, setup, and failure
|
||||
modes.
|
||||
Reference in New Issue
Block a user