The media services are splitting into an adult (fafflix) and kids (cheeztv)
tier, and Authentik group membership will drive the media proxy's routing and
authorization. This adds the two-tier RBAC groups so users can be assigned the
right media access ahead of the provider/application wiring.
- Add akP-media-fafflix and akP-media-cheeztv per-service permission entitlements
(unbound, so they surface in the hierarchical ak_groups claim for the proxy)
- Add akR-media-adult role nesting both fafflix and cheeztv (adults reach both)
- Add akR-media-kids role nesting only cheeztv (kids reach kids services only)
Keep local/default terragrunt runs pointed at the real public URL;
set the goauthentik#954 ClusterIP workaround via TF_VAR_authentik_url
in the CI environment blocks instead of as a terragrunt input.
Parameterise the authentik provider url as var.authentik_url (default:
the environment's public URL) and override it for
identity.k8s.syd1.au.unkin.net to
http://authentik-server.authentik.svc.cluster.local. Combined with
sessionAffinity: ClientIP on the Service, in-cluster CI runs pin to a
single server replica, eliminating the cross-replica read-back race
(goauthentik/terraform-provider-authentik#954).
dnf install reads metadata for every enabled repo and downloads the
vendored vault RPM on every pipeline run. Fetch the pinned upstream zip
from the artifactapi hashicorp-releases remote instead, matching
terraform-vault and terraform-artifactapi.
- Replace dnf install vault with a pinned curl of the vault zip from the
artifactapi hashicorp-releases remote, extracted to /usr/local/bin.
PR plan pipelines were failing with "Error acquiring the state lock"
when they collided with a concurrent apply (or another plan) holding
the lock on the same Consul-backed state.
- plan: pass -lock=false to terragrunt plan; apply keeps locking
The ClickHouse log UI (logviewer) is being exposed at
https://logviewer.unkin.net behind oauth2-proxy in the logging namespace;
this adds the Authentik side as the prerequisite.
- Adds config/providers_oauth2/logviewer.yaml mirroring traefik: confidential
client, client_id logviewer, secret from Vault kv
kubernetes/namespace/logging/default/oauth-credentials,
openid/email/profile scopes (ak_groups is attached to every oauth2
provider by the module), strict redirect URI for the
logviewer oauth2 callback.
- Adds config/permissions/akP-logviewer-admin.yaml bound to the logviewer
application and nests it under akR-global-admin.
The traefik dashboards (internal + external ingress classes) are being
exposed behind oauth2-proxy; this adds the Authentik side as the
prerequisite.
- Adds config/providers_oauth2/traefik.yaml mirroring arrstack: confidential
client, client_id traefik, secret from Vault kv
kubernetes/namespace/traefik-system/default/oauth-credentials,
openid/email/profile scopes (ak_groups is attached to every oauth2
provider by the module), strict redirect URIs for
traefik-internal/traefik-external oauth2 callbacks.
- Adds config/permissions/akP-traefik-admin.yaml bound to the traefik
application and nests it under akR-global-admin.
Jellyfin moves to Authentik SSO via jellyfin-plugin-sso (OIDC), keeping
native clients on Jellyfin local/API auth. Adds the oauth2 provider and
application for jellyfin.k8s.syd1.au.unkin.net plus the akP permission
groups gating access, wired into the standard-user and global-admin
roles per the two-tier RBAC model.
- Adds providers_oauth2/jellyfin.yaml: confidential client, secret read
from kv/kubernetes/namespace/jellyfin/default/oauth-credentials,
redirect URIs for the SSO plugin callback paths
- Adds akP-jellyfin-admin and akP-jellyfin-user bound to the app
- Nests akP-jellyfin-user under akR-standard-user and
akP-jellyfin-admin under akR-global-admin
Add the Authentik OIDC application that fronts the arrproxy media front door
at arrstack.unkin.net, plus the per-app entitlement groups arrproxy reads from
the user's groups claim to decide which backends (sonarr/radarr/prowlarr) a
user may reach.
- config/providers_oauth2/arrstack.yaml: confidential oauth2 client
client_id=arrstack, litellm-style auth/invalidation flows, client_secret
from Vault kv kubernetes/namespace/arrstack/default/oauth-credentials,
openid/email/profile scopes, redirect https://arrstack.unkin.net/oauth2/callback,
launch https://arrstack.unkin.net/. The module always attaches the estate's
hierarchical ak_groups scope mapping, so the front door emits the groups claim.
- config/permissions/akP-arrstack-user.yaml: front-door gate (application: arrstack).
- config/permissions/akP-arrstack-{sonarr,radarr,prowlarr}.yaml: per-app
entitlements, unbound (no application) so they only surface in the ak_groups
claim for arrproxy to authorize backends.
- config/roles/akR-arrstack-user.yaml: full media role nesting all four.
- akR-global-admin: also nests the arrstack front door + all per-app perms.
Ben (akR-global-admin) does not see the LiteLLM tile on the Authentik user
dashboard, while ArgoCD/Grafana/Rancher appear normally. The live API shows
LiteLLM is configured identically to those apps: the app exists, its access
binding akP-litellm-admin -> litellm is present, and akR-global-admin nests
akP-litellm-admin (bidirectionally, same as the others). A CI-style plan against
live state reports "No changes" -- so this is not terraform-correctable drift,
and a plain re-apply fixes nothing. Yet check_access for Ben returns
passing=false for litellm and passing=true for the rest: a stale cached access
policy result inside Authentik.
Add an optional per-app launch_url to the providers_oauth2 config (default null,
which keeps Authentik's redirect-derived URL) and wire it to the application's
meta_launch_url. Set it for LiteLLM to its UI. This makes the dashboard tile
deterministic and, on apply, re-saves the application -- invalidating the stale
access-policy cache so Ben's (already-correct) access re-evaluates and the tile
appears.
Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
OAuth2 providers with no signing_key fall back to HS256, which RS256-only
RP clients (argocd confirmed, and the rest) reject with "unexpected
signature algorithm HS256; expected [RS256]", breaking OIDC login.
- Add data.authentik_certificate_key_pair.signing, resolving the estate's
RSA keypair by name (var.oauth2_signing_key_name, default the built-in
"authentik Self-signed Certificate").
- Default every provider's signing_key to that keypair via coalesce, so all
providers sign with RS256 while keeping the per-yaml signing_key override.
Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
The k8s Gitea drops SSH and serves git.unkin.net (canonical) plus
git.k8s.syd1.au.unkin.net (admin/backup route, live now via external-dns).
Replace the old git2 validation host in the OAuth2 redirect URIs to match
argocd-apps#309.
Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
Register the k8s Gitea forge as an Authentik OIDC app so it can use SSO at
cutover. Redirect URIs cover both the temporary git2 validation host and the
final git.unkin.net host so login works across the migration.
- add config/providers_oauth2/gitea.yaml (confidential OAuth2 provider + app,
client_secret read from kv/kubernetes/namespace/gitea/default/oauth-credentials)
Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
## Why
All Authentik OIDC logins (ArgoCD, Grafana, Rancher, LiteLLM, NetBox) fail
with `invalid_request` / "The request is otherwise malformed". Authentik
2026.5 added an explicit `grant_types` allow-list to the OAuth2 provider
(model default = empty list). Our module never set it, so every provider has
`grant_types = []`, and `authorize.py` rejects the authorization_code grant
(`if self.grant_type not in self.provider.grant_types`) before any user auth.
## Change
- modules/authentik: add a `grant_types` field to the `providers_oauth2`
variable, defaulting to `["authorization_code", "refresh_token"]` (the
standard confidential web-app set), and wire it into
`authentik_provider_oauth2`.
## Plan
`0 to add, 5 to change, 0 to destroy` — each existing oauth2 provider's
`grant_types` goes `[] -> ["authorization_code", "refresh_token"]`; no other
attributes change. Baseline plan (pre-change) was clean (no netbox/state drift).
Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
NetBox is being deployed to k8s (argocd-apps) with Authentik SSO via
python-social-auth's OpenIdConnectAuth backend. Add the confidential OAuth2
provider/application (client_id netbox, openid/email/profile scopes, strict
redirect to /oauth/complete/oidc/); the client_secret is read from Vault at
kubernetes/namespace/netbox/default/oauth-credentials (the terraform-authentik
runner policy already covers namespace/+/default/oauth-credentials).
Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
Bring LiteLLM into the two-tier RBAC and map groups to LiteLLM roles.
- akP-litellm-admin / akP-litellm-user permission groups (bound to the litellm
app for access); added to akR-global-admin / akR-standard-user roles.
- Generic per-provider role_mappings: emit an app role claim computed from
effective (hierarchical) group membership. LiteLLM: emits `litellm_role`
(proxy_admin for akP-litellm-admin, internal_user for akP-litellm-user, else
internal_user_view_only); LiteLLM reads it via GENERIC_USER_ROLE_ATTRIBUTE.
Validated: plan 5 to add, 3 to change; generated role expression renders correctly.
Ceph dashboard SSO is SAML 2.0 (no native OIDC), so onboard it via an Authentik
SAML provider + application. Also resolve SAML authorization/invalidation flows
by slug and the signing keypair by name (mirrors the oauth2 handling), since the
SAML path had not been exercised before.
- config/providers_saml/ceph.yaml: SP entity id/ACS derived from the dashboard
base URL (audience .../auth/saml2/metadata, acs .../auth/saml2, HTTP-POST),
signed with the built-in self-signed keypair.
Ceph side (separate, Puppet): ceph dashboard sso setup saml2
https://dashboard.ceph.unkin.net <authentik-idp-metadata-url>
Validated with `terragrunt plan`: 2 to add (provider + application).
- Permission/role group name now comes from the config filename (the map key),
dropping the redundant `name` field from each YAML and the object types.
- The hierarchical mapping emits an `ak_groups` claim (scope `ak_groups`) instead
of `groups`, so it never collides with the direct-groups the default profile
mapping already emits under `groups` (Authentik overrides same-key claims in an
unpredictable order). Apps request the `ak_groups` scope and read that claim.
Introduce a user -> role -> [permissions] model for app access and roles,
managed declaratively.
- Permission groups (akP-<app>-<access>) under config/permissions/: atomic units,
each names the application it grants access to.
- Role groups (akR-<role>) under config/roles/: what users are assigned to;
each nests permission groups via parents (akR-global-admin -> all *-admin,
akR-standard-user -> all *-user). Split into a separate authentik_group
resource so roles can reference permission ids without self-reference.
- Policy bindings gate each application to its permission groups (and, via
child->parent membership propagation, the roles that nest them).
- Hierarchical `groups` scope mapping: walks user groups up through .parents so
the OIDC claim includes inherited permission groups (works around
goauthentik/authentik#15579). Inert until a provider requests the `groups`
scope, so no behaviour change to existing apps until they opt in.
Validated with `tofu validate`.