fafflix,cheeztv: declarative SSO + LDAP plugin auth config #424

Merged
benvin merged 2 commits from benvin/jellyfin-plugin-config into main 2026-08-29 20:47:54 +10:00
Member

Summary

  • Adds SSO-Auth.xml, LDAP-Auth.xml, and branding.xml plugin config (rendered by an initContainer into /config) to fafflix and cheeztv
  • Adds a per-namespace oauth-credentials VaultStaticSecret to each instance
  • Adds a "Sign in with SSO" login link via jellyfin branding config

Why

Phase-1 jellyfin auth: browser SSO via the 9p4 SSO plugin (Authentik OIDC, ak_groups claim) and phone/TV app-passwords via the LDAP plugin against the Authentik LDAP outpost. Config is declarative — restarts overwrite UI edits. Activates when the plugin-baked jellyfin-ha image lands (separate PR).

## Summary - Adds SSO-Auth.xml, LDAP-Auth.xml, and branding.xml plugin config (rendered by an initContainer into /config) to fafflix and cheeztv - Adds a per-namespace oauth-credentials VaultStaticSecret to each instance - Adds a "Sign in with SSO" login link via jellyfin branding config ## Why Phase-1 jellyfin auth: browser SSO via the 9p4 SSO plugin (Authentik OIDC, ak_groups claim) and phone/TV app-passwords via the LDAP plugin against the Authentik LDAP outpost. Config is declarative — restarts overwrite UI edits. Activates when the plugin-baked jellyfin-ha image lands (separate PR).
unkin-agent added 1 commit 2026-08-26 22:24:01 +10:00
fafflix,cheeztv: declarative SSO + LDAP plugin auth config
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
571c3c0320
Wire phase-1 plugin-based auth into both Jellyfin instances so browser users
sign in via Authentik OIDC (jellyfin-plugin-sso) and native/app-password clients
authenticate through the Authentik LDAP outpost (jellyfin-plugin-ldapauth),
JIT-provisioning users.

Both instances share the single Authentik "jellyfin" OAuth2 client, so the SSO
config is identical; each reads the shared client_secret from its own
namespace-scoped Vault path via a per-namespace VaultStaticSecret.

- plugin-configmap.yaml: SSO-Auth.xml (OID provider "authentik", issuer
  identity.k8s.syd1.au.unkin.net/application/o/jellyfin/, EnableAllFolders,
  groups-claim role mapping akP-jellyfin-admin->admin / akP-jellyfin-user->user),
  LDAP-Auth.xml (server authentik-ldap.authentik.svc:6636 LDAPS,
  CreateUsersFromLdap), and branding.xml with a "Sign in with SSO" login link to
  /sso/OID/start/authentik.
- statefulset.yaml: inject-plugin-config initContainer renders the XMLs into
  /config/plugins/configurations/, substituting client_secret / ldap_bind_password
  from the VSO-synced Secret (never committed) before Jellyfin starts.
- vaultstaticsecret.yaml: per-namespace oauth-credentials VSS.

Image tag is intentionally left at v0.1.3; the plugin DLLs land via a separate
jellyfin-ha release and a follow-up pin bump.
Author
Member

Reviewed against the 9p4/jellyfin-plugin-sso, jellyfin/jellyfin-plugin-ldapauth, and jellyfin/jellyfin (BrandingOptions) source, and cross-checked against terraform-authentik#28 (branch benvin/jellyfin-sso, head 9760c2e). CI is green (kubeconform/pre-commit/vector-test all pass on 571c3c0). One field-name/schema check is XML-valid but functionally blocks all SSO logins — please fix before this is safe to merge alongside terraform-authentik#28.

Blocking: RoleClaim + OidScopes will never authorize anyone via SSO

