From c313c7cc3f517bce0840c639b55d617216f7868e Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sat, 29 Aug 2026 20:47:54 +1000 Subject: [PATCH] fafflix,cheeztv: declarative SSO + LDAP plugin auth config (#424) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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). Reviewed-on: https://git.unkin.net/unkin/argocd-apps/pulls/424 Co-authored-by: unkin-agent Co-committed-by: unkin-agent --- apps/base/cheeztv/kustomization.yaml | 1 + apps/base/cheeztv/plugin-configmap.yaml | 97 ++++++++++++++++++++++++ apps/base/cheeztv/statefulset.yaml | 54 +++++++++++++ apps/base/cheeztv/vaultstaticsecret.yaml | 24 ++++++ apps/base/fafflix/kustomization.yaml | 1 + apps/base/fafflix/plugin-configmap.yaml | 97 ++++++++++++++++++++++++ apps/base/fafflix/statefulset.yaml | 54 +++++++++++++ apps/base/fafflix/vaultstaticsecret.yaml | 24 ++++++ 8 files changed, 352 insertions(+) create mode 100644 apps/base/cheeztv/plugin-configmap.yaml create mode 100644 apps/base/fafflix/plugin-configmap.yaml diff --git a/apps/base/cheeztv/kustomization.yaml b/apps/base/cheeztv/kustomization.yaml index 26efe34..d2454f3 100644 --- a/apps/base/cheeztv/kustomization.yaml +++ b/apps/base/cheeztv/kustomization.yaml @@ -18,6 +18,7 @@ resources: - pvc-media-tv.yaml - pvc-media-movies.yaml - statefulset.yaml + - plugin-configmap.yaml - pdb.yaml - service.yaml - valkey.yaml diff --git a/apps/base/cheeztv/plugin-configmap.yaml b/apps/base/cheeztv/plugin-configmap.yaml new file mode 100644 index 0000000..0b21790 --- /dev/null +++ b/apps/base/cheeztv/plugin-configmap.yaml @@ -0,0 +1,97 @@ +--- +# Declarative config for the browser-auth plugins bundled in the jellyfin-ha +# image (jellyfin-plugin-sso, jellyfin-plugin-ldapauth). Rendered into +# /config/plugins/configurations/ by the inject-plugin-config initContainer, +# which substitutes the OidSecret / LdapBindPassword placeholders from the +# VSO-synced oauth-credentials Secret so no secret is committed here. The SSO +# provider key "authentik" must match the redirect path segment registered on +# the shared Authentik "jellyfin" OAuth2 client. Roles/AdminRoles are matched +# against the hierarchical Authentik groups claim (akP-jellyfin-user grants +# login, akP-jellyfin-admin grants Jellyfin admin; global admins inherit the +# latter via akR-global-admin). +apiVersion: v1 +kind: ConfigMap +metadata: + name: cheeztv-plugin-config + namespace: cheeztv +data: + SSO-Auth.xml: | + + + + + + + authentik + + + + https://identity.k8s.syd1.au.unkin.net/application/o/jellyfin/ + jellyfin + @@CLIENT_SECRET@@ + true + true + true + + + akP-jellyfin-admin + + + akP-jellyfin-user + akP-jellyfin-admin + + false + false + false + false + + + + ak_groups + + openid + profile + email + ak_groups + + + false + false + false + https + + + + + + LDAP-Auth.xml: | + + + authentik-ldap.authentik.svc.cluster.local + 6636 + true + false + true + cn=jellyfin-ldap,ou=users,DC=ldap,DC=goauthentik,DC=io + @@LDAP_BIND_PASSWORD@@ + ou=users,DC=ldap,DC=goauthentik,DC=io + (objectClass=user) + ou=users,DC=ldap,DC=goauthentik,DC=io + (memberOf=cn=akP-jellyfin-admin,ou=groups,DC=ldap,DC=goauthentik,DC=io) + false + uid, cn, mail, displayName + true + false + cn + cn + userPassword + true + + + branding.xml: | + + + <p style="text-align:center"><a href="/sso/OID/start/authentik">Sign in with SSO</a></p> + + false + diff --git a/apps/base/cheeztv/statefulset.yaml b/apps/base/cheeztv/statefulset.yaml index 002f933..825f226 100644 --- a/apps/base/cheeztv/statefulset.yaml +++ b/apps/base/cheeztv/statefulset.yaml @@ -109,6 +109,57 @@ spec: volumeMounts: - name: config mountPath: /config + # Render the SSO/LDAP plugin configs into the shared config volume, + # substituting the client secret and LDAP bind password from the + # VSO-synced oauth-credentials Secret (never committed). Plugin configs + # are fully managed here so they are overwritten every start; the login + # button branding is written only when absent so admin edits survive. + - name: inject-plugin-config + image: busybox:1.37.0 + command: + - sh + - -c + - | + mkdir -p /config/plugins/configurations /config/config + chown 1000:1000 /config/plugins /config/plugins/configurations /config/config + esc() { printf '%s' "$1" | sed -e 's/[&|\\]/\\&/g'; } + cs=$(esc "${CLIENT_SECRET}") + lp=$(esc "${LDAP_BIND_PASSWORD}") + sed "s|@@CLIENT_SECRET@@|${cs}|" /templates/SSO-Auth.xml > /config/plugins/configurations/SSO-Auth.xml + sed "s|@@LDAP_BIND_PASSWORD@@|${lp}|" /templates/LDAP-Auth.xml > /config/plugins/configurations/LDAP-Auth.xml + chown 1000:1000 /config/plugins/configurations/SSO-Auth.xml /config/plugins/configurations/LDAP-Auth.xml + chmod 600 /config/plugins/configurations/SSO-Auth.xml /config/plugins/configurations/LDAP-Auth.xml + if [ ! -e /config/config/branding.xml ]; then + cp /templates/branding.xml /config/config/branding.xml + chown 1000:1000 /config/config/branding.xml + chmod 664 /config/config/branding.xml + fi + env: + - name: CLIENT_SECRET + valueFrom: + secretKeyRef: + name: oauth-credentials + key: client_secret + optional: true + - name: LDAP_BIND_PASSWORD + valueFrom: + secretKeyRef: + name: oauth-credentials + key: ldap_bind_password + optional: true + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + volumeMounts: + - name: config + mountPath: /config + - name: plugin-config + mountPath: /templates + readOnly: true containers: - name: cheeztv image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/jellyfin-ha:v0.2.0 @@ -222,6 +273,9 @@ spec: subPath: kids readOnly: true volumes: + - name: plugin-config + configMap: + name: cheeztv-plugin-config - name: config persistentVolumeClaim: claimName: cheeztv-config diff --git a/apps/base/cheeztv/vaultstaticsecret.yaml b/apps/base/cheeztv/vaultstaticsecret.yaml index 1ea18fd..842e652 100644 --- a/apps/base/cheeztv/vaultstaticsecret.yaml +++ b/apps/base/cheeztv/vaultstaticsecret.yaml @@ -22,3 +22,27 @@ spec: refreshAfter: 5m type: kv-v2 vaultAuthRef: default +--- +# Shared Authentik "jellyfin" OAuth2 client secret (key: client_secret) plus the +# LDAP outpost bind password (key: ldap_bind_password) for the auth plugins. +# The default k8s role's templated policy is namespace-scoped +# (kv/data/kubernetes/namespace/{{sa_namespace}}/{{sa_name}}/*), so each instance +# reads its own namespace path; the SAME shared values must be seeded at both +# fafflix and cheeztv paths. VSO syncs into the oauth-credentials Secret, whose +# keys the inject-plugin-config initContainer substitutes into the plugin XML. +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: oauth-credentials + namespace: cheeztv +spec: + destination: + create: true + name: oauth-credentials + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/cheeztv/default/oauth-credentials + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default diff --git a/apps/base/fafflix/kustomization.yaml b/apps/base/fafflix/kustomization.yaml index 26efe34..d2454f3 100644 --- a/apps/base/fafflix/kustomization.yaml +++ b/apps/base/fafflix/kustomization.yaml @@ -18,6 +18,7 @@ resources: - pvc-media-tv.yaml - pvc-media-movies.yaml - statefulset.yaml + - plugin-configmap.yaml - pdb.yaml - service.yaml - valkey.yaml diff --git a/apps/base/fafflix/plugin-configmap.yaml b/apps/base/fafflix/plugin-configmap.yaml new file mode 100644 index 0000000..e5b9e19 --- /dev/null +++ b/apps/base/fafflix/plugin-configmap.yaml @@ -0,0 +1,97 @@ +--- +# Declarative config for the browser-auth plugins bundled in the jellyfin-ha +# image (jellyfin-plugin-sso, jellyfin-plugin-ldapauth). Rendered into +# /config/plugins/configurations/ by the inject-plugin-config initContainer, +# which substitutes the OidSecret / LdapBindPassword placeholders from the +# VSO-synced oauth-credentials Secret so no secret is committed here. The SSO +# provider key "authentik" must match the redirect path segment registered on +# the shared Authentik "jellyfin" OAuth2 client. Roles/AdminRoles are matched +# against the hierarchical Authentik groups claim (akP-jellyfin-user grants +# login, akP-jellyfin-admin grants Jellyfin admin; global admins inherit the +# latter via akR-global-admin). +apiVersion: v1 +kind: ConfigMap +metadata: + name: fafflix-plugin-config + namespace: fafflix +data: + SSO-Auth.xml: | + + + + + + + authentik + + + + https://identity.k8s.syd1.au.unkin.net/application/o/jellyfin/ + jellyfin + @@CLIENT_SECRET@@ + true + true + true + + + akP-jellyfin-admin + + + akP-jellyfin-user + akP-jellyfin-admin + + false + false + false + false + + + + ak_groups + + openid + profile + email + ak_groups + + + false + false + false + https + + + + + + LDAP-Auth.xml: | + + + authentik-ldap.authentik.svc.cluster.local + 6636 + true + false + true + cn=jellyfin-ldap,ou=users,DC=ldap,DC=goauthentik,DC=io + @@LDAP_BIND_PASSWORD@@ + ou=users,DC=ldap,DC=goauthentik,DC=io + (objectClass=user) + ou=users,DC=ldap,DC=goauthentik,DC=io + (memberOf=cn=akP-jellyfin-admin,ou=groups,DC=ldap,DC=goauthentik,DC=io) + false + uid, cn, mail, displayName + true + false + cn + cn + userPassword + true + + + branding.xml: | + + + <p style="text-align:center"><a href="/sso/OID/start/authentik">Sign in with SSO</a></p> + + false + diff --git a/apps/base/fafflix/statefulset.yaml b/apps/base/fafflix/statefulset.yaml index c7b3352..68d8275 100644 --- a/apps/base/fafflix/statefulset.yaml +++ b/apps/base/fafflix/statefulset.yaml @@ -109,6 +109,57 @@ spec: volumeMounts: - name: config mountPath: /config + # Render the SSO/LDAP plugin configs into the shared config volume, + # substituting the client secret and LDAP bind password from the + # VSO-synced oauth-credentials Secret (never committed). Plugin configs + # are fully managed here so they are overwritten every start; the login + # button branding is written only when absent so admin edits survive. + - name: inject-plugin-config + image: busybox:1.37.0 + command: + - sh + - -c + - | + mkdir -p /config/plugins/configurations /config/config + chown 1000:1000 /config/plugins /config/plugins/configurations /config/config + esc() { printf '%s' "$1" | sed -e 's/[&|\\]/\\&/g'; } + cs=$(esc "${CLIENT_SECRET}") + lp=$(esc "${LDAP_BIND_PASSWORD}") + sed "s|@@CLIENT_SECRET@@|${cs}|" /templates/SSO-Auth.xml > /config/plugins/configurations/SSO-Auth.xml + sed "s|@@LDAP_BIND_PASSWORD@@|${lp}|" /templates/LDAP-Auth.xml > /config/plugins/configurations/LDAP-Auth.xml + chown 1000:1000 /config/plugins/configurations/SSO-Auth.xml /config/plugins/configurations/LDAP-Auth.xml + chmod 600 /config/plugins/configurations/SSO-Auth.xml /config/plugins/configurations/LDAP-Auth.xml + if [ ! -e /config/config/branding.xml ]; then + cp /templates/branding.xml /config/config/branding.xml + chown 1000:1000 /config/config/branding.xml + chmod 664 /config/config/branding.xml + fi + env: + - name: CLIENT_SECRET + valueFrom: + secretKeyRef: + name: oauth-credentials + key: client_secret + optional: true + - name: LDAP_BIND_PASSWORD + valueFrom: + secretKeyRef: + name: oauth-credentials + key: ldap_bind_password + optional: true + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + volumeMounts: + - name: config + mountPath: /config + - name: plugin-config + mountPath: /templates + readOnly: true containers: - name: fafflix image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/jellyfin-ha:v0.2.0 @@ -236,6 +287,9 @@ spec: subPath: kids readOnly: true volumes: + - name: plugin-config + configMap: + name: fafflix-plugin-config - name: config persistentVolumeClaim: claimName: fafflix-config diff --git a/apps/base/fafflix/vaultstaticsecret.yaml b/apps/base/fafflix/vaultstaticsecret.yaml index 5f53f89..0c2b7e3 100644 --- a/apps/base/fafflix/vaultstaticsecret.yaml +++ b/apps/base/fafflix/vaultstaticsecret.yaml @@ -22,3 +22,27 @@ spec: refreshAfter: 5m type: kv-v2 vaultAuthRef: default +--- +# Shared Authentik "jellyfin" OAuth2 client secret (key: client_secret) plus the +# LDAP outpost bind password (key: ldap_bind_password) for the auth plugins. +# The default k8s role's templated policy is namespace-scoped +# (kv/data/kubernetes/namespace/{{sa_namespace}}/{{sa_name}}/*), so each instance +# reads its own namespace path; the SAME shared values must be seeded at both +# fafflix and cheeztv paths. VSO syncs into the oauth-credentials Secret, whose +# keys the inject-plugin-config initContainer substitutes into the plugin XML. +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: oauth-credentials + namespace: fafflix +spec: + destination: + create: true + name: oauth-credentials + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/fafflix/default/oauth-credentials + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default