Scope RBAC for an AI Agent's Kubernetes Access
Quick answer
Build an explicit, non-aggregated ClusterRole for an AI agent, issue it a short-lived audience-bound token, verify the ceiling in a scriptable check, and flip your audit policy so every read the agent makes is logged as the egress event it actually is.
- Step 1: Create the Identity
- Step 2: Write an Explicit ClusterRole — Not view
- Step 3: Bind It — Namespaced, Not Cluster-Wide
- Step 4: Issue a Short-Lived Token
- Step 5: Verify the Ceiling, Scriptably
intermediate · 45 min
Before you begin
- A cluster you can use (kind or minikube is fine) and kubectl configured with cluster-admin
- Basic familiarity with Kubernetes RBAC (Roles, ClusterRoles, bindings)
- Comfort editing YAML
"Just give it read-only access" undersells what's actually different about an AI agent's credential. A human with view looks at a Secret and closes the terminal; an agent forwards whatever it reads to a model provider, where it's retained, possibly echoed back, and very likely captured in your tracing. The RBAC mechanics you already know still apply — this tutorial is about which grants are safe to hand an agent and how to prove the ceiling holds, not a general RBAC primer. If you need that first, start with Kubernetes RBAC in Practice.
What You'll Build
- A namespace and
ServiceAccountdedicated to one agent - An explicit, non-aggregated
ClusterRolethat excludes Secrets and ConfigMaps by default - A short-lived, audience-scoped token issued the way you'd issue one in CI, not
kubectl configcopy-paste - A scriptable
kubectl auth can-icheck that fails the build if the ceiling ever widens - An inverted audit policy that logs the agent's reads at full detail
Step 1: Create the Identity
kubectl create namespace agents
kubectl create serviceaccount k8s-agent -n agentsOne ServiceAccount per agent, not a shared one. If two agents share an identity, you can't tell from the audit log which one read what.
Step 2: Write an Explicit ClusterRole — Not view
view looks like the obvious grant and is the one to avoid here. view is aggregated: its rules are assembled at runtime from every ClusterRole labelled rbac.authorization.k8s.io/aggregate-to-view: "true". Any operator installed later can silently widen it — for every subject bound to view, with nothing in your Git history. Check what's currently feeding it in your own cluster:
kubectl get clusterroles -l rbac.authorization.k8s.io/aggregate-to-view=true -o nameWrite the role yourself instead, scoped to exactly what the agent's tools need:
1# agent-role.yaml
2apiVersion: rbac.authorization.k8s.io/v1
3kind: ClusterRole
4metadata:
5 name: agent-readonly
6rules:
7 - apiGroups: [""]
8 resources: ["pods", "events", "services", "nodes", "namespaces"]
9 verbs: ["get", "list"]
10 - apiGroups: [""]
11 resources: ["pods/log"]
12 verbs: ["get"]
13 - apiGroups: ["apps"]
14 resources: ["deployments", "replicasets", "statefulsets", "daemonsets"]
15 verbs: ["get", "list"]
16 - apiGroups: ["batch"]
17 resources: ["jobs", "cronjobs"]
18 verbs: ["get", "list"]Two resources are deliberately missing: Secrets and ConfigMaps. Granting get on Secrets means copying every credential in scope into a context window that leaves the cluster. ConfigMaps are barely safer — in a real cluster they routinely hold connection strings and internal hostnames that should have been Secrets in the first place.
kubectl apply -f agent-role.yamlStep 3: Bind It — Namespaced, Not Cluster-Wide
1# agent-binding.yaml
2apiVersion: rbac.authorization.k8s.io/v1
3kind: RoleBinding
4metadata:
5 name: agent-readonly-binding
6 namespace: workloads
7subjects:
8 - kind: ServiceAccount
9 name: k8s-agent
10 namespace: agents
11roleRef:
12 kind: ClusterRole
13 name: agent-readonly
14 apiGroup: rbac.authorization.k8s.iokubectl create namespace workloads # or point this at a real target namespace
kubectl apply -f agent-binding.yamlA RoleBinding referencing a ClusterRole scopes the grant to one namespace, even though the role itself is cluster-scoped — that's what lets you reuse the same ClusterRole across many teams' namespaces without a ClusterRoleBinding. One catch: nodes and namespaces in the role above are themselves cluster-scoped resources, so this RoleBinding grants nothing for those two lines however the role is written. If the agent genuinely needs them, bind a separate ClusterRole with a ClusterRoleBinding deliberately — don't assume a namespaced binding covers cluster-scoped resources.
Step 4: Issue a Short-Lived Token
Don't hand out a long-lived kubeconfig. Mint a token that expires:
kubectl create token k8s-agent -n agents --duration=15mIf the agent presents this token to something other than the API server — a proxy, a sidecar — bind a custom audience so a leaked token is useless outside that one context:
kubectl create token k8s-agent -n agents --duration=15m --audience=kubernetes-agentLeave --audience off when the token goes straight to the API server; kubectl create token already requests the right default audience for that case, and an explicit mismatched audience gets the token refused.
Step 5: Verify the Ceiling, Scriptably
The system prompt telling the model "you have read-only access" is a request. The control is that the credential physically cannot do more — and that's the thing worth testing, not the prompt wording.
1#!/usr/bin/env bash
2# verify-agent-ceiling.sh
3set -euo pipefail
4
5SA=system:serviceaccount:agents:k8s-agent
6FAILED=0
7
8for check in "delete pods" "create pods --subresource=exec" "get secrets" \
9 "create serviceaccounts --subresource=token" "patch deployments"; do
10 if kubectl auth can-i $check --as=$SA -n workloads --quiet 2>/dev/null; then
11 echo "FAIL: agent can $check"
12 FAILED=1
13 fi
14done
15
16if [ "$FAILED" -eq 0 ]; then
17 echo "OK: agent ceiling holds"
18else
19 exit 1
20fichmod +x verify-agent-ceiling.sh
./verify-agent-ceiling.shRun this after every RBAC change, and — because of the aggregation problem in Step 2 — after every operator install too. An unrelated Helm chart can move the view ceiling without touching a single one of your own manifests; it can't move this one, because this role isn't aggregated. Wire the script into CI so a widened grant fails the pipeline instead of getting noticed in an incident review.
Step 6: Invert the Audit Policy
Standard audit policy treats reads as cheap and logs them at Metadata or not at all. For an agent, the read is the event — it's the moment data left the cluster toward a model provider.
1# audit-policy.yaml
2apiVersion: audit.k8s.io/v1
3kind: Policy
4rules:
5 # Expected agent behaviour — log it fully. This is the egress record.
6 - level: RequestResponse
7 users: ["system:serviceaccount:agents:k8s-agent"]
8 verbs: ["get", "list"]
9 resources:
10 - group: ""
11 resources: ["pods", "pods/log", "events"]
12
13 # Anything else from this identity is, by definition, unexpected.
14 - level: RequestResponse
15 users: ["system:serviceaccount:agents:k8s-agent"]
16
17 # Catch-all. Without this, every identity that isn't the agent stops
18 # being audited at all — see the warning below.
19 - level: MetadataThese are two rules to add to your policy, not a policy to drop in whole. Audit rules are first-match-wins, and a request matching no rule is not logged. Both agent rules are scoped to users: [...], so a file containing only them audits the agent and nothing else — replace a running cluster's policy with that and you silently stop recording every human, controller, and other ServiceAccount on the cluster. Merge the two agent rules into the top of your existing policy, above its own catch-all, or keep the trailing - level: Metadata above so the rest of the cluster stays covered at its previous level.
Wire this into your API server's --audit-policy-file (managed clusters expose this differently — EKS via control plane logging config, GKE via Cloud Audit Logs). The second rule is the one that earns its place: the agent should only ever perform the reads in rule one, so anything else it attempts — including a 403 from a prompt-injection attempt — becomes a finding you can alert on, not a line you'd have to notice by hand.
Step 7: Confirm It End-to-End
Simulate what an agent's tool call would do:
TOKEN=$(kubectl create token k8s-agent -n agents --duration=15m)
curl -sS --cacert /path/to/ca.crt \
-H "Authorization: Bearer $TOKEN" \
"https://<api-server>/api/v1/namespaces/workloads/pods" | jq '.items[].metadata.name'Then check the audit log (or your cluster's equivalent) and confirm that request shows up at RequestResponse level with the ServiceAccount identity attached. If it doesn't, the audit policy isn't wired to the API server the way you think it is — fix that before trusting the log during an incident.
Where This Leaves You
You have an identity that can't do more than five specific checks allow, a token that expires in minutes, a script that fails loudly if the ceiling ever widens, and a log that answers "what did the agent see?" without guessing.
What this setup does not do: stop data egress you explicitly authorized (if pod logs contain PII, a perfectly-scoped read-only agent still reads that PII — fix the logging, not the RoleBinding), or make the agent's output correct. Permissions are a containment story, not a correctness story.
Next steps:
- Give the agent tools to use this identity through — Build a Kubernetes MCP Server in Python deploys a server behind exactly this kind of
ServiceAccount. - When someone asks "can it just restart the pod?" — that's a design change, not a permissions tweak. Human Approval Workflows for AI Infrastructure Agents covers the approval-gated path.
- Read the full threat-model reasoning behind this role — Give an AI Agent Read-Only Access to Kubernetes.
We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.
Struggling with this in production?
We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.