Compare commits
34 Commits
77adee64aa
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c6145e057 | |||
| 89a5caf353 | |||
| e90e6d027b | |||
| b3c08369fd | |||
| 9508c151c4 | |||
| 405dede578 | |||
| 4fceaeeee6 | |||
| 1e42fdfc2c | |||
| e2d7de4148 | |||
| 331330872d | |||
| 3f6a046dff | |||
| 3662ff15ca | |||
| d848d9ae86 | |||
| fedbb2fa42 | |||
| e5dbcb56a6 | |||
| cd31cb6fa1 | |||
| 4e164014d7 | |||
| 0d17dc2cf1 | |||
| a6d6681bc9 | |||
| 93c634e4c4 | |||
| 3977f6f86b | |||
| 6d014ce913 | |||
| 230db5ad7e | |||
| 2f80c4a536 | |||
| 5ecd03cdd5 | |||
| 0c6a9104c0 | |||
| 3884a5b21f | |||
| 8ec8bbda8e | |||
| 09f7c9042e | |||
| 1485962cf5 | |||
| 72e65d7810 | |||
| e397fd909f | |||
| 9760c2eb3f | |||
| 3396b399ce |
@@ -22,7 +22,7 @@ endef
|
||||
|
||||
init:
|
||||
@$(call vault_env) && \
|
||||
terragrunt run --all --non-interactive init -- -upgrade
|
||||
terragrunt run --all --non-interactive init
|
||||
|
||||
plan: init
|
||||
@$(call vault_env) && \
|
||||
|
||||
@@ -4,11 +4,13 @@ Terraform configuration for managing the Authentik identity provider at identity
|
||||
|
||||
## Managed Resources
|
||||
|
||||
- **Groups** — roles and group hierarchy (users are invited manually)
|
||||
- **Groups** — roles and group hierarchy (accounts themselves are created elsewhere)
|
||||
- **User role membership** — which `akR-*` roles a human holds (see `config/users/`)
|
||||
- **SAML providers** — SAML application integrations
|
||||
- **OAuth2/OIDC providers** — OAuth2 and OpenID Connect integrations
|
||||
- **LDAP providers** — LDAP provider and outpost configuration
|
||||
- **Applications** — application definitions linked to providers
|
||||
- **Service accounts** — machine identities with RBAC roles and API tokens (keys published to Vault kv)
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -19,7 +21,9 @@ config/
|
||||
├── groups/ # Group definitions
|
||||
├── providers_saml/ # SAML provider definitions
|
||||
├── providers_oauth2/ # OAuth2/OIDC provider definitions
|
||||
└── providers_ldap/ # LDAP provider definitions
|
||||
├── providers_ldap/ # LDAP provider definitions
|
||||
├── service_accounts/ # Automation service accounts + API tokens
|
||||
└── users/ # Human role membership (authoritative per named role)
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -37,5 +37,15 @@ locals {
|
||||
trimsuffix(basename(file_path), ".yaml") => content
|
||||
if startswith(file_path, "providers_ldap/")
|
||||
}
|
||||
service_accounts = {
|
||||
for file_path, content in local.all_configs :
|
||||
trimsuffix(basename(file_path), ".yaml") => content
|
||||
if startswith(file_path, "service_accounts/")
|
||||
}
|
||||
users = {
|
||||
for file_path, content in local.all_configs :
|
||||
trimsuffix(basename(file_path), ".yaml") => content
|
||||
if startswith(file_path, "users/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Permission group akP-artifactapi-admin (name = filename). Grants admin
|
||||
# access to artifactapi: bound to the artifactapi application, gating the web UI.
|
||||
application: artifactapi
|
||||
@@ -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,4 @@
|
||||
# Permission group akP-mediamark-user (name = filename). Grants user access to
|
||||
# mediamark: bound to the mediamark application, gating the kids-content
|
||||
# marking UI.
|
||||
application: mediamark
|
||||
@@ -0,0 +1,3 @@
|
||||
# Permission group akP-repospawner-admin (name = filename). Grants admin
|
||||
# access to repospawner: bound to the repospawner application, gating the UI.
|
||||
application: repospawner
|
||||
@@ -0,0 +1,5 @@
|
||||
# Permission group akP-vault-admin (name = filename). Gates the OpenBao
|
||||
# application: without a binding every Authentik user could complete an OIDC
|
||||
# login, so access is restricted to this group. OpenBao's own policy mapping
|
||||
# keys off the same group name in the ak_groups claim.
|
||||
application: vault
|
||||
@@ -0,0 +1,3 @@
|
||||
# Permission group akP-vlogs-admin (name = filename). Grants admin
|
||||
# access to vlogs: bound to the vlogs application, gating the VictoriaLogs UI.
|
||||
application: vlogs
|
||||
@@ -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: kv/kubernetes/namespace/authentik/default/outpost-token (key: token).
|
||||
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,9 +1,18 @@
|
||||
# OAuth2/OIDC provider + application for the in-cluster ArgoCD
|
||||
# (argocd.k8s.syd1.au.unkin.net). client_secret is read from Vault, not committed.
|
||||
# (argocd.k8s.syd1.au.unkin.net), serving the web UI, the `argocd` CLI and the
|
||||
# ArgoCD mobile app.
|
||||
#
|
||||
# public, not confidential: the native clients cannot hold a secret, and they
|
||||
# cannot have their own client either -- Authentik derives the `iss` claim from
|
||||
# the application slug, while ArgoCD validates every token against the single
|
||||
# issuer in oidc.config, so a second application would issue tokens ArgoCD
|
||||
# rejects. One client for all three; the strict redirect URIs below are the
|
||||
# control. Authentik ignores the secret for public clients, but the Vault read
|
||||
# stays so argocd-cm's `$argocd-oidc:client_secret` keeps resolving.
|
||||
name: ArgoCD
|
||||
authorization_flow: default-provider-authorization-implicit-consent
|
||||
invalidation_flow: default-provider-invalidation-flow
|
||||
client_type: confidential
|
||||
client_type: public
|
||||
client_id: argocd
|
||||
client_secret_vault:
|
||||
mount: kv
|
||||
@@ -19,3 +28,6 @@ redirect_uris:
|
||||
# `argocd login --sso` CLI callback (local listener).
|
||||
- matching_mode: strict
|
||||
url: http://localhost:8085/auth/callback
|
||||
# Mobile app callback (custom URL scheme, PKCE).
|
||||
- matching_mode: strict
|
||||
url: argocd://auth/callback
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# OAuth2/OIDC provider + application for the artifactapi web UI
|
||||
# (https://artifactapi.k8s.syd1.au.unkin.net/ui in the artifactapi namespace).
|
||||
# An oauth2-proxy fronts ONLY /ui and /oauth2; access is gated on the user's
|
||||
# hierarchical ak_groups claim (akP-artifactapi-admin). The package-manager
|
||||
# surfaces (/api/v1, /api/v2, /v2, /terraform, /.well-known) are NOT behind this
|
||||
# provider -- yum, containerd, docker, terraform and CI cannot do a browser flow.
|
||||
# client_secret is read from Vault (seeded out of band), never committed.
|
||||
name: artifactapi
|
||||
authorization_flow: default-provider-authorization-implicit-consent
|
||||
invalidation_flow: default-provider-invalidation-flow
|
||||
client_type: confidential
|
||||
client_id: artifactapi
|
||||
launch_url: https://artifactapi.k8s.syd1.au.unkin.net/ui/
|
||||
client_secret_vault:
|
||||
mount: kv
|
||||
path: kubernetes/namespace/artifactapi/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:
|
||||
- matching_mode: strict
|
||||
url: https://artifactapi.k8s.syd1.au.unkin.net/oauth2/callback
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# OAuth2/OIDC provider + application for mediamark (the Go web UI for marking
|
||||
# media as kids content, served at https://mediamark.unkin.net (external
|
||||
# hostname) and https://mediamark.k8s.syd1.au.unkin.net (cluster hostname) in
|
||||
# its own mediamark namespace). An oauth2-proxy in front of the UI performs the
|
||||
# OIDC login with a relative redirect, so both hostnames must be registered;
|
||||
# access is gated on the user's hierarchical ak_groups claim
|
||||
# (akP-mediamark-user).
|
||||
# client_secret is read from Vault (seeded out of band), never committed.
|
||||
name: mediamark
|
||||
authorization_flow: default-provider-authorization-implicit-consent
|
||||
invalidation_flow: default-provider-invalidation-flow
|
||||
client_type: confidential
|
||||
client_id: mediamark
|
||||
launch_url: https://mediamark.unkin.net/
|
||||
client_secret_vault:
|
||||
mount: kv
|
||||
path: kubernetes/namespace/mediamark/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:
|
||||
- matching_mode: strict
|
||||
url: https://mediamark.k8s.syd1.au.unkin.net/oauth2/callback
|
||||
- matching_mode: strict
|
||||
url: https://mediamark.unkin.net/oauth2/callback
|
||||
@@ -0,0 +1,25 @@
|
||||
# OAuth2/OIDC provider + application for repospawner (the internal repository
|
||||
# provisioning admin tool, served at
|
||||
# https://repospawner.k8s.syd1.au.unkin.net (cluster hostname) and
|
||||
# https://repospawner.unkin.net (external hostname) in the repospawner namespace).
|
||||
# An oauth2-proxy in front of the UI performs the OIDC login; access is gated on
|
||||
# the user's hierarchical ak_groups claim (akP-repospawner-admin).
|
||||
# client_secret is read from Vault (seeded out of band), never committed.
|
||||
name: repospawner
|
||||
authorization_flow: default-provider-authorization-implicit-consent
|
||||
invalidation_flow: default-provider-invalidation-flow
|
||||
client_type: confidential
|
||||
client_id: repospawner
|
||||
launch_url: https://repospawner.unkin.net/
|
||||
client_secret_vault:
|
||||
mount: kv
|
||||
path: kubernetes/namespace/repospawner/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:
|
||||
- matching_mode: strict
|
||||
url: https://repospawner.k8s.syd1.au.unkin.net/oauth2/callback
|
||||
- matching_mode: strict
|
||||
url: https://repospawner.unkin.net/oauth2/callback
|
||||
@@ -0,0 +1,32 @@
|
||||
# OAuth2/OIDC provider + application for OpenBao (the estate's Vault), making
|
||||
# Authentik the default *human* login. Machine auth (approle, k8s, CI) and the
|
||||
# break-glass paths are untouched and stay on the OpenBao side.
|
||||
#
|
||||
# client_secret is generated here and written to kv/service/authentik/oidc-vault
|
||||
# ({client_id, client_secret}); the companion terraform-vault change reads it to
|
||||
# configure the OIDC auth mount. Nothing is seeded by hand.
|
||||
name: OpenBao
|
||||
authorization_flow: default-provider-authorization-implicit-consent
|
||||
invalidation_flow: default-provider-invalidation-flow
|
||||
client_type: confidential
|
||||
client_id: vault
|
||||
client_secret_vault:
|
||||
mount: kv
|
||||
path: service/authentik/oidc-vault
|
||||
generate: true
|
||||
scope_mappings:
|
||||
- goauthentik.io/providers/oauth2/scope-openid
|
||||
- goauthentik.io/providers/oauth2/scope-email
|
||||
- goauthentik.io/providers/oauth2/scope-profile
|
||||
redirect_uris:
|
||||
# `bao login -method=oidc` CLI callback (local listener, fixed port 8250).
|
||||
- matching_mode: strict
|
||||
url: http://localhost:8250/oidc/callback
|
||||
# UI SSO callback, gateway host (traefik-internal -> vault svc :8200).
|
||||
- matching_mode: strict
|
||||
url: https://vault.k8s.syd1.au.unkin.net/ui/vault/auth/oidc/oidc/callback
|
||||
# UI SSO callback, direct Consul service address (the address the estate
|
||||
# documents for Vault access; any node forwards to the active replica).
|
||||
- matching_mode: strict
|
||||
url: https://vault.service.consul:8200/ui/vault/auth/oidc/oidc/callback
|
||||
launch_url: https://vault.k8s.syd1.au.unkin.net/ui/
|
||||
@@ -0,0 +1,21 @@
|
||||
# OAuth2/OIDC provider + application for vlogs (the VictoriaLogs query UI,
|
||||
# served at https://vlogs.unkin.net in the vlogs namespace). An oauth2-proxy
|
||||
# in front of the UI performs the OIDC login; access is gated on the user's
|
||||
# hierarchical ak_groups claim (akP-vlogs-admin).
|
||||
# client_secret is read from Vault (seeded out of band), never committed.
|
||||
name: vlogs
|
||||
authorization_flow: default-provider-authorization-implicit-consent
|
||||
invalidation_flow: default-provider-invalidation-flow
|
||||
client_type: confidential
|
||||
client_id: vlogs
|
||||
launch_url: https://vlogs.unkin.net/
|
||||
client_secret_vault:
|
||||
mount: kv
|
||||
path: kubernetes/namespace/vlogs/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:
|
||||
- matching_mode: strict
|
||||
url: https://vlogs.unkin.net/oauth2/callback
|
||||
@@ -1,4 +1,6 @@
|
||||
# Role akR-global-admin (name = filename): full admin across all onboarded apps.
|
||||
# Nests akP-media-fafflix / akP-media-cheeztv so admins pass the jellyfin
|
||||
# application gate and can reach the akP-jellyfin-admin rights nested below.
|
||||
permissions:
|
||||
- akP-grafana-admin
|
||||
- akP-argocd-admin
|
||||
@@ -7,10 +9,16 @@ permissions:
|
||||
- akP-jellyfin-admin
|
||||
- akP-traefik-admin
|
||||
- akP-logviewer-admin
|
||||
- akP-vlogs-admin
|
||||
- akP-watchstate-admin
|
||||
- akP-repospawner-admin
|
||||
- akP-vault-admin
|
||||
- akP-artifactapi-admin
|
||||
# arrstack has no admin tier (it is a proxy front door); grant global admins
|
||||
# the front door plus every per-app entitlement so they reach all media apps.
|
||||
- akP-arrstack-user
|
||||
- akP-arrstack-sonarr
|
||||
- akP-arrstack-radarr
|
||||
- akP-arrstack-prowlarr
|
||||
- akP-media-fafflix
|
||||
- akP-media-cheeztv
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
# adult ("fafflix") and kids ("cheeztv") services, so this role nests both
|
||||
# per-service entitlements. Membership propagates child -> parent, so a member
|
||||
# appears in both permission groups in the hierarchical `ak_groups` claim and the
|
||||
# media proxy routes/authorizes them for fafflix and cheeztv.
|
||||
# media proxy routes/authorizes them for fafflix and cheeztv. Adults also curate
|
||||
# which titles are kids content, so the role nests akP-mediamark-user for access
|
||||
# to the mediamark UI.
|
||||
permissions:
|
||||
- akP-media-fafflix
|
||||
- akP-media-cheeztv
|
||||
- akP-mediamark-user
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Service account sa-agent-api (username = filename). Machine identity for
|
||||
# estate automation that needs to read Authentik outpost tokens; replaces the
|
||||
# hand-created token an operator used to paste into Vault.
|
||||
name: Agent API
|
||||
permissions:
|
||||
# List outposts and read their bootstrap token keys.
|
||||
- authentik_outposts.view_outpost
|
||||
- authentik_core.view_token
|
||||
- authentik_core.view_token_key
|
||||
tokens:
|
||||
agent-api-token:
|
||||
description: >-
|
||||
Used by agentvault seed-outpost to look up Authentik outpost tokens.
|
||||
Managed by terraform-authentik; key published to Vault kv.
|
||||
expiring: false
|
||||
vault:
|
||||
mount: kv
|
||||
path: service/authentik/agent-api-token
|
||||
key: token
|
||||
@@ -0,0 +1,17 @@
|
||||
# users
|
||||
|
||||
One file per human, `<username>.yaml`, listing the `akR-*` roles they hold:
|
||||
|
||||
```yaml
|
||||
# Human user jane (username = filename). The account itself is not managed here
|
||||
# (humans come from LDAP sync / invite); only its role membership is.
|
||||
roles:
|
||||
- akR-media-adult
|
||||
```
|
||||
|
||||
The account is looked up by username and must already exist — nothing here
|
||||
creates users. A role that has no `config/roles/<name>.yaml` fails the plan.
|
||||
|
||||
**Naming a role here makes Terraform authoritative over that role's entire
|
||||
member list**: members added by hand in the Authentik UI for that role are
|
||||
removed on the next apply. Roles no user file names are left untouched.
|
||||
@@ -23,4 +23,6 @@ inputs = {
|
||||
providers_saml = local.config.providers_saml
|
||||
providers_oauth2 = local.config.providers_oauth2
|
||||
providers_ldap = local.config.providers_ldap
|
||||
service_accounts = local.config.service_accounts
|
||||
users = local.config.users
|
||||
}
|
||||
|
||||
Generated
+56
-33
@@ -2,45 +2,68 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/goauthentik/authentik" {
|
||||
version = "2026.5.0"
|
||||
constraints = ">= 2026.5.0"
|
||||
version = "2026.5.1"
|
||||
constraints = "2026.5.1"
|
||||
hashes = [
|
||||
"h1:SeznjPKBzSrgo8WasRnuxiGMDSeQHEKsv3U/xw8bhQE=",
|
||||
"zh:0dc1706f6fbff866f4a96de56a4934b9a277954bcdd0713549a29a9b8ec85153",
|
||||
"zh:218417ec4e864f2d7e585d6c08d39bccb96d8f3bca16c6f762be15365e434234",
|
||||
"zh:24f9afa7a1174316da3478811848cd76ef348d8a983310b8d75ed6f45abe1a92",
|
||||
"zh:560092e47cb8a72b890b3eeafe1803202cd25cf27f5f5a6e2c370f645f5d86ae",
|
||||
"zh:5bc69d8de198007ad1587e146f98cffacf0d1a571800da549b308ff5f4541474",
|
||||
"zh:65248dce941472ad2a30d0754d2f3c2db6bb6fe5080946316fb097d6ba7cc79f",
|
||||
"zh:79c9a59a8d3c60280e27a064668889594da44c60f940b046b7c8e63be01067d0",
|
||||
"zh:87f26cadcd842d6e6d0af94ef0e56860557f5d07f487b10d69d38b63af68bea5",
|
||||
"zh:8e42c9d0e77d61cc2e5f8c8b761f6e484774d93771927b4cb5fbdae41209dd33",
|
||||
"zh:94ff632b9b4841527c6b652d51a850a8a47c84c0308a3efc189e0ff7e2558f87",
|
||||
"zh:b8d32d9f17a905b63c87a23306c02c295b7c8b70f72950071aa3086396932816",
|
||||
"zh:c91982af99474fc2e4e69be36ed3a68847f261963ed79f6a546fc75703992f99",
|
||||
"zh:eb9c1fd3020cf61e9b7a6a38d2965f4b521495a9928705e963459a4af857f97d",
|
||||
"h1:L9q3pjCoeKQdn0/OApv4O1HiC/PDLDqcnM7Ff5XFB+0=",
|
||||
"zh:05f252734db99792e5d3f52b582a4e0027348ac2614d8c9621bf7e623cff8036",
|
||||
"zh:0e1cd8041650aed9bb8d4af23301e786223ed5d1a00c2a5150b7ad874ae03917",
|
||||
"zh:26b1daebd30650df411f903248e061cf963712a063b38e731782fb13a5755ddf",
|
||||
"zh:362b536ff6bf8866c7c1094c3e2322c22b8c36d8d4caa4db0397cebbcad08561",
|
||||
"zh:3d94367b853960f5e88efe3ae69d995d02a11dc8d60ec4d7413a37aabf78b379",
|
||||
"zh:3e840e204677eabe562d17c2ad12afdfa250098725bebac4e91fb8934e233c22",
|
||||
"zh:4db66139d7a9ec6f7852538573261010d48f0a939e2fe8eeb5c55b01149e3629",
|
||||
"zh:7b88a830fb26c697f9d240f498f604485454b12e56b7604eda4ca35d10660ecd",
|
||||
"zh:851bc8bd2d4a16dc71f30da17b6285aa7d85f4bc4175fde7b8319a6a8987ec65",
|
||||
"zh:8bdfc2c70271d48126eef3b1a30d0d132e868d2118ae7277323ed4c905636e4a",
|
||||
"zh:e85c03eac43f23f25ad5443a2984cc67977c3abdcb4c4c426d37342c2f25c37e",
|
||||
"zh:efc296826a700a4e90ee06f8bd3b137617486ce774772aabed3053d4ef8dd906",
|
||||
"zh:f249033dc242c51600cd3968535efa0780352d5ae7435e8ab1482a7ccc9c2f5e",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.opentofu.org/hashicorp/random" {
|
||||
version = "3.9.1"
|
||||
constraints = ">= 3.6.0"
|
||||
hashes = [
|
||||
"h1:CEQeHfnUDB3uqAkKoEWfWgbj+kpoQHgcuPbAjPzbh+U=",
|
||||
"zh:09aaf19b0d22726d2378e0e89fbbefc183494d7bd585759d6c4e69ba50951a2f",
|
||||
"zh:31575ca9bc0db20337096d178ea73bce3ebca343ed071c67f78cf39f800c9ec6",
|
||||
"zh:624fb6ed552abc34a5aaac41e76a373da65ac08e524b09b672f29c60e6ac896a",
|
||||
"zh:6a4760d55132b9750ac1a04f6fc32e247034daa999f71452dba9cbca225a529a",
|
||||
"zh:768a6047cfb8958e7b0b120c580aa3de6624a7fbb2c56ad6df85cd559ed26ec7",
|
||||
"zh:8983c788ba660bcb587e64ff9c3e4323515caf78facbe0abe6432e7aff8df893",
|
||||
"zh:8d570eb026a4f00b58a1d36be0ce3c13adf4d973efcd4162b05cb295bbc14257",
|
||||
"zh:a2259540854d5f699c36b89244fb202ebb2c219b64669a51072687d04fb47152",
|
||||
"zh:aaa51d905b0e80a28e02f9bee2cf6c91ffade7389d77ab9198aa12809ed04955",
|
||||
"zh:afb60995e98573facddfb47baedf7e288408680eb00b5d3df570611758947c72",
|
||||
"zh:b9a46d852ce53fa037f47537a7de53f37b759ccf211600b7ba44c66ba4b616b7",
|
||||
"zh:bafcfeeefcd0dfefeff120b655b45edb0497c4717534ffe5201b3cb556d1ffe6",
|
||||
"zh:c3ac24d397eae054aca2290e20943e0c767592cc661c890850c25ac01829308d",
|
||||
"zh:eafba4127ebadcc5ed0e427935c66fb5e2da7cfdaae39a66d52f4a50d51faf1e",
|
||||
"zh:f39d4bce213ed9bba3474bad468136af08ff6c4c33adaafcc10c1f78067adfe3",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.opentofu.org/hashicorp/vault" {
|
||||
version = "5.10.1"
|
||||
version = "5.12.0"
|
||||
constraints = ">= 4.0.0"
|
||||
hashes = [
|
||||
"h1:wo5cTkl/1nlxMfdn1yEDIHNoRLMczuK6COH2Id4/zeY=",
|
||||
"zh:0abf976c01f0c0732d0ccc6481e52008be5ee9c8e3d9b5eba0573c640fcf7019",
|
||||
"zh:2aff4d7ee7ba9eb3de2cd5cda16ba92b4ec7a2b43232aec180984241a323b216",
|
||||
"zh:2cc186fd0bfc44e100a22b0b40ae8ddcd0ec210a53c1da65d310ee758b1d2b08",
|
||||
"zh:3f8fb8594736b34af4b26437dd4df4dd4042ad4905223995cfebb8a1f10682ec",
|
||||
"zh:47fb41b18b74073f557dbcd6aad2183e416293405ccd70c0691a279cfe97f8cd",
|
||||
"zh:517e2f2764d671c22d22def0384fdfc521b456458189189c0363375495d114dc",
|
||||
"zh:5a49a2003636f2b8a547d494a6c06d43d62a68299775305408c52eff22b1c11f",
|
||||
"zh:66d4e716920ada84b0c768f4aca4c8948388995462923349a01bd3818d82b618",
|
||||
"zh:7599f652e89a3f18fa4b76a59d115cc63255cc36ce6b273850509ba25031abca",
|
||||
"zh:9c3e38ae7e670de973b6255d7050f526cd2b3ca7c383d7ba7226fc204d97c507",
|
||||
"zh:d04b046023fa9fd69def678f27e001c298ea34fc99ba51f835cda82e496fdb57",
|
||||
"zh:d9acd8810f6660cd51bb4c25596632984ae18e93340c82a102d074c6eac95151",
|
||||
"zh:e161bcb9a22607270b980eeff2ba693335fb62d6978516dff93dd4c91cda99b3",
|
||||
"zh:ef47502f08cfcb5311b7b16a7905e0052bf28359e07cc00ed080ef454e0946cf",
|
||||
"zh:f0640ddb52e7e90c5006ff571f6ad0554e593665320c764c57a3d8b7ec31b490",
|
||||
"h1:HVdhw1ShP/LlYuDOdVKmLO/kLVfngw9VHM2YfDl879Y=",
|
||||
"zh:070709539eeff2dbf6af13269e6a3c14cea0ee4e5a5fff237d574805df2787e9",
|
||||
"zh:0e017e993252c37008dcf1848deb32f15a14b303335c3e06885d7efea07fef1d",
|
||||
"zh:1717ba0a0f8d906ced0eb5a3ed1e2a1808d824868da5095ea1da12d60904777d",
|
||||
"zh:218f879c5f3f97564f4867bcbb419197c4aa4e41ce5bc3eed50c71926f0519f9",
|
||||
"zh:2437d8f76d6220883c96a073801af973db957c8b1c79187256c204437f4ed08e",
|
||||
"zh:9dce3198d2f03ce05c3fb4cb90433de5863f0d1818fc4abc3e3576f562409c85",
|
||||
"zh:a51d00c192e6cf86d588501d8c4a2a37a258679784651909d55f64d28011b28d",
|
||||
"zh:c99586f88b0166d5522dedfbec9d246a51efae346bbdfcaeb296ff72b5c80f94",
|
||||
"zh:cee10cca2a295e4fd9ce2779239f7f79ab97328d9e30833924cceb349e509051",
|
||||
"zh:d9b5cde1c02db0b211ec3f77b519122f93b442594ce9f20d386ab5d0d05ead93",
|
||||
"zh:deafe53fa413e71dac120177a58b678e81a64279a692ba77eca09a9bce0bff1a",
|
||||
"zh:f372b83c70ee5dabd892bd9ec1eb9127fe6331d88178b7b84fa5f52f616b59df",
|
||||
"zh:f47e4fd82b72e62a5e1eeaaa1b72cc2709545a8431964123b24ed5c03a03e492",
|
||||
"zh:f629a86c98be3fd2aacb58336d26ce268a76fe754b048f58a3faa0712bb4a837",
|
||||
"zh:feaef85debc4a4a72d7a23988b18fff0b4dbc47f37e31a0b51022df071ac7b85",
|
||||
]
|
||||
}
|
||||
|
||||
+182
-4
@@ -16,6 +16,31 @@ resource "authentik_group" "permission" {
|
||||
attributes = jsonencode(each.value.attributes)
|
||||
}
|
||||
|
||||
# Humans are created outside this module (LDAP sync / invite), so they are
|
||||
# looked up rather than declared: resolve the username to the numeric pk that
|
||||
# group membership is keyed on. A missing account fails the plan; nothing here
|
||||
# can create a user.
|
||||
data "authentik_user" "human" {
|
||||
for_each = var.users
|
||||
username = each.key
|
||||
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = length(setsubtract(each.value.roles, keys(var.role_groups))) == 0
|
||||
error_message = "config/users/${each.key}.yaml names a role with no config/roles/<name>.yaml."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
# Invert user -> roles into role -> member pks. Only roles some user file
|
||||
# names appear here; every other role falls through to a null `users` below.
|
||||
role_members = {
|
||||
for role in distinct(flatten([for u, v in var.users : v.roles])) :
|
||||
role => [for u, v in var.users : data.authentik_user.human[u].pk if contains(v.roles, role)]
|
||||
}
|
||||
}
|
||||
|
||||
# Role groups (akR-*): what users are assigned to. Each nests permission groups
|
||||
# as parents, so a role member is an effective member of every permission it
|
||||
# grants. Separate resource from permissions so this reference is not a
|
||||
@@ -27,6 +52,10 @@ resource "authentik_group" "role" {
|
||||
is_superuser = each.value.is_superuser
|
||||
parents = [for p in each.value.permissions : authentik_group.permission[p].id]
|
||||
attributes = jsonencode(each.value.attributes)
|
||||
# Authoritative: a role claimed by config/users/ has exactly these members, so
|
||||
# one dropped from a user file is removed. null (every unclaimed role) leaves
|
||||
# the attribute computed, i.e. membership stays whatever Authentik holds.
|
||||
users = lookup(local.role_members, each.key, null)
|
||||
}
|
||||
|
||||
# Emit an `ak_groups` claim containing the user's groups AND all inherited
|
||||
@@ -136,12 +165,53 @@ data "authentik_property_mapping_provider_scope" "oauth2" {
|
||||
managed_list = each.value.scope_mappings
|
||||
}
|
||||
|
||||
locals {
|
||||
# Providers whose client secret is pre-seeded in Vault and only read here.
|
||||
oauth2_secret_read = {
|
||||
for k, v in var.providers_oauth2 : k => v
|
||||
if v.client_secret_vault != null && !v.client_secret_vault.generate
|
||||
}
|
||||
# Providers whose client secret is generated here and published to Vault, so
|
||||
# onboarding needs no operator seeding the path first.
|
||||
oauth2_secret_generate = {
|
||||
for k, v in var.providers_oauth2 : k => v
|
||||
if v.client_secret_vault != null && v.client_secret_vault.generate
|
||||
}
|
||||
|
||||
oauth2_client_secret = merge(
|
||||
{ for k, v in local.oauth2_secret_read : k => data.vault_kv_secret_v2.oauth2[k].data["client_secret"] },
|
||||
{ for k, v in local.oauth2_secret_generate : k => random_password.oauth2_client_secret[k].result },
|
||||
)
|
||||
}
|
||||
|
||||
data "vault_kv_secret_v2" "oauth2" {
|
||||
for_each = { for k, v in var.providers_oauth2 : k => v if v.client_secret_vault != null }
|
||||
for_each = local.oauth2_secret_read
|
||||
mount = each.value.client_secret_vault.mount
|
||||
name = each.value.client_secret_vault.path
|
||||
}
|
||||
|
||||
# Alphanumeric only: the secret is pasted into consumer configs and CLI flags,
|
||||
# where punctuation is an easy way to hit shell/URL escaping bugs.
|
||||
resource "random_password" "oauth2_client_secret" {
|
||||
for_each = local.oauth2_secret_generate
|
||||
|
||||
length = 64
|
||||
special = false
|
||||
}
|
||||
|
||||
# Publish the generated credential so consumers (terraform-vault, app configs)
|
||||
# read it from Vault instead of an operator copying it out of Authentik.
|
||||
resource "vault_kv_secret_v2" "oauth2_client_secret" {
|
||||
for_each = local.oauth2_secret_generate
|
||||
|
||||
mount = each.value.client_secret_vault.mount
|
||||
name = each.value.client_secret_vault.path
|
||||
data_json = jsonencode({
|
||||
client_id = each.value.client_id
|
||||
client_secret = random_password.oauth2_client_secret[each.key].result
|
||||
})
|
||||
}
|
||||
|
||||
# Default JWT signing key for OAuth2 providers. Without a signing_key Authentik
|
||||
# signs ID tokens with HS256, which RS256-only RP clients (argocd, grafana, ...)
|
||||
# reject. Look up the estate's RSA keypair by name so providers default to RS256.
|
||||
@@ -157,7 +227,7 @@ resource "authentik_provider_oauth2" "this" {
|
||||
invalidation_flow = data.authentik_flow.oauth2_invalidation[each.key].id
|
||||
client_type = each.value.client_type
|
||||
client_id = each.value.client_id
|
||||
client_secret = each.value.client_secret_vault != null ? data.vault_kv_secret_v2.oauth2[each.key].data["client_secret"] : null
|
||||
client_secret = lookup(local.oauth2_client_secret, each.key, null)
|
||||
property_mappings = concat(
|
||||
try(data.authentik_property_mapping_provider_scope.oauth2[each.key].ids, []),
|
||||
[authentik_property_mapping_provider_scope.groups_hierarchical.id],
|
||||
@@ -169,12 +239,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
|
||||
@@ -230,3 +312,99 @@ resource "authentik_policy_binding" "app_access" {
|
||||
group = authentik_group.permission[each.key].id
|
||||
order = 0
|
||||
}
|
||||
|
||||
# Created server-side, but its post-create read-back hit the Authentik Postgres
|
||||
# read replica before the row replicated and 404'd, so it never reached state.
|
||||
import {
|
||||
to = authentik_policy_binding.app_access["akP-artifactapi-admin"]
|
||||
id = "c9f22628-d48c-477b-b9ac-a952c7d081ce"
|
||||
}
|
||||
|
||||
# Service accounts: non-human identities for automation. Kept out of the group
|
||||
# hierarchy above (which models human app access) and given capabilities through
|
||||
# RBAC roles instead.
|
||||
resource "authentik_user" "service_account" {
|
||||
for_each = var.service_accounts
|
||||
|
||||
username = each.key
|
||||
name = coalesce(each.value.name, each.key)
|
||||
type = "service_account"
|
||||
# roles is only populated for accounts that declare permissions; try() keeps
|
||||
# the reference lazy so accounts without a role still plan.
|
||||
roles = try([authentik_rbac_role.service_account[each.key].id], [])
|
||||
}
|
||||
|
||||
# One role per service account carrying its global permissions.
|
||||
# authentik_rbac_permission_user is deprecated in favour of the role form, so
|
||||
# permissions are attached to a role and the role to the account.
|
||||
resource "authentik_rbac_role" "service_account" {
|
||||
for_each = { for k, v in var.service_accounts : k => v if length(v.permissions) > 0 }
|
||||
|
||||
name = each.key
|
||||
}
|
||||
|
||||
locals {
|
||||
service_account_permissions = merge([
|
||||
for k, v in var.service_accounts : {
|
||||
for perm in v.permissions : "${k}/${perm}" => {
|
||||
service_account = k
|
||||
permission = perm
|
||||
}
|
||||
}
|
||||
]...)
|
||||
|
||||
# Keyed by account/identifier so two accounts reusing an identifier do not
|
||||
# collapse into one entry under merge().
|
||||
service_account_tokens = merge([
|
||||
for k, v in var.service_accounts : {
|
||||
for identifier, t in v.tokens : "${k}/${identifier}" => merge(t, {
|
||||
service_account = k
|
||||
identifier = identifier
|
||||
})
|
||||
}
|
||||
]...)
|
||||
}
|
||||
|
||||
resource "authentik_rbac_permission_role" "service_account" {
|
||||
for_each = local.service_account_permissions
|
||||
|
||||
role = authentik_rbac_role.service_account[each.value.service_account].id
|
||||
permission = each.value.permission
|
||||
}
|
||||
|
||||
# retrieve_key is required for `key` to be populated; without it the attribute
|
||||
# stays empty and nothing can be published to Vault.
|
||||
resource "authentik_token" "service_account" {
|
||||
for_each = local.service_account_tokens
|
||||
|
||||
identifier = each.value.identifier
|
||||
user = authentik_user.service_account[each.value.service_account].id
|
||||
description = each.value.description
|
||||
intent = "api"
|
||||
expiring = each.value.expiring
|
||||
retrieve_key = true
|
||||
}
|
||||
|
||||
# Publish token keys to kv-v2 so consumers (agentvault, CI) read them from Vault.
|
||||
# The key also lands in Terraform state, same as the oauth2 client secrets this
|
||||
# module already reads.
|
||||
resource "vault_kv_secret_v2" "service_account_token" {
|
||||
for_each = { for k, v in local.service_account_tokens : k => v if v.vault != null }
|
||||
|
||||
mount = each.value.vault.mount
|
||||
name = each.value.vault.path
|
||||
data_json = jsonencode({ (each.value.vault.key) = authentik_token.service_account[each.key].key })
|
||||
}
|
||||
|
||||
# One-off re-address for the tokens that existed before the map was namespaced
|
||||
# by service account. Without these the rekey reads as destroy+create and the
|
||||
# published token key rotates. Safe to drop once applied.
|
||||
moved {
|
||||
from = authentik_token.service_account["agent-api-token"]
|
||||
to = authentik_token.service_account["sa-agent-api/agent-api-token"]
|
||||
}
|
||||
|
||||
moved {
|
||||
from = vault_kv_secret_v2.service_account_token["agent-api-token"]
|
||||
to = vault_kv_secret_v2.service_account_token["sa-agent-api/agent-api-token"]
|
||||
}
|
||||
|
||||
@@ -60,9 +60,13 @@ variable "providers_oauth2" {
|
||||
client_id = string
|
||||
# client_secret is never committed. Point at a Vault kv-v2 secret whose
|
||||
# `client_secret` key holds the value (seeded out of band); TF reads it.
|
||||
# generate = true flips that around: the secret is created here and written
|
||||
# to that path as {client_id, client_secret}, so onboarding needs no manual
|
||||
# seed. Requires write access to the kv path.
|
||||
client_secret_vault = optional(object({
|
||||
mount = string
|
||||
path = string
|
||||
mount = string
|
||||
path = string
|
||||
generate = optional(bool, false)
|
||||
}), null)
|
||||
# Managed identifiers of scope property mappings (e.g.
|
||||
# goauthentik.io/providers/oauth2/scope-openid). Resolved to ids.
|
||||
@@ -127,3 +131,43 @@ variable "providers_ldap" {
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
# Machine identities for automation (agents, CI). Each entry creates a service
|
||||
# account user, an RBAC role carrying its global permissions, and any API tokens
|
||||
# it needs. The username is the map key (the config filename).
|
||||
variable "service_accounts" {
|
||||
type = map(object({
|
||||
name = optional(string, null) # display name; defaults to the key
|
||||
description = optional(string, "")
|
||||
# Global RBAC permissions granted via a dedicated role, in
|
||||
# `<app_label>.<codename>` form (e.g. authentik_outposts.view_outpost).
|
||||
permissions = optional(list(string), [])
|
||||
# API tokens keyed by identifier. Set `vault` to publish the generated key
|
||||
# into kv-v2 so consumers read it from Vault instead of an operator pasting it.
|
||||
tokens = optional(map(object({
|
||||
description = optional(string, "")
|
||||
expiring = optional(bool, false)
|
||||
vault = optional(object({
|
||||
mount = string
|
||||
path = string
|
||||
key = optional(string, "token")
|
||||
}), null)
|
||||
})), {})
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
# Human role membership. The username is the map key (the config filename). The
|
||||
# account itself is never managed here — humans are created by LDAP sync/invite
|
||||
# and only looked up — so this grants and revokes roles, it does not make users.
|
||||
#
|
||||
# OWNERSHIP: naming a role in any user file makes Terraform authoritative over
|
||||
# that role's entire member list, so members added by hand in the UI for that
|
||||
# role are removed on the next apply. Roles no user file names are left alone.
|
||||
variable "users" {
|
||||
type = map(object({
|
||||
# keys into var.role_groups (akR-*) this user is a member of.
|
||||
roles = optional(list(string), [])
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
terraform {
|
||||
required_version = ">= 1.10"
|
||||
required_providers {
|
||||
# 2026.8.0 requires pbm_uuid on applications; the deployed 2026.5.3 server does not return it.
|
||||
authentik = {
|
||||
source = "goauthentik/authentik"
|
||||
version = ">= 2026.5.0"
|
||||
version = "2026.5.1"
|
||||
}
|
||||
vault = {
|
||||
source = "hashicorp/vault"
|
||||
version = ">= 4.0.0"
|
||||
}
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = ">= 3.6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user