Manage the agent API service account and token in Terraform #30

Merged
benvin merged 1 commits from benvin/agent-api-token-iac into main 2026-08-29 23:04:05 +10:00
6 changed files with 124 additions and 1 deletions
+3 -1
View File
@@ -9,6 +9,7 @@ Terraform configuration for managing the Authentik identity provider at identity
- **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 +20,8 @@ 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
```
## Usage
+5
View File
@@ -37,5 +37,10 @@ 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/")
}
}
}
+19
View File
@@ -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
@@ -23,4 +23,5 @@ 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
}
+71
View File
@@ -242,3 +242,74 @@ resource "authentik_policy_binding" "app_access" {
group = authentik_group.permission[each.key].id
order = 0
}
# 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
}
}
]...)
service_account_tokens = merge([
for k, v in var.service_accounts : {
for identifier, t in v.tokens : identifier => merge(t, { service_account = k })
}
]...)
}
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.key
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 })
}
+25
View File
@@ -127,3 +127,28 @@ 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 = {}
}