140 lines
5.4 KiB
Terraform
140 lines
5.4 KiB
Terraform
# 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
|
||
|
|
}
|