← All posts

RBAC that gets approved.

Any tool that wants to see your cluster has to ask for credentials, and your security team is right to be suspicious. After enough of these reviews you learn there are four questions they actually ask — and one of them has an uncomfortable answer.

3 August 2026· 8 min read· Runtimez
kubernetes security rbac sre

Every agent that wants to look at your cluster has to ask for credentials. Most security reviews of that request go badly, and usually for the same reason: the manifest asks for more than the vendor can justify, and nobody on either side can tell which permissions are load-bearing.

Having been on both sides of this review, there are four questions that decide it. Three have clean answers. One doesn't, and pretending otherwise is how you lose the room.

1. What verbs, exactly

The answer that passes is get, list, watch. Nothing else, anywhere in the ClusterRole. No create, no patch, no update, no delete, and no * — not on verbs, not on resources, not on apiGroups.

The read-only ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cluster-agent-read
rules:
  - apiGroups: [""]
    resources:
      - pods
      - nodes
      - services
      - namespaces
      - configmaps
      - serviceaccounts
      - persistentvolumeclaims
    verbs: ["get", "list", "watch"]

  - apiGroups: ["apps"]
    resources: ["deployments", "statefulsets", "daemonsets", "replicasets"]
    verbs: ["get", "list", "watch"]

  - apiGroups: ["batch"]
    resources: ["jobs", "cronjobs"]
    verbs: ["get", "list", "watch"]

  - apiGroups: ["networking.k8s.io"]
    resources: ["ingresses", "networkpolicies"]
    verbs: ["get", "list", "watch"]

  - apiGroups: ["policy"]
    resources: ["poddisruptionbudgets"]
    verbs: ["get", "list", "watch"]

  - apiGroups: ["rbac.authorization.k8s.io"]
    resources: ["roles", "rolebindings", "clusterroles", "clusterrolebindings"]
    verbs: ["get", "list", "watch"]

  - apiGroups: ["apiextensions.k8s.io"]
    resources: ["customresourcedefinitions"]
    verbs: ["get", "list", "watch"]

Two notes a reviewer will raise. Reading RBAC objects looks alarming and is the point — you can't report on an over-privileged ServiceAccount without reading the bindings, and the objects themselves contain no secrets. Reading CRDs gets you definitions, not custom resources; if the agent needs the actual CRs, those are separate rules and each one should be named explicitly rather than swept up in a wildcard.

2. Do we have to hand over a kubeconfig

No, and any tool that asks for one should be turned down.

An agent that runs inside the cluster authenticates with a projected ServiceAccount token that the kubelet rotates. Nothing long-lived is minted, exported, or pasted into a vendor's dashboard. There's no credential to leak from their side, because they never held one.

The connection is outbound-only, which is the second half of the argument. No inbound rule, no ingress path, no exposed port. If your egress is locked down, the allowlist is one hostname, and that's a control you keep.

Compare that to a kubeconfig: a long-lived credential, on someone else's infrastructure, that you cannot rotate without their cooperation and cannot revoke without noticing you need to.

3. What about secrets

This is the question with the bad answer, and it's worth giving straight.

Inventory work wants secret names — which workloads mount what, which pull secrets exist, which ServiceAccounts have tokens. It does not want values. But RBAC cannot express that distinction. There is no verb for "list the names but not the contents." Grant list on secrets and the API server returns full objects, data field included.

There is a real mitigation, and it's less known than it should be. The API server can return metadata-only representations if you ask for them with an Accept header:

Verify it yourself
kubectl proxy --port=8001 &

curl -s -H 'Accept: application/json;as=PartialObjectMetadataList;g=meta.k8s.io;v=v1' \
  http://127.0.0.1:8001/api/v1/namespaces/default/secrets \
  | jq '.items[0] | keys'
Result
[
  "apiVersion",
  "kind",
  "metadata"
]

No data key. The values never cross the network, never enter the agent's memory, and never appear in a heap dump or a crash log.

Be honest about what this is. PartialObjectMetadata is a client-side discipline, not an authorization boundary. The token is still permitted to make the full request. A different build of the agent, or a compromised one, could simply drop the header.

So there are two defensible positions, and the customer picks:

A vendor that can't operate without the first option, and won't tell you why, has answered a different question than the one you asked.

4. What about image scanning

Scanning container images is where read-only tools quietly stop being read-only, because something has to pull and unpack the image. Two ways to do that, and only one survives review.

The bad one: ship image references, or the images themselves, to the vendor. Now your registry credentials or your proprietary layers are outside your perimeter, and you're having a much longer conversation.

The good one: scan in-cluster with ephemeral Jobs. The image never moves, the pull uses the cluster's existing pull secrets, and only the findings leave. That does require write permission — but it's create and delete on jobs, in one namespace, via a namespaced Role. Not in the ClusterRole.

Namespaced scan permissions — scoped to the agent's own namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: cluster-agent-scan
  namespace: cluster-agent
rules:
  - apiGroups: ["batch"]
    resources: ["jobs"]
    verbs: ["create", "get", "list", "watch", "delete"]

The blast radius of that grant is one namespace that contains nothing but the agent. It's a materially different ask from cluster-wide write, and framing it that way is usually the difference between a yes and a three-week thread.

Verify it rather than believing it

Don't review the YAML the vendor sent. Review what the token can actually do, after install:

The checks worth running
SA=system:serviceaccount:cluster-agent:cluster-agent

# Everything it can do, cluster-wide
kubectl auth can-i --list --as="$SA"

# The things that must come back "no"
kubectl auth can-i delete pods            --as="$SA" -A
kubectl auth can-i create clusterrolebindings --as="$SA"
kubectl auth can-i patch deployments      --as="$SA" -A
kubectl auth can-i '*' '*'                --as="$SA" -A

# And confirm the scan permission really is namespaced
kubectl auth can-i create jobs --as="$SA" -n kube-system   # expect: no
kubectl auth can-i create jobs --as="$SA" -n cluster-agent # expect: yes

kubectl auth can-i --list resolves every binding that applies, including ones the vendor's chart didn't create and inherited grants nobody remembered. Run it after install, keep the output, and re-run it after upgrades — chart changes are where scope quietly grows.

Write down what leaves

The last thing that unblocks a review isn't a permission, it's a sentence. Security teams will accept a lot if they know precisely what egresses. An answer that works:

Workload, node, and image metadata; object names, labels, and annotations; resource specs and status; scan findings. Not secret values, not pull secrets, not image layers, not application data, not logs.

If a vendor can't produce that list in one paragraph, they haven't thought about it, which is itself the finding.

Where this breaks down

Disclosure: we build Runtimez, and this is the shape of our own agent — read-only by default, ServiceAccount rather than kubeconfig, ephemeral in-cluster scan Jobs, metadata-only egress. We're describing it publicly because the review above is one you should run against any vendor asking for cluster access, us included.

See the intersection on your own clusters

One read-only Helm install · first report in under an hour · secrets and images never leave your cluster.

Questions? hello@runtimez.io