SSO-Auth.xml sets <RoleClaim>groups</RoleClaim> and <OidScopes>openid, profile, email</OidScopes> in both apps/base/fafflix/plugin-configmap.yaml and apps/base/cheeztv/plugin-configmap.yaml.

  • Every other Authentik-backed app in this repo (apps/base/arrstack/arrproxy/oauth2-proxy-configmap.yaml, apps/base/logging/logviewer/oauth2-proxy-configmap.yaml) reads the group-membership claim as ak_groups, not groups. That's not incidental — terraform-authentik's modules/authentik/main.tf (authentik_property_mapping_provider_scope.groups_hierarchical, scope_name = "ak_groups") unconditionally attaches an ak_groups scope mapping to every OAuth2 provider the module creates, including the jellyfin client in terraform-authentik#28's config/providers_oauth2/jellyfin.yaml (whose own comment says "the plugin matching the ak_groups claim against akP-jellyfin-admin / akP-jellyfin-user"). Authentik never emits a claim literally named groups.
  • Even after fixing the claim name, the mapping is scope-gated: Authentik only includes a property-mapping's claim in the token if the client requests its scope_name. OidScopes here is [openid, profile, email]ak_groups is never requested, so the claim would be absent from the token regardless.
  • In 9p4/jellyfin-plugin-sso's SSOController.cs (OidCallback, ~L177-269): timedState.Valid is only set true when config.Roles is non-empty (it is here: [akP-jellyfin-user, akP-jellyfin-admin]) and a claim of type config.RoleClaim carries a matching value. With no ak_groups claim ever present, Valid stays false for every user, including admins — this isn't "nobody gets admin," it's "nobody can log in via SSO at all."

Fix: add <string>ak_groups</string> to OidScopes and change <RoleClaim>groups</RoleClaim> to <RoleClaim>ak_groups</RoleClaim> in both instances' SSO-Auth.xml.

Verified correct (no action needed)

  • Redirect-URI alias: terraform-authentik#28 registers only the /sso/OID/redirect/authentik strict URIs (no /r/ alias) for all three hosts. In the plugin, OidChallenge (SSOController.cs L360-384) derives newPath from whether the inbound request path contains /start/ (when isLinking is false), and only falls back to the persisted config.NewPath otherwise. Since branding.xml's login link is /sso/OID/start/authentik, every fresh login always hits the /start/ route and constructs redirect_uri=.../sso/OID/redirect/authentik — this matches the strict registration exactly. Good.
  • Branding path: Jellyfin's BaseConfigurationManager.GetConfigurationFile maps config key → {ConfigurationDirectoryPath}/{key}.xml, and BrandingConfigurationFactory.Key = "branding", so /config/config/branding.xml is the correct target (matches the existing inject-config initContainer's database.xml/encoding.xml convention in the same directory).
  • XML field names: every element in SSO-Auth.xml's OidConfig, LDAP-Auth.xml's PluginConfiguration, and branding.xml's BrandingOptions matches the upstream C# property names/casing exactly (checked field-by-field against 9p4/jellyfin-plugin-sso Config/PluginConfiguration.cs, jellyfin-plugin-ldapauth Config/PluginConfiguration.cs, and jellyfin MediaBrowser.Model/Branding/BrandingOptions.cs).
  • initContainer ordering/idempotency/perms: runs before the main container (K8s initContainer ordering, no sync-waves used), plugin XMLs are unconditionally overwritten every start (IaC wins over UI edits, per design) while branding.xml is write-if-absent (admin edits survive), files chowned 1000:1000 matching the pod's fsGroup: 1000 / container runAsUser: 1000, and secret values come only from optional: true env vars sourced from the VSO-synced Secret — never baked into the ConfigMap. Consistent with the existing inject-config initContainer's (pre-existing) root-chown pattern.
  • LDAP base/bind DN: DC=ldap,DC=goauthentik,DC=io matches terraform-authentik#28's providers_ldap/jellyfin-ldap.yaml base_dn exactly.

