From 0e6da5cdd3315085faa9dda28c27a410023f0e3b Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sun, 2 Aug 2026 21:55:05 +1000 Subject: [PATCH] Add Vault-scoped agent kubernetes roles + agents AppRole (#109) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why Agentic workloads currently need cluster-admin/root kubeconfig contexts to do routine per-domain work. This adds domain-scoped, Vault-issued kubernetes credentials plus an `agents` AppRole so agents get least-privilege access instead of escalating. ## Changes - Add `kubernetes_secret_backend_role` configs `agent-dhcp`, `agent-dns`, `agent-certs`, `agent-storage` (au/syd1): - **agent-dhcp** (Role, ns `dhcp-system`): full verbs on `kea.unkin.net` CRDs; get/list/watch pods/services/configmaps/events + pods/log. - **agent-dns** (`service_account_name` mode): mints tokens for the static `agent-dns` SA (argocd-apps#332) whose per-namespace RoleBindings confine access to bind-system/bind-internal/bind-external/externaldns. `allowed_kubernetes_namespaces: [bind-system]` (the SA's namespace). - **agent-certs** (Role, ns `cert-manager`): full verbs on `cert-manager.io` + `acme.cert-manager.io` (closes the orders/challenges debugging gap); get/list/watch/delete secrets; get/list/watch pods + pods/log. Secret delete confined to `cert-manager`. - **agent-storage** (Role, ns `cephrgw-system`): full verbs on `ceph.unkin.net` CRDs (buckets/bucketaccesses/objectstoreusers); get/list/watch pods + pods/log. - Extend the `kubernetes_secret_backend_role` module with an optional `service_account_name`; when set, `generated_role_rules`/`kubernetes_role_type` are omitted (the SA's own bindings supply RBAC). - Add creds policies for each role, bound to the `kubernetes_au_syd1_cluster_operator` ldap group (human kubectl use) and the `agents` AppRole (programmatic use). - Add the `agents` AppRole (mirrors the certmanager approle schema): `bind_secret_id: false` (role_id-only login), `token_bound_cidrs: [10.10.12.200/32]` (agent workstation wg0 addr), deterministic role_id, 1h/4h TTLs. - Add `policies/kv/kubernetes/agents.yaml` granting the AppRole create/read/update/list on `kv/data/kubernetes/*` + read/list on `kv/metadata/kubernetes/*` (no delete). ## Ordering argocd-apps#332 (the `agent-dns` SA + ClusterRole + per-namespace RoleBindings) must sync **before** the `agent-dns` creds here are usable — Vault mints tokens for a service account that must already exist. https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT Reviewed-on: https://git.unkin.net/unkin/terraform-vault/pulls/109 Co-authored-by: Ben Vincent Co-committed-by: Ben Vincent --- config/auth_approle_role/approle/agents.yaml | 6 ++++ .../kubernetes/au/syd1/agent-certs.yaml | 4 +++ .../kubernetes/au/syd1/agent-dhcp.yaml | 4 +++ .../kubernetes/au/syd1/agent-dns.yaml | 4 +++ .../kubernetes/au/syd1/agent-storage.yaml | 4 +++ modules/vault_cluster/main.tf | 1 + .../kubernetes_secret_backend_role/main.tf | 9 +++-- .../variables.tf | 6 ++++ modules/vault_cluster/variables.tf | 1 + .../kubernetes/au/syd1/creds/agent-certs.yaml | 12 +++++++ .../kubernetes/au/syd1/creds/agent-dhcp.yaml | 12 +++++++ .../kubernetes/au/syd1/creds/agent-dns.yaml | 12 +++++++ .../au/syd1/creds/agent-storage.yaml | 12 +++++++ policies/kv/kubernetes/agents.yaml | 17 ++++++++++ .../kubernetes/au/syd1/roles/agent-certs.yaml | 33 +++++++++++++++++++ .../kubernetes/au/syd1/roles/agent-dhcp.yaml | 26 +++++++++++++++ .../au/syd1/roles/agent-storage.yaml | 23 +++++++++++++ 17 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 config/auth_approle_role/approle/agents.yaml create mode 100644 config/kubernetes_secret_backend_role/kubernetes/au/syd1/agent-certs.yaml create mode 100644 config/kubernetes_secret_backend_role/kubernetes/au/syd1/agent-dhcp.yaml create mode 100644 config/kubernetes_secret_backend_role/kubernetes/au/syd1/agent-dns.yaml create mode 100644 config/kubernetes_secret_backend_role/kubernetes/au/syd1/agent-storage.yaml create mode 100644 policies/kubernetes/au/syd1/creds/agent-certs.yaml create mode 100644 policies/kubernetes/au/syd1/creds/agent-dhcp.yaml create mode 100644 policies/kubernetes/au/syd1/creds/agent-dns.yaml create mode 100644 policies/kubernetes/au/syd1/creds/agent-storage.yaml create mode 100644 policies/kv/kubernetes/agents.yaml create mode 100644 resources/secret_backend/kubernetes/au/syd1/roles/agent-certs.yaml create mode 100644 resources/secret_backend/kubernetes/au/syd1/roles/agent-dhcp.yaml create mode 100644 resources/secret_backend/kubernetes/au/syd1/roles/agent-storage.yaml diff --git a/config/auth_approle_role/approle/agents.yaml b/config/auth_approle_role/approle/agents.yaml new file mode 100644 index 0000000..8a23894 --- /dev/null +++ b/config/auth_approle_role/approle/agents.yaml @@ -0,0 +1,6 @@ +token_ttl: 3600 +token_max_ttl: 14400 +bind_secret_id: false +token_bound_cidrs: + - "10.10.12.200/32" +use_deterministic_role_id: true diff --git a/config/kubernetes_secret_backend_role/kubernetes/au/syd1/agent-certs.yaml b/config/kubernetes_secret_backend_role/kubernetes/au/syd1/agent-certs.yaml new file mode 100644 index 0000000..10fd4ed --- /dev/null +++ b/config/kubernetes_secret_backend_role/kubernetes/au/syd1/agent-certs.yaml @@ -0,0 +1,4 @@ +allowed_kubernetes_namespaces: + - "cert-manager" +kubernetes_role_type: "Role" +extra_labels: {} diff --git a/config/kubernetes_secret_backend_role/kubernetes/au/syd1/agent-dhcp.yaml b/config/kubernetes_secret_backend_role/kubernetes/au/syd1/agent-dhcp.yaml new file mode 100644 index 0000000..c5ed8f5 --- /dev/null +++ b/config/kubernetes_secret_backend_role/kubernetes/au/syd1/agent-dhcp.yaml @@ -0,0 +1,4 @@ +allowed_kubernetes_namespaces: + - "dhcp-system" +kubernetes_role_type: "Role" +extra_labels: {} diff --git a/config/kubernetes_secret_backend_role/kubernetes/au/syd1/agent-dns.yaml b/config/kubernetes_secret_backend_role/kubernetes/au/syd1/agent-dns.yaml new file mode 100644 index 0000000..634dd96 --- /dev/null +++ b/config/kubernetes_secret_backend_role/kubernetes/au/syd1/agent-dns.yaml @@ -0,0 +1,4 @@ +allowed_kubernetes_namespaces: + - "bind-system" +service_account_name: "agent-dns" +extra_labels: {} diff --git a/config/kubernetes_secret_backend_role/kubernetes/au/syd1/agent-storage.yaml b/config/kubernetes_secret_backend_role/kubernetes/au/syd1/agent-storage.yaml new file mode 100644 index 0000000..b1f1301 --- /dev/null +++ b/config/kubernetes_secret_backend_role/kubernetes/au/syd1/agent-storage.yaml @@ -0,0 +1,4 @@ +allowed_kubernetes_namespaces: + - "cephrgw-system" +kubernetes_role_type: "Role" +extra_labels: {} diff --git a/modules/vault_cluster/main.tf b/modules/vault_cluster/main.tf index 3940844..e7851bb 100644 --- a/modules/vault_cluster/main.tf +++ b/modules/vault_cluster/main.tf @@ -299,6 +299,7 @@ module "kubernetes_secret_backend_role" { allowed_kubernetes_namespaces = each.value.allowed_kubernetes_namespaces kubernetes_role_type = each.value.kubernetes_role_type extra_labels = each.value.extra_labels + service_account_name = each.value.service_account_name depends_on = [module.kubernetes_secret_backend] } diff --git a/modules/vault_cluster/modules/kubernetes_secret_backend_role/main.tf b/modules/vault_cluster/modules/kubernetes_secret_backend_role/main.tf index 6833a9a..8e56de5 100644 --- a/modules/vault_cluster/modules/kubernetes_secret_backend_role/main.tf +++ b/modules/vault_cluster/modules/kubernetes_secret_backend_role/main.tf @@ -2,6 +2,10 @@ locals { # Auto-generate role rules path: resources/secret_backend/{backend_path}/roles/{role_name}.yaml role_rules_file = "resources/secret_backend/${var.backend}/roles/${var.name}.yaml" + # service_account_name mode mints tokens for a pre-existing SA, so the + # generated_role_rules / kubernetes_role_type binding fields must be unset. + use_service_account = var.service_account_name != null + # Auto-generate extra labels based on country/region and role name auto_labels = merge(var.extra_labels, { vault-region = "${var.country}-${var.region}" @@ -13,7 +17,8 @@ resource "vault_kubernetes_secret_backend_role" "role" { backend = var.backend name = var.name allowed_kubernetes_namespaces = var.allowed_kubernetes_namespaces - kubernetes_role_type = var.kubernetes_role_type - generated_role_rules = file("${path.module}/../../../../../../../../${local.role_rules_file}") + kubernetes_role_type = local.use_service_account ? null : var.kubernetes_role_type + generated_role_rules = local.use_service_account ? null : file("${path.module}/../../../../../../../../${local.role_rules_file}") + service_account_name = var.service_account_name extra_labels = local.auto_labels } diff --git a/modules/vault_cluster/modules/kubernetes_secret_backend_role/variables.tf b/modules/vault_cluster/modules/kubernetes_secret_backend_role/variables.tf index 43d1fbf..af2a6a3 100644 --- a/modules/vault_cluster/modules/kubernetes_secret_backend_role/variables.tf +++ b/modules/vault_cluster/modules/kubernetes_secret_backend_role/variables.tf @@ -34,4 +34,10 @@ variable "extra_labels" { description = "Additional labels to apply to generated Kubernetes objects" type = map(string) default = {} +} + +variable "service_account_name" { + description = "Pre-existing service account to mint tokens for. When set, RBAC comes from that SA's own bindings instead of generated_role_rules." + type = string + default = null } \ No newline at end of file diff --git a/modules/vault_cluster/variables.tf b/modules/vault_cluster/variables.tf index 2c59ddc..87787f5 100644 --- a/modules/vault_cluster/variables.tf +++ b/modules/vault_cluster/variables.tf @@ -285,6 +285,7 @@ variable "kubernetes_secret_backend_role" { allowed_kubernetes_namespaces = optional(list(string), ["*"]) kubernetes_role_type = optional(string, "Role") extra_labels = optional(map(string), {}) + service_account_name = optional(string) })) default = {} } diff --git a/policies/kubernetes/au/syd1/creds/agent-certs.yaml b/policies/kubernetes/au/syd1/creds/agent-certs.yaml new file mode 100644 index 0000000..fcc78b7 --- /dev/null +++ b/policies/kubernetes/au/syd1/creds/agent-certs.yaml @@ -0,0 +1,12 @@ +# Allow access to agent-certs Kubernetes credentials +--- +rules: + - path: "kubernetes/au/syd1/creds/agent-certs" + capabilities: + - update + +auth: + ldap: + - kubernetes_au_syd1_cluster_operator + approle: + - agents diff --git a/policies/kubernetes/au/syd1/creds/agent-dhcp.yaml b/policies/kubernetes/au/syd1/creds/agent-dhcp.yaml new file mode 100644 index 0000000..76e007d --- /dev/null +++ b/policies/kubernetes/au/syd1/creds/agent-dhcp.yaml @@ -0,0 +1,12 @@ +# Allow access to agent-dhcp Kubernetes credentials +--- +rules: + - path: "kubernetes/au/syd1/creds/agent-dhcp" + capabilities: + - update + +auth: + ldap: + - kubernetes_au_syd1_cluster_operator + approle: + - agents diff --git a/policies/kubernetes/au/syd1/creds/agent-dns.yaml b/policies/kubernetes/au/syd1/creds/agent-dns.yaml new file mode 100644 index 0000000..e61a423 --- /dev/null +++ b/policies/kubernetes/au/syd1/creds/agent-dns.yaml @@ -0,0 +1,12 @@ +# Allow access to agent-dns Kubernetes credentials +--- +rules: + - path: "kubernetes/au/syd1/creds/agent-dns" + capabilities: + - update + +auth: + ldap: + - kubernetes_au_syd1_cluster_operator + approle: + - agents diff --git a/policies/kubernetes/au/syd1/creds/agent-storage.yaml b/policies/kubernetes/au/syd1/creds/agent-storage.yaml new file mode 100644 index 0000000..0463013 --- /dev/null +++ b/policies/kubernetes/au/syd1/creds/agent-storage.yaml @@ -0,0 +1,12 @@ +# Allow access to agent-storage Kubernetes credentials +--- +rules: + - path: "kubernetes/au/syd1/creds/agent-storage" + capabilities: + - update + +auth: + ldap: + - kubernetes_au_syd1_cluster_operator + approle: + - agents diff --git a/policies/kv/kubernetes/agents.yaml b/policies/kv/kubernetes/agents.yaml new file mode 100644 index 0000000..ea89078 --- /dev/null +++ b/policies/kv/kubernetes/agents.yaml @@ -0,0 +1,17 @@ +# Allow the agents AppRole to manage the kubernetes KV subtree (no delete) +--- +rules: + - path: "kv/data/kubernetes/*" + capabilities: + - create + - read + - update + - list + - path: "kv/metadata/kubernetes/*" + capabilities: + - read + - list + +auth: + approle: + - agents diff --git a/resources/secret_backend/kubernetes/au/syd1/roles/agent-certs.yaml b/resources/secret_backend/kubernetes/au/syd1/roles/agent-certs.yaml new file mode 100644 index 0000000..995710d --- /dev/null +++ b/resources/secret_backend/kubernetes/au/syd1/roles/agent-certs.yaml @@ -0,0 +1,33 @@ +--- +rules: + - apiGroups: + - "cert-manager.io" + - "acme.cert-manager.io" + resources: + - "*" + verbs: + - "*" + - apiGroups: + - "" + resources: + - "secrets" + verbs: + - "get" + - "list" + - "watch" + - "delete" + - apiGroups: + - "" + resources: + - "pods" + verbs: + - "get" + - "list" + - "watch" + - apiGroups: + - "" + resources: + - "pods/log" + verbs: + - "get" + - "list" diff --git a/resources/secret_backend/kubernetes/au/syd1/roles/agent-dhcp.yaml b/resources/secret_backend/kubernetes/au/syd1/roles/agent-dhcp.yaml new file mode 100644 index 0000000..a425a34 --- /dev/null +++ b/resources/secret_backend/kubernetes/au/syd1/roles/agent-dhcp.yaml @@ -0,0 +1,26 @@ +--- +rules: + - apiGroups: + - "kea.unkin.net" + resources: + - "*" + verbs: + - "*" + - apiGroups: + - "" + resources: + - "pods" + - "services" + - "configmaps" + - "events" + verbs: + - "get" + - "list" + - "watch" + - apiGroups: + - "" + resources: + - "pods/log" + verbs: + - "get" + - "list" diff --git a/resources/secret_backend/kubernetes/au/syd1/roles/agent-storage.yaml b/resources/secret_backend/kubernetes/au/syd1/roles/agent-storage.yaml new file mode 100644 index 0000000..6c5b6d3 --- /dev/null +++ b/resources/secret_backend/kubernetes/au/syd1/roles/agent-storage.yaml @@ -0,0 +1,23 @@ +--- +rules: + - apiGroups: + - "ceph.unkin.net" + resources: + - "*" + verbs: + - "*" + - apiGroups: + - "" + resources: + - "pods" + verbs: + - "get" + - "list" + - "watch" + - apiGroups: + - "" + resources: + - "pods/log" + verbs: + - "get" + - "list"