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.
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:
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:
- Grant it, and verify behaviorally. Keep
secretsin the ClusterRole, require the vendor to document metadata-only access, and confirm it in your audit log — every request records theAcceptnegotiation, so "did they ever pull values" is an answerable question rather than a matter of trust. - Don't grant it. Drop
secretsfrom the role entirely. You lose secret-name inventory and any finding that depends on it. Everything else still works.
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.
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 runningSA=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
- Read-only is not harmless. An agent that can list every ConfigMap in the cluster can read whatever people wrongly put in ConfigMaps, which in practice is credentials. Read access to a cluster is still a serious grant and should be reviewed like one.
listhurts big clusters. A full list of every pod on a large cluster is an expensive API server call, and an agent that polls it on a tight loop will show up in your latency graphs. Watch semantics withresourceVersion, and paginated lists vialimit/continue, are the difference between an agent you notice and one you don't.- CRDs are an ongoing negotiation. The rules above read CRD definitions. Every custom resource the agent actually needs is another explicit rule, added over time — which means scope creeps across releases unless someone re-runs the audit.
- Audit logging may not be on. The "verify behaviorally" option for secrets assumes you have API server audit logs at a policy level that records request metadata. On several managed control planes that's off by default or costs extra. Check before you rely on it.
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