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

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.
This commit is contained in:
2026-08-26 22:23:00 +10:00
parent b9f727bc97
commit 571c3c0320
8 changed files with 350 additions and 0 deletions
+1
View File
@@ -18,6 +18,7 @@ resources:
- pvc-media-tv.yaml
- pvc-media-movies.yaml
- statefulset.yaml
- plugin-configmap.yaml
- pdb.yaml
- service.yaml
- valkey.yaml
+96
View File
@@ -0,0 +1,96 @@
---
# 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: |
<?xml version="1.0" encoding="utf-8"?>
<PluginConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<SamlConfigs />
<OidConfigs>
<item>
<key>
<string>authentik</string>
</key>
<value>
<PluginConfiguration>
<OidEndpoint>https://identity.k8s.syd1.au.unkin.net/application/o/jellyfin/</OidEndpoint>
<OidClientId>jellyfin</OidClientId>
<OidSecret>@@CLIENT_SECRET@@</OidSecret>
<Enabled>true</Enabled>
<EnableAuthorization>true</EnableAuthorization>
<EnableAllFolders>true</EnableAllFolders>
<EnabledFolders />
<AdminRoles>
<string>akP-jellyfin-admin</string>
</AdminRoles>
<Roles>
<string>akP-jellyfin-user</string>
<string>akP-jellyfin-admin</string>
</Roles>
<EnableFolderRoles>false</EnableFolderRoles>
<EnableLiveTvRoles>false</EnableLiveTvRoles>
<EnableLiveTv>false</EnableLiveTv>
<EnableLiveTvManagement>false</EnableLiveTvManagement>
<LiveTvRoles />
<LiveTvManagementRoles />
<FolderRoleMappings />
<RoleClaim>groups</RoleClaim>
<OidScopes>
<string>openid</string>
<string>profile</string>
<string>email</string>
</OidScopes>
<CanonicalLinks></CanonicalLinks>
<DisableHttps>false</DisableHttps>
<DoNotValidateEndpoints>false</DoNotValidateEndpoints>
<DoNotValidateIssuerName>false</DoNotValidateIssuerName>
<SchemeOverride>https</SchemeOverride>
</PluginConfiguration>
</value>
</item>
</OidConfigs>
</PluginConfiguration>
LDAP-Auth.xml: |
<?xml version="1.0" encoding="utf-8"?>
<PluginConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<LdapServer>authentik-ldap.authentik.svc.cluster.local</LdapServer>
<LdapPort>6636</LdapPort>
<UseSsl>true</UseSsl>
<UseStartTls>false</UseStartTls>
<SkipSslVerify>true</SkipSslVerify>
<LdapBindUser>cn=jellyfin-ldap,ou=users,DC=ldap,DC=goauthentik,DC=io</LdapBindUser>
<LdapBindPassword>@@LDAP_BIND_PASSWORD@@</LdapBindPassword>
<LdapBaseDn>ou=users,DC=ldap,DC=goauthentik,DC=io</LdapBaseDn>
<LdapSearchFilter>(objectClass=user)</LdapSearchFilter>
<LdapAdminBaseDn>ou=users,DC=ldap,DC=goauthentik,DC=io</LdapAdminBaseDn>
<LdapAdminFilter>(memberOf=cn=akP-jellyfin-admin,ou=groups,DC=ldap,DC=goauthentik,DC=io)</LdapAdminFilter>
<EnableLdapAdminFilterMemberUid>false</EnableLdapAdminFilterMemberUid>
<LdapSearchAttributes>uid, cn, mail, displayName</LdapSearchAttributes>
<CreateUsersFromLdap>true</CreateUsersFromLdap>
<AllowPassChange>false</AllowPassChange>
<LdapUidAttribute>cn</LdapUidAttribute>
<LdapUsernameAttribute>cn</LdapUsernameAttribute>
<LdapPasswordAttribute>userPassword</LdapPasswordAttribute>
<EnableAllFolders>true</EnableAllFolders>
<EnabledFolders />
</PluginConfiguration>
branding.xml: |
<?xml version="1.0" encoding="utf-8"?>
<BrandingOptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<LoginDisclaimer>&lt;p style="text-align:center"&gt;&lt;a href="/sso/OID/start/authentik"&gt;Sign in with SSO&lt;/a&gt;&lt;/p&gt;</LoginDisclaimer>
<CustomCss></CustomCss>
<SplashscreenEnabled>false</SplashscreenEnabled>
</BrandingOptions>
+54
View File
@@ -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.1.3
@@ -222,6 +273,9 @@ spec:
subPath: kids
readOnly: true
volumes:
- name: plugin-config
configMap:
name: cheeztv-plugin-config
- name: config
persistentVolumeClaim:
claimName: cheeztv-config
+24
View File
@@ -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
+1
View File
@@ -18,6 +18,7 @@ resources:
- pvc-media-tv.yaml
- pvc-media-movies.yaml
- statefulset.yaml
- plugin-configmap.yaml
- pdb.yaml
- service.yaml
- valkey.yaml
+96
View File
@@ -0,0 +1,96 @@
---
# 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: |
<?xml version="1.0" encoding="utf-8"?>
<PluginConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<SamlConfigs />
<OidConfigs>
<item>
<key>
<string>authentik</string>
</key>
<value>
<PluginConfiguration>
<OidEndpoint>https://identity.k8s.syd1.au.unkin.net/application/o/jellyfin/</OidEndpoint>
<OidClientId>jellyfin</OidClientId>
<OidSecret>@@CLIENT_SECRET@@</OidSecret>
<Enabled>true</Enabled>
<EnableAuthorization>true</EnableAuthorization>
<EnableAllFolders>true</EnableAllFolders>
<EnabledFolders />
<AdminRoles>
<string>akP-jellyfin-admin</string>
</AdminRoles>
<Roles>
<string>akP-jellyfin-user</string>
<string>akP-jellyfin-admin</string>
</Roles>
<EnableFolderRoles>false</EnableFolderRoles>
<EnableLiveTvRoles>false</EnableLiveTvRoles>
<EnableLiveTv>false</EnableLiveTv>
<EnableLiveTvManagement>false</EnableLiveTvManagement>
<LiveTvRoles />
<LiveTvManagementRoles />
<FolderRoleMappings />
<RoleClaim>groups</RoleClaim>
<OidScopes>
<string>openid</string>
<string>profile</string>
<string>email</string>
</OidScopes>
<CanonicalLinks></CanonicalLinks>
<DisableHttps>false</DisableHttps>
<DoNotValidateEndpoints>false</DoNotValidateEndpoints>
<DoNotValidateIssuerName>false</DoNotValidateIssuerName>
<SchemeOverride>https</SchemeOverride>
</PluginConfiguration>
</value>
</item>
</OidConfigs>
</PluginConfiguration>
LDAP-Auth.xml: |
<?xml version="1.0" encoding="utf-8"?>
<PluginConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<LdapServer>authentik-ldap.authentik.svc.cluster.local</LdapServer>
<LdapPort>6636</LdapPort>
<UseSsl>true</UseSsl>
<UseStartTls>false</UseStartTls>
<SkipSslVerify>true</SkipSslVerify>
<LdapBindUser>cn=jellyfin-ldap,ou=users,DC=ldap,DC=goauthentik,DC=io</LdapBindUser>
<LdapBindPassword>@@LDAP_BIND_PASSWORD@@</LdapBindPassword>
<LdapBaseDn>ou=users,DC=ldap,DC=goauthentik,DC=io</LdapBaseDn>
<LdapSearchFilter>(objectClass=user)</LdapSearchFilter>
<LdapAdminBaseDn>ou=users,DC=ldap,DC=goauthentik,DC=io</LdapAdminBaseDn>
<LdapAdminFilter>(memberOf=cn=akP-jellyfin-admin,ou=groups,DC=ldap,DC=goauthentik,DC=io)</LdapAdminFilter>
<EnableLdapAdminFilterMemberUid>false</EnableLdapAdminFilterMemberUid>
<LdapSearchAttributes>uid, cn, mail, displayName</LdapSearchAttributes>
<CreateUsersFromLdap>true</CreateUsersFromLdap>
<AllowPassChange>false</AllowPassChange>
<LdapUidAttribute>cn</LdapUidAttribute>
<LdapUsernameAttribute>cn</LdapUsernameAttribute>
<LdapPasswordAttribute>userPassword</LdapPasswordAttribute>
<EnableAllFolders>true</EnableAllFolders>
<EnabledFolders />
</PluginConfiguration>
branding.xml: |
<?xml version="1.0" encoding="utf-8"?>
<BrandingOptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<LoginDisclaimer>&lt;p style="text-align:center"&gt;&lt;a href="/sso/OID/start/authentik"&gt;Sign in with SSO&lt;/a&gt;&lt;/p&gt;</LoginDisclaimer>
<CustomCss></CustomCss>
<SplashscreenEnabled>false</SplashscreenEnabled>
</BrandingOptions>
+54
View File
@@ -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.1.3
@@ -236,6 +287,9 @@ spec:
subPath: kids
readOnly: true
volumes:
- name: plugin-config
configMap:
name: fafflix-plugin-config
- name: config
persistentVolumeClaim:
claimName: fafflix-config
+24
View File
@@ -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