docs: Jellyfin LDAP-Auth integration (outpost + plugin) and authentik LDAP rules
This commit is contained in:
@@ -101,11 +101,50 @@ cgroup regardless of `GroupAdd=keep-groups`.
|
||||
|
||||
## Authentication
|
||||
|
||||
- **Built-in auth (default):** Per-user libraries, parental controls, max bitrate limits. Manage via Dashboard → Users.
|
||||
- **Caddy basic auth:** Single login layer. Does not integrate with per-user Jellyfin permissions.
|
||||
- **LDAP/OIDC:** Available via plugins (LDAP-Auth, Jellyfin SSO). Integrates with Authentik, Keycloak, etc.
|
||||
- **Built-in auth:** per-user libraries, parental controls, max bitrate
|
||||
limits. The local `dank` admin account is built-in and its password is
|
||||
Frank's — never recorded in this repo (the bootstrap-era value no longer
|
||||
authenticates on the live database).
|
||||
- **LDAP via Authentik (live 2026-09-01):** the LDAP-Auth plugin
|
||||
authenticates users against the authentik LDAP outpost on Dankclaw — see
|
||||
[[Authentik]] for the outpost unit, provider, and RBAC objects.
|
||||
|
||||
For Dankserver, built-in auth is recommended. Use Caddy basic auth on top for external access.
|
||||
### LDAP-Auth plugin (v23.0.0.0, live 2026-09-01)
|
||||
|
||||
- **Install from the GitHub releases zip** — `repo.jellyfin.org`'s stable
|
||||
manifest path 404s here, so fetch
|
||||
`jellyfin/jellyfin-plugin-ldapauth` release asset
|
||||
`ldap-authentication_23.0.0.0.zip` from GitHub and unzip into
|
||||
`config/plugins/LDAP-Auth_23.0.0.0/`. The config lives at
|
||||
`config/plugins/configurations/LDAP-Auth.xml` — write it directly, then
|
||||
restart the service.
|
||||
- **Live config:** server `192.168.0.143:389`, base DN
|
||||
`dc=franksdank,dc=cloud`, bind DN
|
||||
`cn=svc-jellyfin-ldap,ou=users,dc=franksdank,dc=cloud`, bind password =
|
||||
the authentik outpost token (secret — the XML lives on the host only,
|
||||
never in the repo), search filter
|
||||
`(memberOf=cn=jellyfin_users,ou=groups,dc=franksdank,dc=cloud)`, admin
|
||||
filter on `cn=jellyfin_admins,ou=groups,…`, `CreateUsersFromLdap=true`
|
||||
(auto-create on first login).
|
||||
- **Attribute separator is a comma.** `LdapSearchAttributes` is
|
||||
`Split(',')` in the plugin source — a semicolon-separated list
|
||||
(`uid;cn;mail;displayName`) parses as ONE bogus attribute, the search
|
||||
silently matches nothing, and the log says `Found no users matching X`.
|
||||
Use `uid,cn,mail,displayName`.
|
||||
- **Username attribute is `cn`.** Authentik 2026.x maps `uid` to an opaque
|
||||
per-user hash, not the username, so `uid=<name>` never matches. Set
|
||||
`LdapUsernameAttribute=cn` (authentik exposes the username as `cn`).
|
||||
- **Verify with a throwaway user:** create a test member of
|
||||
`jellyfin_users` in authentik, then
|
||||
`POST /Users/AuthenticateByName` with headers `Content-Type:
|
||||
application/json` **and** `X-Emby-Authorization: MediaBrowser
|
||||
Client="…", Device="…", DeviceId="…", Version="10.11.0"` — both are
|
||||
required in 10.11 (missing `X-Emby-Authorization` → 400). 200 + non-admin
|
||||
policy = LDAP path works. Delete the test user from authentik **and**
|
||||
Jellyfin afterward.
|
||||
- **Plugin load lines only show in `journalctl --user -u jellyfin.service`**
|
||||
(`Loaded plugin: LDAP-Auth 23.0.0.0`) — `podman logs` does not carry the
|
||||
loader lines in this image.
|
||||
|
||||
## Metadata Scraping
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ Pi-hole) are not wrapped — they handle their own session.
|
||||
- **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.
|
||||
|
||||
## Why Authentik
|
||||
|
||||
@@ -155,6 +157,51 @@ implementation diverges from upstream in ways that bit us; all verified live:
|
||||
`grafana-profile-scope`, Python-syntax expressions) and will start emitting
|
||||
claims the moment the evaluator bug is fixed — no config change 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.
|
||||
|
||||
- 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).
|
||||
|
||||
### Authentik-side objects (provisioned via `ak shell` ORM)
|
||||
|
||||
| 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`) |
|
||||
|
||||
Hard-won rules, each verified live:
|
||||
|
||||
- **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.
|
||||
- **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`.
|
||||
|
||||
## Configuration (secrets)
|
||||
|
||||
Two secrets live in the host `.env` (git-ignored; placeholders in
|
||||
@@ -205,5 +252,6 @@ Two secrets live in the host `.env` (git-ignored; placeholders in
|
||||
|
||||
- [Caddy](Caddy.md) — the reverse proxy carrying the `:8016`/`:8017` auth-chain listeners
|
||||
- [Pi-hole](Pi-hole.md) — the LAN DNS that resolves `auth.franksdank.cloud`
|
||||
- [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
|
||||
|
||||
@@ -11,6 +11,46 @@ timestamp: 2026-09-14
|
||||
|
||||
# Bundle Change Log
|
||||
|
||||
## 2026-09-01 — Jellyfin authentication moved to Authentik LDAP
|
||||
- `/dankclaw/authentik/quadlet/authentik-outpost-ldap.container` (`4ad85fa`,
|
||||
digest-pinned in `f4ef2b8`) — new `authentik-outpost-ldap.service` on
|
||||
Dankclaw serving LDAP :389/:636 for Jellyfin. Image
|
||||
`ghcr.io/goauthentik/ldap` pinned by **amd64 single-platform digest**:
|
||||
podman 5.7.0 refuses the tag because it resolves to a nested manifest
|
||||
list ("unexpectedly received a manifest list instead of a manifest for a
|
||||
single image") — resolve via `podman manifest inspect | jq` and pin.
|
||||
- Authentik objects provisioned via `ak shell` ORM (live, idempotent
|
||||
get_or_create): LDAP provider `Jellyfin LDAP`, service account
|
||||
`svc-jellyfin-ldap` + bind token, groups `jellyfin_users` /
|
||||
`jellyfin_admins`, application `jellyfin`.
|
||||
- `/scripts/deploy-remote.sh` (`691da47`) — `authentik-outpost-ldap` added
|
||||
to `service_is_allowed` (the deploy runner has its own allowlist beyond
|
||||
deploy-plan.py's).
|
||||
- Two authentik-side rules proven by live bind tests (`ldapsearch` through
|
||||
the published port):
|
||||
1. Bind token **intent must be `verification`** — `app_password` tokens
|
||||
are rejected by the authorization flow's password stage with
|
||||
`Invalid password`.
|
||||
2. Search requires the **`search_full_directory`** object permission on
|
||||
the provider (granted via RBAC role `svc-jellyfin-ldap-search`);
|
||||
without it the outpost returns only the bind user, so `memberOf`
|
||||
filters match zero entries.
|
||||
- Jellyfin side (host-only, no repo change): LDAP-Auth **23.0.0.0**
|
||||
installed from the GitHub releases zip (the `repo.jellyfin.org` stable
|
||||
manifest path 404s), config XML at
|
||||
`config/plugins/configurations/LDAP-Auth.xml`, username attribute `cn`
|
||||
(authentik 2026.x maps `uid` to an opaque hash), and — the big one —
|
||||
**`LdapSearchAttributes` is comma-separated** (`Split(',')` in the plugin
|
||||
source; semicolons silently produce "Found no users matching X").
|
||||
End-to-end verified with a throwaway `jellyfin_users` member
|
||||
(`AuthenticateByName` 200, non-admin policy), then removed from both
|
||||
authentik and the Jellyfin DB directly (its admin-token cleanup 401'd —
|
||||
the local `dank` admin password on the live DB is Frank's, not the
|
||||
bootstrap-era value; notated in the Jellyfin doc).
|
||||
- Docs: [[Authentik]] gained the LDAP-outpost section (objects table +
|
||||
rules), [[Jellyfin]] gained the LDAP-Auth plugin section. Docs-only
|
||||
commit → deploy no-op.
|
||||
|
||||
## 2026-09-01 — frank-default-small: two compressed-tensors FP8 swaps fail to load; Huihui restored
|
||||
- `/scripts/frank-default-small.env`, `/scripts/serve-frank-small.sh`,
|
||||
`/scripts/frank-default-small.service` (`af6cd72` → `34ab6b4` → reverted
|
||||
|
||||
Reference in New Issue
Block a user