Advisory (non-blocking)

  • SkipSslVerify=true on the LDAPS (6636) endpoint: the plugin exposes LdapRootCaPath (confirmed in jellyfin-plugin-ldapauth's PluginConfiguration.cs). Worth switching to pinning the outpost's CA via that field (combined-certs pattern used elsewhere in the repo) rather than disabling verification outright, once the outpost cert material is known — the PR body already flags this as a follow-up.
  • Issuer host https://identity.k8s.syd1.au.unkin.net/application/o/jellyfin/: diverges from the majority in-repo convention (arrstack, logviewer, litellm all use identity.unkin.net). DoNotValidateIssuerName is false (strict), so if Authentik's discovery document actually emits iss as identity.unkin.net (its configured Brand domain) rather than the .k8s.syd1.au.unkin.net alias, token validation will fail. The PR body already flags this as an assumption to verify at first login — agreed this needs a live check, not something resolvable from source alone.
  • Secret values aren't XML-escaped before substitution: the inject-plugin-config sed only escapes &, |, \ for sed-safety, not for XML validity. If a rotated client_secret or ldap_bind_password ever contains a literal <, >, or unescaped &, the rendered SSO-Auth.xml/LDAP-Auth.xml becomes malformed XML on next pod restart. Low likelihood (Authentik client secrets are typically URL-safe base64), but worth a defensive XML-escape (sed for &&amp;, <&lt;, >&gt;) in addition to the existing sed-metachar escaping.

Everything else (VSS wiring, kustomization registration, mirroring across both instances, no image bump) looks correct and atomic.

Reviewed against the `9p4/jellyfin-plugin-sso`, `jellyfin/jellyfin-plugin-ldapauth`, and `jellyfin/jellyfin` (`BrandingOptions`) source, and cross-checked against `terraform-authentik#28` (branch `benvin/jellyfin-sso`, head `9760c2e`). CI is green (kubeconform/pre-commit/vector-test all pass on `571c3c0`). One field-name/schema check is XML-valid but functionally blocks all SSO logins — please fix before this is safe to merge alongside `terraform-authentik#28`. ## Blocking: `RoleClaim` + `OidScopes` will never authorize anyone via SSO `SSO-Auth.xml` sets `<RoleClaim>groups</RoleClaim>` and `<OidScopes>openid, profile, email</OidScopes>` in both `apps/base/fafflix/plugin-configmap.yaml` and `apps/base/cheeztv/plugin-configmap.yaml`. - Every other Authentik-backed app in this repo (`apps/base/arrstack/arrproxy/oauth2-proxy-configmap.yaml`, `apps/base/logging/logviewer/oauth2-proxy-configmap.yaml`) reads the group-membership claim as **`ak_groups`**, not `groups`. That's not incidental — `terraform-authentik`'s `modules/authentik/main.tf` (`authentik_property_mapping_provider_scope.groups_hierarchical`, `scope_name = "ak_groups"`) unconditionally attaches an `ak_groups` scope mapping to *every* OAuth2 provider the module creates, including the `jellyfin` client in `terraform-authentik#28`'s `config/providers_oauth2/jellyfin.yaml` (whose own comment says "the plugin matching the **ak_groups** claim against akP-jellyfin-admin / akP-jellyfin-user"). Authentik never emits a claim literally named `groups`. - Even after fixing the claim name, the mapping is scope-gated: Authentik only includes a property-mapping's claim in the token if the client requests its `scope_name`. `OidScopes` here is `[openid, profile, email]` — `ak_groups` is never requested, so the claim would be absent from the token regardless. - In `9p4/jellyfin-plugin-sso`'s `SSOController.cs` (`OidCallback`, ~L177-269): `timedState.Valid` is only set `true` when `config.Roles` is non-empty (it is here: `[akP-jellyfin-user, akP-jellyfin-admin]`) **and** a claim of type `config.RoleClaim` carries a matching value. With no `ak_groups` claim ever present, `Valid` stays `false` for every user, including admins — this isn't "nobody gets admin," it's "nobody can log in via SSO at all." Fix: add `<string>ak_groups</string>` to `OidScopes` and change `<RoleClaim>groups</RoleClaim>` to `<RoleClaim>ak_groups</RoleClaim>` in both instances' `SSO-Auth.xml`. ## Verified correct (no action needed) - **Redirect-URI alias**: `terraform-authentik#28` registers only the `/sso/OID/redirect/authentik` strict URIs (no `/r/` alias) for all three hosts. In the plugin, `OidChallenge` (`SSOController.cs` L360-384) derives `newPath` from whether the *inbound* request path contains `/start/` (when `isLinking` is false), and only falls back to the persisted `config.NewPath` otherwise. Since `branding.xml`'s login link is `/sso/OID/start/authentik`, every fresh login always hits the `/start/` route and constructs `redirect_uri=.../sso/OID/redirect/authentik` — this matches the strict registration exactly. Good. - **Branding path**: Jellyfin's `BaseConfigurationManager.GetConfigurationFile` maps config key → `{ConfigurationDirectoryPath}/{key}.xml`, and `BrandingConfigurationFactory.Key = "branding"`, so `/config/config/branding.xml` is the correct target (matches the existing `inject-config` initContainer's `database.xml`/`encoding.xml` convention in the same directory). - **XML field names**: every element in `SSO-Auth.xml`'s `OidConfig`, `LDAP-Auth.xml`'s `PluginConfiguration`, and `branding.xml`'s `BrandingOptions` matches the upstream C# property names/casing exactly (checked field-by-field against `9p4/jellyfin-plugin-sso` `Config/PluginConfiguration.cs`, `jellyfin-plugin-ldapauth` `Config/PluginConfiguration.cs`, and `jellyfin` `MediaBrowser.Model/Branding/BrandingOptions.cs`). - **initContainer ordering/idempotency/perms**: runs before the main container (K8s initContainer ordering, no sync-waves used), plugin XMLs are unconditionally overwritten every start (IaC wins over UI edits, per design) while `branding.xml` is write-if-absent (admin edits survive), files chowned `1000:1000` matching the pod's `fsGroup: 1000` / container `runAsUser: 1000`, and secret values come only from `optional: true` env vars sourced from the VSO-synced Secret — never baked into the ConfigMap. Consistent with the existing `inject-config` initContainer's (pre-existing) root-chown pattern. - **LDAP base/bind DN**: `DC=ldap,DC=goauthentik,DC=io` matches `terraform-authentik#28`'s `providers_ldap/jellyfin-ldap.yaml` `base_dn` exactly. ## Advisory (non-blocking) - **`SkipSslVerify=true` on the LDAPS (6636) endpoint**: the plugin exposes `LdapRootCaPath` (confirmed in `jellyfin-plugin-ldapauth`'s `PluginConfiguration.cs`). Worth switching to pinning the outpost's CA via that field (combined-certs pattern used elsewhere in the repo) rather than disabling verification outright, once the outpost cert material is known — the PR body already flags this as a follow-up. - **Issuer host** `https://identity.k8s.syd1.au.unkin.net/application/o/jellyfin/`: diverges from the majority in-repo convention (`arrstack`, `logviewer`, `litellm` all use `identity.unkin.net`). `DoNotValidateIssuerName` is `false` (strict), so if Authentik's discovery document actually emits `iss` as `identity.unkin.net` (its configured Brand domain) rather than the `.k8s.syd1.au.unkin.net` alias, token validation will fail. The PR body already flags this as an assumption to verify at first login — agreed this needs a live check, not something resolvable from source alone. - **Secret values aren't XML-escaped before substitution**: the `inject-plugin-config` `sed` only escapes `&`, `|`, `\` for *sed*-safety, not for XML validity. If a rotated `client_secret` or `ldap_bind_password` ever contains a literal `<`, `>`, or unescaped `&`, the rendered `SSO-Auth.xml`/`LDAP-Auth.xml` becomes malformed XML on next pod restart. Low likelihood (Authentik client secrets are typically URL-safe base64), but worth a defensive XML-escape (`sed` for `&`→`&amp;`, `<`→`&lt;`, `>`→`&gt;`) in addition to the existing sed-metachar escaping. Everything else (VSS wiring, kustomization registration, mirroring across both instances, no image bump) looks correct and atomic.
unkin-agent added 1 commit 2026-08-26 22:35:20 +10:00
Update Jellyfin SSO plugin config for ak_groups claim
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
55137bff29
Change RoleClaim from 'groups' to 'ak_groups' in both fafflix and cheeztv
plugin configs to match Authentik's hierarchical group claim emission.
Add 'ak_groups' to requested scopes in OidScopes list.
Author
Member

RoleClaim + requested scopes switched to ak_groups per review — the hierarchical claim this estate actually emits.

RoleClaim + requested scopes switched to ak_groups per review — the hierarchical claim this estate actually emits.
benvin merged commit c313c7cc3f into main 2026-08-29 20:47:54 +10:00
benvin deleted branch benvin/jellyfin-plugin-config 2026-08-29 20:47:55 +10:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unkin/argocd-apps#424