From 0d1181abb146cf658b55c46288f89f6d0853acdb Mon Sep 17 00:00:00 2001 From: atlas-bot Date: Wed, 30 Sep 2026 23:14:52 +0000 Subject: [PATCH] paperclip: per-agent kubectl ServiceAccounts + apiserver egress CNP (REN-414) Five ServiceAccounts agent-{atlas,iris,hestia,argus,hermes} in paperclip with empty service-account-token Secrets. The four readers get the same rights they hold today via the shared pod SA: `view` cluster-wide and the existing monitoring/paperclip-prometheus-read Role. Hermes and the shared SA get `get` on the five named token Secrets only. CiliumNetworkPolicy paperclip-renner/paperclip-agents-kube-apiserver lets the five agents' pods reach the kube-apiserver entity on 6443. Spec: REN-412. Nothing restarts. Co-Authored-By: Claude Opus 5.5 --- .../apps/paperclip/agent-kubectl-rbac.yaml | 214 ++++++++++++++++++ kubernetes/apps/paperclip/kustomization.yaml | 1 + .../network-policies/paperclip-renner-ns.yaml | 37 +++ 3 files changed, 252 insertions(+) create mode 100644 kubernetes/apps/paperclip/agent-kubectl-rbac.yaml diff --git a/kubernetes/apps/paperclip/agent-kubectl-rbac.yaml b/kubernetes/apps/paperclip/agent-kubectl-rbac.yaml new file mode 100644 index 00000000..fb19cd82 --- /dev/null +++ b/kubernetes/apps/paperclip/agent-kubectl-rbac.yaml @@ -0,0 +1,214 @@ +--- +# Per-agent kubectl identities for agent pods (REN-412, parent REN-284). +# +# Each agent gets its own ServiceAccount instead of the shared pod token of +# `paperclip-prometheus-read`. Atlas, Iris, Hestia and Argus get exactly what +# they hold today through that shared account: built-in `view` cluster-wide +# (no Secrets, no write verb) plus the `monitoring/paperclip-prometheus-read` +# Role (see prometheus-read-rbac.yaml). Hermes gets only `get` on the five +# token Secrets below, to issue and rotate the kubeconfigs. +# +# Token Secrets carry no data in git; kube-controller-manager fills +# `data.token`. Flux applies server-side and does not own `data`, so it does +# not wipe the token. Revoke one agent: delete its Secret (the token dies at +# once; Flux recreates it with a fresh token on the next reconcile). +# +# The shared account may read the tokens too (Hermes still runs in the shared +# pod). Each token is worth at most what the shared account already holds, so +# that is no escalation. Drop that subject once Hermes runs in its own pod. +# +# PRODUCTION +# +# ServiceAccounts, bindings and empty token Secrets only. No workload changes, +# nothing restarts. Revert: delete this file and its line in kustomization.yaml. +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: agent-atlas + namespace: paperclip + labels: + app.kubernetes.io/name: paperclip +automountServiceAccountToken: false +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: agent-iris + namespace: paperclip + labels: + app.kubernetes.io/name: paperclip +automountServiceAccountToken: false +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: agent-hestia + namespace: paperclip + labels: + app.kubernetes.io/name: paperclip +automountServiceAccountToken: false +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: agent-argus + namespace: paperclip + labels: + app.kubernetes.io/name: paperclip +automountServiceAccountToken: false +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: agent-hermes + namespace: paperclip + labels: + app.kubernetes.io/name: paperclip +automountServiceAccountToken: false +--- +apiVersion: v1 +kind: Secret +type: kubernetes.io/service-account-token +metadata: + name: agent-atlas-token + namespace: paperclip + labels: + app.kubernetes.io/name: paperclip + annotations: + kubernetes.io/service-account.name: agent-atlas +--- +apiVersion: v1 +kind: Secret +type: kubernetes.io/service-account-token +metadata: + name: agent-iris-token + namespace: paperclip + labels: + app.kubernetes.io/name: paperclip + annotations: + kubernetes.io/service-account.name: agent-iris +--- +apiVersion: v1 +kind: Secret +type: kubernetes.io/service-account-token +metadata: + name: agent-hestia-token + namespace: paperclip + labels: + app.kubernetes.io/name: paperclip + annotations: + kubernetes.io/service-account.name: agent-hestia +--- +apiVersion: v1 +kind: Secret +type: kubernetes.io/service-account-token +metadata: + name: agent-argus-token + namespace: paperclip + labels: + app.kubernetes.io/name: paperclip + annotations: + kubernetes.io/service-account.name: agent-argus +--- +apiVersion: v1 +kind: Secret +type: kubernetes.io/service-account-token +metadata: + name: agent-hermes-token + namespace: paperclip + labels: + app.kubernetes.io/name: paperclip + annotations: + kubernetes.io/service-account.name: agent-hermes +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: paperclip-agents-view + labels: + app.kubernetes.io/name: paperclip +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: view +subjects: + - kind: ServiceAccount + name: agent-atlas + namespace: paperclip + - kind: ServiceAccount + name: agent-iris + namespace: paperclip + - kind: ServiceAccount + name: agent-hestia + namespace: paperclip + - kind: ServiceAccount + name: agent-argus + namespace: paperclip +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: paperclip-agents-prometheus-read + namespace: monitoring + labels: + app.kubernetes.io/name: paperclip +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + # Existing Role in prometheus-read-rbac.yaml, unchanged. + name: paperclip-prometheus-read +# Subjects live in `paperclip`, the binding in `monitoring`: `namespace:` is +# load-bearing. +subjects: + - kind: ServiceAccount + name: agent-atlas + namespace: paperclip + - kind: ServiceAccount + name: agent-iris + namespace: paperclip + - kind: ServiceAccount + name: agent-hestia + namespace: paperclip + - kind: ServiceAccount + name: agent-argus + namespace: paperclip +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: agent-token-read + namespace: paperclip + labels: + app.kubernetes.io/name: paperclip +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get"] + resourceNames: + - agent-atlas-token + - agent-iris-token + - agent-hestia-token + - agent-argus-token + - agent-hermes-token +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: agent-token-read + namespace: paperclip + labels: + app.kubernetes.io/name: paperclip +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: agent-token-read +subjects: + # Hermes still runs in the shared pod; drop this subject once Hermes runs in + # its own pod. + - kind: ServiceAccount + name: paperclip-prometheus-read + namespace: paperclip + - kind: ServiceAccount + name: agent-hermes + namespace: paperclip diff --git a/kubernetes/apps/paperclip/kustomization.yaml b/kubernetes/apps/paperclip/kustomization.yaml index 7f7fdaeb..d4e5e283 100644 --- a/kubernetes/apps/paperclip/kustomization.yaml +++ b/kubernetes/apps/paperclip/kustomization.yaml @@ -10,6 +10,7 @@ resources: - prometheus-read-rbac.yaml - cluster-view-rbac.yaml - sandbox-rbac.yaml + - agent-kubectl-rbac.yaml - paperclip.yaml - gate-service.yaml - ingress.yaml diff --git a/kubernetes/infrastructure/network-policies/paperclip-renner-ns.yaml b/kubernetes/infrastructure/network-policies/paperclip-renner-ns.yaml index 2b54be34..24756fad 100644 --- a/kubernetes/infrastructure/network-policies/paperclip-renner-ns.yaml +++ b/kubernetes/infrastructure/network-policies/paperclip-renner-ns.yaml @@ -31,3 +31,40 @@ spec: - ports: - port: "8443" protocol: TCP +--- +# Agent pods reach the Kubernetes API server with their own kubeconfig +# (REN-412, parent REN-284; RBAC in apps/paperclip/agent-kubectl-rbac.yaml). +# +# A `toCIDRSet` rule cannot open this path: `policy-cidr-match-mode` is unset +# in cilium-config, so CIDR rules never match node IPs, and the API server +# (10.2.46.11-13:6443, Service 10.96.0.1:443 translated by KPR) runs on the +# nodes. The `kube-apiserver` entity matches it. Port is the post-DNAT 6443. +# +# Selects by agent id, so only the five agents with a kubectl identity get +# the path; other agent pods stay closed. Not labelled +# `paperclip.io/managed-by`, so the plugin does not own or delete it. +# +# Revert: delete this document. +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: paperclip-agents-kube-apiserver + namespace: paperclip-renner +spec: + endpointSelector: + matchExpressions: + - key: paperclip.io/agent-id + operator: In + values: + - 27d813a4-ae75-4913-b25b-3516a893d614 # Atlas + - 6eb4d648-83b4-40cb-b16e-d05d3ef372cb # Iris + - 32b3c62a-a5dc-4499-b970-25f5c7f60e8c # Hestia + - b023e112-1466-46f1-9fc1-e87e5cd3f005 # Argus + - b1be203c-d260-4922-8b48-ab9e87e73fb0 # Hermes + egress: + - toEntities: + - kube-apiserver + toPorts: + - ports: + - port: "6443" + protocol: TCP -- 2.54.0