refactor(iam): retire terraform/zitadel, provision through the API
The Terraform root module duplicated a writer that the provisioning script could be, and its state carried generated client secrets. Declare the objects in the script instead: project, 19 roles, 12 OIDC apps, applied through the management API with the iam-admin PAT. State is a 0600 JSON file under ~/.config/zitadel. Also add zitadel to the Cloudflare tunnel hostname list, with the import command and the note that the tunnel's Public Hostname entry is a separate dashboard step: DNS alone yields a 404 from the tunnel.
This commit is contained in:
@@ -1,139 +0,0 @@
|
||||
# Machine users and their project grants (plan §1.4, §5.3).
|
||||
#
|
||||
# Authentik gave each agent an app password or a client_credentials grant built
|
||||
# on a user password. ZITADEL has no user password on a machine user: it issues
|
||||
# a JWT profile key (`KEY_TYPE_JSON`) that the SDK signs its token request with.
|
||||
# That removes the password path entirely, which is the point — see §5.3.
|
||||
#
|
||||
# Per-agent claim values are copied from `SERVICE_ACCOUNTS` in
|
||||
# scripts/iam/provision-rbac.py. They live in user metadata as JSON strings,
|
||||
# because the groups-claim action (actions.tf) is the only thing that turns them
|
||||
# into token claims. Without that action a consumer sees roles but not
|
||||
# `minio_buckets`, `authorized_models`, `sqs_queues`, `memory_projects`,
|
||||
# `memory_visibility` or `paperless_doctypes`.
|
||||
#
|
||||
# Keys are generated per machine user and land in `terraform output -json
|
||||
# machine_keys` (sensitive). Feed each one to the owning workload as a mounted
|
||||
# file, never as an environment variable dumps-able through `ps`.
|
||||
|
||||
locals {
|
||||
machine_users = {
|
||||
"memory-agent" = {
|
||||
description = "Memory service agent"
|
||||
project_roles = ["poimen-admins"]
|
||||
authenticated_by = "poimen memory (Rust)"
|
||||
metadata = {
|
||||
roles = jsonencode(["llm:inference", "memory:read", "memory:write", "notification:write", "sessions:read", "sessions:write"])
|
||||
memory_projects = jsonencode(["*"])
|
||||
memory_visibility = "private"
|
||||
authorized_models = jsonencode(["reasoning", "ornith:35b", "qwen2.5:3b"])
|
||||
}
|
||||
}
|
||||
"paperless-ai-agent" = {
|
||||
description = "Paperless AI plugin (auto-tagging, entity extraction)"
|
||||
project_roles = ["poimen-devs"]
|
||||
authenticated_by = "paperless-ai"
|
||||
metadata = {
|
||||
roles = jsonencode(["llm:inference", "memory:write", "paperless:admin", "notification:write"])
|
||||
minio_buckets = jsonencode(["paperless"])
|
||||
paperless_doctypes = jsonencode(["*"])
|
||||
memory_projects = jsonencode(["*"])
|
||||
authorized_models = jsonencode(["reasoning", "qwen2.5:3b"])
|
||||
}
|
||||
}
|
||||
"portfolio-agent" = {
|
||||
description = "Portfolio service agent"
|
||||
project_roles = ["poimen-prod-readonly"]
|
||||
authenticated_by = "portfolio service"
|
||||
metadata = {
|
||||
roles = jsonencode(["llm:inference", "memory:read"])
|
||||
memory_projects = jsonencode(["homelab", "portfolio"])
|
||||
memory_visibility = "public"
|
||||
authorized_models = jsonencode(["ornith:35b"])
|
||||
minio_buckets = jsonencode(["backups"])
|
||||
}
|
||||
}
|
||||
"temporal-worker-agent" = {
|
||||
description = "Temporal workflow worker"
|
||||
project_roles = ["poimen-admins"]
|
||||
authenticated_by = "Temporal worker"
|
||||
metadata = {
|
||||
roles = jsonencode(["llm:inference", "workflow:execute", "memory:read", "memory:write", "queue:send", "notification:write"])
|
||||
memory_projects = jsonencode(["*"])
|
||||
sqs_queues = jsonencode(["*"])
|
||||
authorized_models = jsonencode(["reasoning", "ornith:35b", "qwen2.5:3b"])
|
||||
}
|
||||
}
|
||||
# The gateway is the one consumer that used client_credentials on a machine
|
||||
# token, so it needs both the OIDC app (clients.tf) and a machine user
|
||||
# (here) plus its API application (api_apps.tf). Its capability check reads
|
||||
# the `roles` claim, which arrives as metadata.
|
||||
"api-gw" = {
|
||||
description = "Go LLM gateway machine identity"
|
||||
project_roles = []
|
||||
authenticated_by = "api-gw"
|
||||
metadata = {
|
||||
roles = jsonencode(["llm:inference"])
|
||||
}
|
||||
}
|
||||
# pi-worker has no anchor in git (plan §1.1: "not in git") and no group in
|
||||
# the Authentik provisioning script. It gets a machine user and no project
|
||||
# role until the owner names the consumer configuration that holds its old
|
||||
# credentials. Creating a role grant blind would widen access, not preserve it.
|
||||
"pi-worker" = {
|
||||
description = "pi sandbox worker (no git anchor for its consumer config)"
|
||||
project_roles = []
|
||||
authenticated_by = "pi sandbox worker"
|
||||
metadata = {
|
||||
roles = jsonencode([])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
machine_metadata = merge([
|
||||
for user, spec in local.machine_users : {
|
||||
for key, value in spec.metadata : "${user}/${key}" => {
|
||||
user = user
|
||||
key = key
|
||||
value = value
|
||||
}
|
||||
}
|
||||
]...)
|
||||
}
|
||||
|
||||
resource "zitadel_machine_user" "agent" {
|
||||
for_each = local.machine_users
|
||||
|
||||
org_id = data.zitadel_org.default.id
|
||||
user_name = each.key
|
||||
name = each.key
|
||||
description = "${each.value.description} — consumer: ${each.value.authenticated_by}"
|
||||
with_secret = false
|
||||
}
|
||||
|
||||
resource "zitadel_machine_key" "agent" {
|
||||
for_each = local.machine_users
|
||||
|
||||
org_id = data.zitadel_org.default.id
|
||||
user_id = zitadel_machine_user.agent[each.key].id
|
||||
key_type = "KEY_TYPE_JSON"
|
||||
expiration_date = var.machine_key_expiry
|
||||
}
|
||||
|
||||
resource "zitadel_project_member" "agent" {
|
||||
for_each = { for user, spec in local.machine_users : user => spec if length(spec.project_roles) > 0 }
|
||||
|
||||
org_id = data.zitadel_org.default.id
|
||||
project_id = zitadel_project_v2.homelab.id
|
||||
user_id = zitadel_machine_user.agent[each.key].id
|
||||
roles = each.value.project_roles
|
||||
}
|
||||
|
||||
resource "zitadel_user_metadata" "agent_claim" {
|
||||
for_each = local.machine_metadata
|
||||
|
||||
org_id = data.zitadel_org.default.id
|
||||
user_id = zitadel_machine_user.agent[each.value.user].id
|
||||
key = each.value.key
|
||||
value = each.value.value
|
||||
}
|
||||
Reference in New Issue
Block a user