Jellyfin SSO: cheeztv OIDC hosts, media-group gating, LDAP outpost
Completes the Authentik-side plumbing for Jellyfin SSO across both media instances -- fafflix (adults, jellyfin.k8s.syd1.au.unkin.net) and cheeztv (kids, cheeztv.unkin.net + cheeztv.k8s.syd1.au.unkin.net) -- and adds an LDAP outpost so native clients can authenticate with app passwords. Why: the previously-merged jellyfin OIDC provider only covered the fafflix host and gated on the generic jellyfin permission groups. cheeztv needs SSO too, access should be limited to media users, and native (non-browser) clients need a password-based path. How: - providers_oauth2/jellyfin.yaml: one shared confidential client now lists strict redirect URIs for all three hosts using the verified jellyfin-plugin-sso callback path /sso/OID/redirect/authentik. Client secret moved to kv kubernetes/namespace/fafflix/default/oauth-credentials. - Access is gated to media users only: akP-media-fafflix and akP-media-cheeztv now carry `application: jellyfin` and bind to the app; akP-jellyfin-admin / akP-jellyfin-user are demoted to pure role-claim groups (no app binding), still mapped by the plugin for admin/user rights. Per-instance authz (adults -> both, kids -> cheeztv only) stays with the media proxy. - providers_ldap/jellyfin-ldap.yaml: new LDAP provider (base_dn DC=ldap,DC=goauthentik,DC=io, direct bind/search) + application (jellyfin-ldap) + outpost (jellyfin-ldap-outpost) via the existing module. - modules/authentik/main.tf: resolve LDAP bind/unbind flow slugs to ids via data.authentik_flow, matching the oauth2/saml convention.
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
# Permission group akP-jellyfin-admin (name = filename). Grants admin
|
||||
# access to jellyfin: bound to the jellyfin application and mapped to its admin role.
|
||||
application: jellyfin
|
||||
# Permission group akP-jellyfin-admin (name = filename). Does NOT gate the
|
||||
# jellyfin application (SSO access is gated by the media groups akP-media-fafflix
|
||||
# / akP-media-cheeztv). It exists purely as an admin role-claim group: the
|
||||
# jellyfin-plugin-sso matches the hierarchical `ak_groups` claim against this
|
||||
# group name to grant Jellyfin administrator rights. Nested under akR-global-admin.
|
||||
attributes: {}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# Permission group akP-jellyfin-user (name = filename). Grants user
|
||||
# access to jellyfin: bound to the jellyfin application and mapped to its user role.
|
||||
application: jellyfin
|
||||
# Permission group akP-jellyfin-user (name = filename). Does NOT gate the
|
||||
# jellyfin application (SSO access is gated by the media groups akP-media-fafflix
|
||||
# / akP-media-cheeztv). It exists purely as a user role-claim group: the
|
||||
# jellyfin-plugin-sso matches the hierarchical `ak_groups` claim against this
|
||||
# group name for regular (non-admin) Jellyfin access. Nested under akR-standard-user.
|
||||
attributes: {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Permission group akP-media-cheeztv (name = filename). Per-service entitlement
|
||||
# for the kids ("cheeztv") media tier: NOT bound to any application (no
|
||||
# `application` field), so it does not gate OIDC. It exists purely to appear in
|
||||
# the user's hierarchical `ak_groups` claim, which the media proxy reads to
|
||||
# decide whether to route/authorize the cheeztv backend.
|
||||
attributes: {}
|
||||
# for the kids ("cheeztv") media tier. Bound to the jellyfin application, so it
|
||||
# gates Jellyfin SSO access: only members (via akR-media-adult or akR-media-kids)
|
||||
# may authorize. It also appears in the user's hierarchical `ak_groups` claim,
|
||||
# which the media proxy reads to route/authorize the cheeztv backend.
|
||||
application: jellyfin
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Permission group akP-media-fafflix (name = filename). Per-service entitlement
|
||||
# for the adult ("fafflix") media tier: NOT bound to any application (no
|
||||
# `application` field), so it does not gate OIDC. It exists purely to appear in
|
||||
# the user's hierarchical `ak_groups` claim, which the media proxy reads to
|
||||
# decide whether to route/authorize the fafflix backend.
|
||||
attributes: {}
|
||||
# for the adult ("fafflix") media tier. Bound to the jellyfin application, so it
|
||||
# gates Jellyfin SSO access: only members (via akR-media-adult) may authorize.
|
||||
# It also appears in the user's hierarchical `ak_groups` claim, which the media
|
||||
# proxy reads to route/authorize the fafflix backend.
|
||||
application: jellyfin
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# LDAP provider + outpost for Jellyfin native-client app-password validation.
|
||||
#
|
||||
# Jellyfin's web UI signs in via OIDC (see providers_oauth2/jellyfin.yaml), but
|
||||
# native clients (mobile/TV apps, DLNA) cannot do a browser OIDC dance. Those
|
||||
# clients authenticate against this Authentik LDAP outpost using their username
|
||||
# plus an Authentik "App password" token as the bind password. The Jellyfin LDAP
|
||||
# plugin binds as the user; a successful bind == valid app-password.
|
||||
#
|
||||
# bind_mode "direct": the outpost runs the bind_flow (default-authentication-flow)
|
||||
# on every bind request, so app-password revocation takes effect immediately.
|
||||
# search_mode "direct": entries are read live from the Authentik API. Search is
|
||||
# gated by Authentik's directory permissions -- the bound user is NOT granted the
|
||||
# "Search full LDAP directory" permission, so it can only read its own entry,
|
||||
# which is all the Jellyfin plugin needs to resolve the user after bind.
|
||||
#
|
||||
# base_dn is the Authentik default LDAP tree. The module creates the matching
|
||||
# authentik_application (slug jellyfin-ldap) and authentik_outpost
|
||||
# (name jellyfin-ldap-outpost, type ldap). The outpost's API token is generated
|
||||
# by Authentik AFTER apply and must be seeded into KV for the k8s outpost
|
||||
# Deployment to consume (see PR body for the exact path).
|
||||
name: Jellyfin LDAP
|
||||
bind_flow: default-authentication-flow
|
||||
unbind_flow: default-invalidation-flow
|
||||
base_dn: DC=ldap,DC=goauthentik,DC=io
|
||||
bind_mode: direct
|
||||
search_mode: direct
|
||||
mfa_support: true
|
||||
@@ -1,29 +1,45 @@
|
||||
# OAuth2/OIDC provider + application for Jellyfin
|
||||
# (jellyfin.k8s.syd1.au.unkin.net), consumed by jellyfin-plugin-sso (OIDC) so
|
||||
# the web UI signs in via Authentik while native clients keep Jellyfin local/API
|
||||
# auth. client_secret is read from Vault, not committed. The plugin requests the
|
||||
# `ak_groups` scope and matches the hierarchical groups claim against
|
||||
# akP-jellyfin-admin / akP-jellyfin-user for its admin/user role mapping, so no
|
||||
# role_mappings claim is needed here. The path segment "authentik" is the SSO
|
||||
# provider name configured in the plugin — it must match on the Jellyfin side.
|
||||
# OAuth2/OIDC provider + application for the Jellyfin web UI SSO, consumed by
|
||||
# jellyfin-plugin-sso (OIDC) so the browser signs in via Authentik while native
|
||||
# clients use the LDAP outpost (see providers_ldap/jellyfin-ldap.yaml).
|
||||
#
|
||||
# ONE shared confidential client (client_id jellyfin) serves BOTH Jellyfin
|
||||
# instances -- fafflix (adults, jellyfin.k8s.syd1.au.unkin.net) and cheeztv
|
||||
# (kids, cheeztv.unkin.net + cheeztv.k8s.syd1.au.unkin.net). Each instance runs
|
||||
# the SSO plugin with provider name "authentik", so its callback is
|
||||
# https://<host>/sso/OID/redirect/authentik (verified plugin path shape); all
|
||||
# three hosts are listed as strict redirect URIs.
|
||||
#
|
||||
# Access is gated to media users only: akP-media-fafflix and akP-media-cheeztv
|
||||
# carry `application: jellyfin` and bind to this app, so only members of the
|
||||
# media roles (akR-media-adult / akR-media-kids) can authorize. Per-instance
|
||||
# authorization (adults -> fafflix + cheeztv, kids -> cheeztv only) is enforced
|
||||
# downstream by the media proxy reading the hierarchical ak_groups claim.
|
||||
# Admin vs. user role inside Jellyfin is still mapped by the plugin matching the
|
||||
# ak_groups claim against akP-jellyfin-admin / akP-jellyfin-user.
|
||||
#
|
||||
# client_secret is read from Vault (seeded out of band), never committed. Both
|
||||
# instances share this one secret; terraform reads it from the fafflix namespace
|
||||
# path, and the cheeztv Deployment reads the same value from its own namespace.
|
||||
name: Jellyfin
|
||||
authorization_flow: default-provider-authorization-implicit-consent
|
||||
invalidation_flow: default-provider-invalidation-flow
|
||||
client_type: confidential
|
||||
client_id: jellyfin
|
||||
# Explicit dashboard tile launch URL (the Jellyfin web UI).
|
||||
# Explicit dashboard tile launch URL (the fafflix Jellyfin web UI).
|
||||
launch_url: https://jellyfin.k8s.syd1.au.unkin.net/
|
||||
client_secret_vault:
|
||||
mount: kv
|
||||
path: kubernetes/namespace/jellyfin/default/oauth-credentials
|
||||
path: kubernetes/namespace/fafflix/default/oauth-credentials
|
||||
scope_mappings:
|
||||
- goauthentik.io/providers/oauth2/scope-openid
|
||||
- goauthentik.io/providers/oauth2/scope-email
|
||||
- goauthentik.io/providers/oauth2/scope-profile
|
||||
redirect_uris:
|
||||
# jellyfin-plugin-sso OIDC callback (plugin >= 3.5.2).
|
||||
# fafflix (adults)
|
||||
- matching_mode: strict
|
||||
url: https://jellyfin.k8s.syd1.au.unkin.net/sso/OID/redirect/authentik
|
||||
# Legacy plugin callback path, kept so older plugin builds also work.
|
||||
# cheeztv (kids) -- external + in-cluster hostnames
|
||||
- matching_mode: strict
|
||||
url: https://jellyfin.k8s.syd1.au.unkin.net/sso/OID/r/authentik
|
||||
url: https://cheeztv.unkin.net/sso/OID/redirect/authentik
|
||||
- matching_mode: strict
|
||||
url: https://cheeztv.k8s.syd1.au.unkin.net/sso/OID/redirect/authentik
|
||||
|
||||
@@ -169,12 +169,24 @@ resource "authentik_provider_oauth2" "this" {
|
||||
grant_types = each.value.grant_types
|
||||
}
|
||||
|
||||
# Resolve LDAP bind/unbind flows by slug (mirrors the oauth2/saml handling) so
|
||||
# configs reference human-readable flow slugs instead of Authentik UUIDs.
|
||||
data "authentik_flow" "ldap_bind" {
|
||||
for_each = var.providers_ldap
|
||||
slug = each.value.bind_flow
|
||||
}
|
||||
|
||||
data "authentik_flow" "ldap_unbind" {
|
||||
for_each = var.providers_ldap
|
||||
slug = each.value.unbind_flow
|
||||
}
|
||||
|
||||
resource "authentik_provider_ldap" "this" {
|
||||
for_each = var.providers_ldap
|
||||
|
||||
name = each.value.name
|
||||
bind_flow = each.value.bind_flow
|
||||
unbind_flow = each.value.unbind_flow
|
||||
bind_flow = data.authentik_flow.ldap_bind[each.key].id
|
||||
unbind_flow = data.authentik_flow.ldap_unbind[each.key].id
|
||||
base_dn = each.value.base_dn
|
||||
certificate = each.value.certificate
|
||||
tls_server_name = each.value.tls_server_name
|
||||
|
||||
Reference in New Issue
Block a user