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