Files
terraform-authentik/modules/authentik/main.tf
T
unkinben 9c5937776e
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline failed
Add Ceph dashboard SAML provider
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).
2026-07-19 02:22:23 +10:00

185 lines
6.7 KiB
Terraform

resource "authentik_group" "this" {
for_each = var.groups
name = each.value.name
is_superuser = each.value.is_superuser
parents = each.value.parents
attributes = jsonencode(each.value.attributes)
}
# Permission groups (akP-*): atomic, leaf groups. Mapped to app roles via the
# groups claim and bound to applications for access.
resource "authentik_group" "permission" {
for_each = var.permission_groups
name = each.key
attributes = jsonencode(each.value.attributes)
}
# 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
# self-reference (authentik_group cannot refer to itself).
resource "authentik_group" "role" {
for_each = var.role_groups
name = each.key
is_superuser = each.value.is_superuser
parents = [for p in each.value.permissions : authentik_group.permission[p].id]
attributes = jsonencode(each.value.attributes)
}
# Emit an `ak_groups` claim containing the user's groups AND all inherited
# (ancestor) groups, so role -> permission nesting reaches apps. The default
# profile mapping only emits *direct* groups under `groups`
# (goauthentik/authentik#15579); we use a distinct claim key so there is no
# collision with that (Authentik dict-overrides same-key claims in an
# unpredictable order). Apps request the `ak_groups` scope and read the
# `ak_groups` claim. Walks each direct group up through `.parents`.
resource "authentik_property_mapping_provider_scope" "groups_hierarchical" {
name = "unkin: ak_groups (hierarchical)"
scope_name = "ak_groups"
expression = <<-EOT
groups = {}
pending = list(user.ak_groups.all())
while pending:
grp = pending.pop()
if grp.pk in groups:
continue
groups[grp.pk] = grp.name
pending += list(grp.parents.all())
return {"ak_groups": sorted(groups.values())}
EOT
}
# Resolve SAML flows by slug and the signing keypair by name, so configs use
# human-readable names instead of Authentik UUIDs (mirrors the oauth2 handling).
data "authentik_flow" "saml_authorization" {
for_each = var.providers_saml
slug = each.value.authorization_flow
}
data "authentik_flow" "saml_invalidation" {
for_each = var.providers_saml
slug = each.value.invalidation_flow
}
data "authentik_certificate_key_pair" "saml_signing" {
for_each = { for k, v in var.providers_saml : k => v if v.signing_kp != null }
name = each.value.signing_kp
}
resource "authentik_provider_saml" "this" {
for_each = var.providers_saml
name = each.value.name
authorization_flow = data.authentik_flow.saml_authorization[each.key].id
invalidation_flow = data.authentik_flow.saml_invalidation[each.key].id
acs_url = each.value.acs_url
sp_binding = each.value.sp_binding
audience = each.value.audience
name_id_mapping = each.value.name_id_mapping
signing_kp = each.value.signing_kp != null ? data.authentik_certificate_key_pair.saml_signing[each.key].id : null
}
# Resolve oauth2 flows by slug and scope mappings by managed identifier, and
# read client secrets from Vault so nothing sensitive is committed.
data "authentik_flow" "oauth2_authorization" {
for_each = var.providers_oauth2
slug = each.value.authorization_flow
}
data "authentik_flow" "oauth2_invalidation" {
for_each = var.providers_oauth2
slug = each.value.invalidation_flow
}
data "authentik_property_mapping_provider_scope" "oauth2" {
for_each = { for k, v in var.providers_oauth2 : k => v if length(v.scope_mappings) > 0 }
managed_list = each.value.scope_mappings
}
data "vault_kv_secret_v2" "oauth2" {
for_each = { for k, v in var.providers_oauth2 : k => v if v.client_secret_vault != null }
mount = each.value.client_secret_vault.mount
name = each.value.client_secret_vault.path
}
resource "authentik_provider_oauth2" "this" {
for_each = var.providers_oauth2
name = each.value.name
authorization_flow = data.authentik_flow.oauth2_authorization[each.key].id
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
property_mappings = concat(
try(data.authentik_property_mapping_provider_scope.oauth2[each.key].ids, []),
[authentik_property_mapping_provider_scope.groups_hierarchical.id],
)
signing_key = each.value.signing_key
access_token_validity = each.value.access_token_validity
allowed_redirect_uris = each.value.redirect_uris
}
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
base_dn = each.value.base_dn
certificate = each.value.certificate
tls_server_name = each.value.tls_server_name
uid_start_number = each.value.uid_start_number
gid_start_number = each.value.gid_start_number
search_mode = each.value.search_mode
bind_mode = each.value.bind_mode
mfa_support = each.value.mfa_support
}
resource "authentik_application" "saml" {
for_each = var.providers_saml
name = each.value.name
slug = each.key
protocol_provider = authentik_provider_saml.this[each.key].id
}
resource "authentik_application" "oauth2" {
for_each = var.providers_oauth2
name = each.value.name
slug = each.key
protocol_provider = authentik_provider_oauth2.this[each.key].id
}
resource "authentik_application" "ldap" {
for_each = var.providers_ldap
name = each.value.name
slug = each.key
protocol_provider = authentik_provider_ldap.this[each.key].id
}
resource "authentik_outpost" "ldap" {
for_each = var.providers_ldap
name = "${each.key}-outpost"
type = "ldap"
protocol_providers = [authentik_provider_ldap.this[each.key].id]
}
# Gate application access: bind each permission group that names an `application`
# to that app. Authentik ORs bindings, and membership propagates from child
# groups, so a member of any role that nests the permission is also covered.
# With any binding present, only these groups (and their children) can authorize.
resource "authentik_policy_binding" "app_access" {
for_each = { for k, v in var.permission_groups : k => v if v.application != null }
target = authentik_application.oauth2[each.value.application].uuid
group = authentik_group.permission[each.key].id
order = 0
}