Onboard OpenBao as an Authentik OIDC client
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline was successful

Adds config/providers_oauth2/vault.yaml so human logins to OpenBao go
through Authentik SSO (bao CLI and the web UI). Machine auth (approle,
kubernetes, CI) and break-glass are unchanged and stay on the OpenBao side.

Extends the oauth2 provider module so a config may generate its own client
secret instead of reading a pre-seeded one: client_secret_vault.generate
creates a random_password and writes {client_id, client_secret} to the given
kv-v2 path. Providers without the flag keep the existing read-only data source
behaviour. This is what lets the provider land with no manual Vault seed.

Gates the new application with akP-vault-admin and nests it in
akR-global-admin, matching how every other app in this repo is bound.
This commit is contained in:
2026-08-30 21:25:39 +10:00
parent 5ecd03cdd5
commit 230db5ad7e
7 changed files with 114 additions and 4 deletions
+43 -2
View File
@@ -136,12 +136,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 +198,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],