Ask most teams "who has cluster-admin in your cluster" and you get a guess, not an answer. RBAC is additive, granted incrementally over years by different people solving different problems, and nobody ever goes back to remove access once the immediate need has passed. The result is a permission graph nobody has actually looked at end-to-end.
This is how to actually look at it — the specific commands to run against your own cluster, what the output means, and what to do about what you find.
Short answer
Run these three checks first; they surface the highest-severity findings fastest:
# 1. Every ClusterRoleBinding to cluster-admin, and who it's bound to
kubectl get clusterrolebindings -o json | \
jq -r '.items[] | select(.roleRef.name == "cluster-admin") | .metadata.name + ": " + (.subjects // [] | map(.kind + "/" + .name) | join(", "))'
# 2. Any Role/ClusterRole with wildcard verbs or resources
kubectl get roles,clusterroles -A -o json | \
jq -r '.items[] | select(.rules[]? | (.verbs[]? == "*") or (.resources[]? == "*")) | .metadata.namespace // "cluster" + "/" + .metadata.name'
# 3. Anything granted escalate, bind, or impersonate
kubectl get roles,clusterroles -A -o json | \
jq -r '.items[] | select(.rules[]?.verbs[]? | IN("escalate","bind","impersonate")) | (.metadata.namespace // "cluster") + "/" + .metadata.name'
Every result from those three needs a specific, current reason to exist. "It's been there since setup" is not a reason — it's the absence of one.
The RBAC model, as much as you need for auditing
Four object types, and the distinction that actually matters for auditing is namespace scope versus cluster scope:
- Role — permissions scoped to one namespace.
- ClusterRole — permissions that can apply cluster-wide, or be bound within a single namespace (a ClusterRole isn't inherently cluster-scoped in effect — only in definition).
- RoleBinding — grants a Role or ClusterRole to a subject (user, group, or ServiceAccount), scoped to the RoleBinding's own namespace.
- ClusterRoleBinding — grants a ClusterRole to a subject cluster-wide, across every namespace.
The trap: a ClusterRole with broad permissions isn't itself a finding — it only matters once you see what it's bound to and where. view, edit, and admin are built-in ClusterRoles Kubernetes ships by default specifically so they can be bound per-namespace via a RoleBinding. Seeing ClusterRole: admin in isolation isn't dangerous. Seeing it bound cluster-wide via a ClusterRoleBinding, when the intent was "admin of one namespace," is a real finding — and it's an easy one to introduce by accident, since kubectl create rolebinding and kubectl create clusterrolebinding are one word apart and both tab-complete the same way.
Finding cluster-admin bindings
cluster-admin is the built-in ClusterRole with * verbs on * resources in every API group — full control of the cluster, including the ability to read every Secret, modify RBAC itself, and delete anything. It exists for legitimate break-glass access and cluster bootstrapping. It should not be routine.
kubectl get clusterrolebindings -o json | \
jq -r '.items[] | select(.roleRef.name == "cluster-admin") | .metadata.name + ": " + (.subjects // [] | map(.kind + "/" + (.namespace // "") + "/" + .name) | join(", "))'
Illustrative output:
cluster-admin-binding: Group//system:masters
ci-deploy-binding: ServiceAccount/ci-cd/deploy-bot
The first line is Kubernetes' own default binding for the bootstrap admin group — expected, leave it. The second is the actual finding: a CI/CD service account with full cluster-admin, almost certainly because a pipeline needed some permission at some point and cluster-admin was the fastest way to make an error go away. Don't check RoleBindings for cluster-admin too — it's a common trap: a namespaced RoleBinding can still reference the cluster-scoped cluster-admin ClusterRole, granting full cluster-admin power scoped only by the binding's namespace membership (which, given cluster-admin's */* rule, doesn't actually constrain much):
kubectl get rolebindings -A -o json | \
jq -r '.items[] | select(.roleRef.name == "cluster-admin") | .metadata.namespace + "/" + .metadata.name'
Remediation is almost always the same shape: identify the minimum verbs and resources the subject actually uses (check its recent API server audit log entries if audit logging is enabled, or reason from what the CI job/controller/application actually does), write a scoped Role or ClusterRole for exactly that, and replace the binding. Test in a non-production namespace first — an overly narrow replacement role breaks the workload in a more confusing way than the original over-broad grant ever did.
Finding wildcard permissions
Wildcards (*) in verbs or resources are the second most common over-grant, usually introduced the same way cluster-admin bindings are: someone hit a permissions error, didn't want to figure out the exact missing verb, and wildcarded it to make the error go away.
kubectl get roles,clusterroles -A -o json | jq -r '
.items[] |
select(.rules[]? | (.verbs[]? == "*") or (.resources[]? == "*") or (.apiGroups[]? == "*")) |
(.metadata.namespace // "cluster-scoped") + "/" + .metadata.name
'
Not every wildcard is wrong — a controller that genuinely needs to manage every resource type in a custom API group may legitimately need resources: ["*"] scoped to just that apiGroup, which is meaningfully narrower than a wildcard across every group. The finding worth prioritizing is apiGroups: ["*"] combined with resources: ["*"] and verbs: ["*"] in the same rule — that's not "broad access to one thing," it's every action on every resource in every API group, functionally equivalent to cluster-admin without the name that would make someone question it in review.
Finding privilege-escalation verbs
Three verbs are privilege-escalation primitives in their own right, distinct from routine read/write access:
escalateonroles/clusterroles— lets a subject modify a Role to grant itself permissions it doesn't currently have.bindonroles/clusterroles— lets a subject create a RoleBinding to a role with more permissions than the subject itself holds, as long as the subject can "bind" that specific role.impersonateonusers/groups/serviceaccounts— lets a subject act as a fully different, potentially more privileged identity for the duration of a request.
kubectl get roles,clusterroles -A -o json | jq -r '
.items[] |
select(.rules[]?.verbs[]? | IN("escalate","bind","impersonate")) |
(.metadata.namespace // "cluster-scoped") + "/" + .metadata.name
'
These three matter more than their rarity suggests, because normal RBAC review (checking whether a subject's current permissions are appropriate) doesn't catch them — the danger isn't what the subject can do directly, it's what the subject can grant itself. A role with nothing but get on pods and escalate on clusterroles looks harmless in a permissions diff and isn't. Legitimate uses exist (a platform team's own RBAC-management tooling genuinely needs bind/escalate to function), but every instance needs a name attached to "why," not just "it's there."
Finding broad Secret access
Not privilege escalation in the RBAC-object sense, but the most consequential read permission in most clusters, since Secrets typically hold the credentials for everything else:
kubectl get roles,clusterroles -A -o json | jq -r '
.items[] |
select(.rules[]? | (.resources[]? == "secrets") and (.verbs[]? | IN("get","list","watch"))) |
(.metadata.namespace // "cluster-scoped") + "/" + .metadata.name
'
Cross-reference against bindings the same way as the cluster-admin check. A ClusterRole with get/list on secrets and no resourceNames restriction, bound broadly, means every subject in that binding can read every Secret the role's scope covers — not just the ones the application actually needs. Where possible, scope with resourceNames to the specific Secrets a workload legitimately needs, rather than namespace-wide access to the resource type.
The default ServiceAccount
Every namespace gets a default ServiceAccount automatically, and every pod that doesn't explicitly set serviceAccountName uses it — including pods where nobody deliberately thought about what identity they'd run as. If a well-meaning platform engineer ever binds a role to default in a shared namespace to unblock one workload, every other unrelated pod in that namespace inherits the same access silently.
kubectl get rolebindings,clusterrolebindings -A -o json | \
jq -r '.items[] | select(.subjects[]? | .kind == "ServiceAccount" and .name == "default") | (.metadata.namespace // "cluster") + "/" + .metadata.name'
Anything here is worth investigating — the fix is almost always creating a dedicated ServiceAccount for the workload that actually needs the access and binding the role to that instead, leaving default with no bindings at all.
Possibly-unused ServiceAccounts
Harder to detect definitively without audit logs, but a reasonable heuristic: ServiceAccounts that exist but aren't referenced by any current pod's serviceAccountName (accounting for the default-SA case above) are candidates for cleanup — leftover from a decommissioned workload, an abandoned experiment, or a service that moved to a different identity without anyone deleting the old one.
kubectl get serviceaccounts -A -o json | jq -r '.items[] | (.metadata.namespace + "/" + .metadata.name)' > /tmp/all-sa.txt
kubectl get pods -A -o json | jq -r '.items[] | (.metadata.namespace + "/" + (.spec.serviceAccountName // "default"))' | sort -u > /tmp/used-sa.txt
comm -23 <(sort /tmp/all-sa.txt) /tmp/used-sa.txt
An unused ServiceAccount with no bindings is just clutter. An unused ServiceAccount that's still bound to something is a stale credential with real access and nothing legitimately using it — a higher-value cleanup target, and worth checking first.
Common mistakes
- Auditing subjects instead of the permission graph. Checking "what can Alice do" one person at a time misses ServiceAccounts, which usually hold far broader and less-reviewed access than any human identity.
- Treating a ClusterRole's existence as the finding. The finding is the binding — where it's granted and to whom. A powerful ClusterRole that's never bound anywhere is inert.
- Fixing the binding without checking what breaks. RBAC over-grants often exist because someone hit a real error once. Replacing a broad grant with a narrow one without confirming the narrow one actually covers current usage just moves the incident from "too much access" to "pipeline broken at 2am."
- One-time audits. RBAC accretes with every new controller, Helm chart, and CI integration. Without a recheck cadence, the findings from this article's commands reappear within a year — worth running these checks (or an audit) on a schedule, not just once.
Conclusion
RBAC audits are one of the highest-value, lowest-effort security reviews available for a Kubernetes cluster — the commands above take minutes to run and surface findings that would otherwise only surface during an incident, when a compromised low-privilege pod turns out to sit behind a binding nobody remembered granting. Run the three commands from the short answer first; they're disproportionately likely to find something that needs fixing today, not eventually.
This is one piece of a broader security review — RBAC findings rarely exist in isolation from Pod Security Standards gaps or overly-broad NetworkPolicy defaults. If you'd rather have this run against your actual cluster with prioritized, written findings instead of working through it manually, the Kubernetes Cluster Audit covers RBAC as part of a fixed-price, $500 assessment — see the sample audit report for the format, or explore Kubernetes Security Consulting for ongoing hardening work.