From 97be93a9ffbb72a68254caec97e9a1fa0c963cb9 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Mon, 6 Jul 2026 22:06:48 +1000 Subject: [PATCH] authentik: add Grafana OAuth2/OIDC provider + application Adds an OIDC provider + application so the in-cluster Grafana (grafana.k8s.syd1.au.unkin.net) can authenticate users against Authentik. Extends the oauth2 module so provider config stays declarative and secret-free: - Resolve authorization/invalidation flows by slug (data.authentik_flow) and scope mappings by managed identifier (data.authentik_property_mapping_provider_scope). - Read client_secret from Vault kv-v2 (data.vault_kv_secret_v2) instead of committing it; adds the hashicorp/vault provider (auth via VAULT_ADDR/ VAULT_TOKEN from the Makefile). - Support allowed_redirect_uris on the oauth2 provider. config/providers_oauth2/grafana.yaml wires client_id `grafana`, the openid/email/profile scopes, the login/generic_oauth redirect URI, and points client_secret at kv/kubernetes/namespace/grafana/default/oauth-credentials. --- config/providers_oauth2/grafana.yaml | 17 ++++++++++++++ environments/root.hcl | 8 +++++++ modules/authentik/.terraform.lock.hcl | 23 +++++++++++++++++++ modules/authentik/main.tf | 32 +++++++++++++++++++++++---- modules/authentik/variables.tf | 25 +++++++++++++++------ modules/authentik/versions.tf | 4 ++++ 6 files changed, 98 insertions(+), 11 deletions(-) create mode 100644 config/providers_oauth2/grafana.yaml diff --git a/config/providers_oauth2/grafana.yaml b/config/providers_oauth2/grafana.yaml new file mode 100644 index 0000000..307d2f8 --- /dev/null +++ b/config/providers_oauth2/grafana.yaml @@ -0,0 +1,17 @@ +# OAuth2/OIDC provider + application for the in-cluster Grafana +# (grafana.k8s.syd1.au.unkin.net). client_secret is read from Vault, not committed. +name: Grafana +authorization_flow: default-provider-authorization-implicit-consent +invalidation_flow: default-provider-invalidation-flow +client_type: confidential +client_id: grafana +client_secret_vault: + mount: kv + path: kubernetes/namespace/grafana/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://grafana.k8s.syd1.au.unkin.net/login/generic_oauth diff --git a/environments/root.hcl b/environments/root.hcl index 8574f82..87559a2 100644 --- a/environments/root.hcl +++ b/environments/root.hcl @@ -7,6 +7,10 @@ provider "authentik" { token = var.authentik_token } +# Reads client secrets seeded in Vault (kv-v2). Auth via VAULT_ADDR + VAULT_TOKEN +# from the environment (set by the Makefile vault_env helper). +provider "vault" {} + variable "authentik_token" { type = string sensitive = true @@ -26,6 +30,10 @@ terraform { source = "goauthentik/authentik" version = "2026.5.0" } + vault = { + source = "hashicorp/vault" + version = ">= 4.0.0" + } } } EOF diff --git a/modules/authentik/.terraform.lock.hcl b/modules/authentik/.terraform.lock.hcl index 28423cf..825a7c6 100644 --- a/modules/authentik/.terraform.lock.hcl +++ b/modules/authentik/.terraform.lock.hcl @@ -21,3 +21,26 @@ provider "registry.opentofu.org/goauthentik/authentik" { "zh:eb9c1fd3020cf61e9b7a6a38d2965f4b521495a9928705e963459a4af857f97d", ] } + +provider "registry.opentofu.org/hashicorp/vault" { + version = "5.10.1" + 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", + ] +} diff --git a/modules/authentik/main.tf b/modules/authentik/main.tf index b5190e0..f6fc278 100644 --- a/modules/authentik/main.tf +++ b/modules/authentik/main.tf @@ -20,18 +20,42 @@ resource "authentik_provider_saml" "this" { signing_kp = each.value.signing_kp } +# 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 = each.value.authorization_flow - invalidation_flow = each.value.invalidation_flow + 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 - property_mappings = each.value.property_mappings + client_secret = each.value.client_secret_vault != null ? data.vault_kv_secret_v2.oauth2[each.key].data["client_secret"] : null + property_mappings = try(data.authentik_property_mapping_provider_scope.oauth2[each.key].ids, []) 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" { diff --git a/modules/authentik/variables.tf b/modules/authentik/variables.tf index 9095a19..6b497a0 100644 --- a/modules/authentik/variables.tf +++ b/modules/authentik/variables.tf @@ -24,13 +24,24 @@ variable "providers_saml" { variable "providers_oauth2" { type = map(object({ - name = string - authorization_flow = string - invalidation_flow = string - client_type = optional(string, "confidential") - client_id = string - client_secret = optional(string, null) - property_mappings = optional(list(string), []) + name = string + authorization_flow = string # flow slug, resolved to id via data.authentik_flow + invalidation_flow = string # flow slug, resolved to id via data.authentik_flow + client_type = optional(string, "confidential") + 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. + client_secret_vault = optional(object({ + mount = string + path = string + }), null) + # Managed identifiers of scope property mappings (e.g. + # goauthentik.io/providers/oauth2/scope-openid). Resolved to ids. + scope_mappings = optional(list(string), []) + redirect_uris = optional(list(object({ + matching_mode = optional(string, "strict") + url = string + })), []) signing_key = optional(string, null) access_token_validity = optional(string, "minutes=10") })) diff --git a/modules/authentik/versions.tf b/modules/authentik/versions.tf index af40006..4f420de 100644 --- a/modules/authentik/versions.tf +++ b/modules/authentik/versions.tf @@ -5,5 +5,9 @@ terraform { source = "goauthentik/authentik" version = ">= 2026.5.0" } + vault = { + source = "hashicorp/vault" + version = ">= 4.0.0" + } } }