Kubernetes cluster-admin sprawl: find escalation paths and remove access
For developers and platform engineers cleaning up Kubernetes privileged access, this runbook shows how to find who effectively has cluster-admin, including indirect escalation paths, and how to remove standing access safely. You’ll get concrete kubectl and jq commands, what output to look for, and remediation patterns that hold up in real clusters.
TL;DR — In most clusters, the real problem is not just subjects bound directly to
cluster-admin; it is indirect escalation via wildcard RBAC,bind/escalate, impersonation, secret access to privileged service accounts, or cloud/IAM-to-RBAC mappings. Start by listing directcluster-adminbindings, then enumerate who can create ClusterRoleBindings, useescalate/bind, impersonate users/groups, or mint tokens for privileged service accounts. Remove standing access by replacing broad bindings with time-bound group membership and narrowly scoped roles. Reading time: ~6 min
The scenario
It’s Tuesday afternoon and your security team drops a ticket with a short deadline: “prove who can get cluster-admin in prod, and remove standing access before the audit call.” You check RBAC and immediately find only two obvious cluster-admin bindings, which looks fine until a developer casually mentions they can “just bind themselves if needed.” Now you need the real answer: not who is directly admin, but who can become admin, through Kubernetes RBAC, service account token creation, or your cloud auth mapping.
Symptoms
- Audit asks for “all identities with effective cluster-admin or privilege-escalation paths,” and your current answer only includes direct bindings.
kubectl auth can-i --listfor a user or group shows dangerous verbs such as:
Resources Non-Resource URLs Resource Names Verbs
clusterrolebindings.rbac.authorization.k8s.io [] [] [create delete get list patch update watch]
clusterroles.rbac.authorization.k8s.io [] [] [bind escalate get list watch]
users [] [] [impersonate]
groups [] [] [impersonate]
serviceaccounts/token [] [] [create]
secrets [] [] [get list watch]
- A supposedly non-admin user can successfully run:
kubectl auth can-i create clusterrolebindings --all-namespaces
# yes
- Or they can impersonate a more privileged identity:
kubectl auth can-i '*' '*' --as=system:serviceaccount:kube-system:admin-sa
# yes
- You find broad RBAC rules like:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
- Cloud-authenticated users appear in Kubernetes groups that are bound to admin roles, but those groups are managed outside the cluster.
Likely causes
| Cause | How common | Quick check |
|---|---|---|
Direct cluster-admin ClusterRoleBinding or RoleBinding | Very common | `kubectl get clusterrolebindings,rolebindings -A -o json |
RBAC allows creating/updating bindings or uses bind/escalate | Very common | `kubectl get clusterroles,roles -A -o json |
Impersonation rights (impersonate on users/groups/serviceaccounts) | Common | `kubectl get clusterroles,roles -A -o json |
| Access to privileged service account tokens or secrets | Common | `kubectl get clusterroles,roles -A -o json |
| External IAM/group mapping grants admin in-cluster | Common | kubectl auth can-i --list |
| Aggregated ClusterRoles accidentally pull admin-like rules together | Less common | `kubectl get clusterroles -o json |
Step-by-step diagnosis
View as text
[Start] ---- 1. direct bindings ----> [cluster-admin bound?]
|
v
[2. bind/escalate rights?]
|
v
[3. impersonation rights?]
|
v
[4. SA token/secret access?]
|
v
[5. external IAM mapping?]
|
v
[6. aggregated roles review]- List direct
cluster-adminbindings.
kubectl get clusterrolebindings,rolebindings -A -o json | jq -r '
.items[]
| select(.roleRef.kind=="ClusterRole" and .roleRef.name=="cluster-admin")
| .kind + "\t" + (.metadata.namespace // "-") + "\t" + .metadata.name + "\t" + ((.subjects // []) | map(.kind + ":" + .name + (if .namespace then ":" + .namespace else "" end)) | join(","))'
If you see human users, broad groups like system:authenticated, or CI identities here, this is your problem. Jump to Fixes → Direct cluster-admin bindings.
- Find who can grant themselves more access.
kubectl get clusterroles,roles -A -o json | jq -r '
.items[]
| select(any(.rules[]?;
(((.resources // []) | index("clusterrolebindings")) and ((.verbs // []) | any(.=="create" or .=="update" or .=="patch")))
or (((.resources // []) | index("rolebindings")) and ((.verbs // []) | any(.=="create" or .=="update" or .=="patch")))
or ((.verbs // []) | any(.=="bind" or .=="escalate"))
))
| [.kind, (.metadata.namespace // "-"), .metadata.name] | @tsv'
Then resolve subjects bound to those roles:
kubectl get clusterrolebindings,rolebindings -A -o json | jq -r '
.items[] | [.kind, (.metadata.namespace // "-"), .metadata.name, .roleRef.kind, .roleRef.name, ((.subjects // []) | map(.kind + ":" + .name + (if .namespace then ":" + .namespace else "" end)) | join(","))] | @tsv'
If a developer group is bound to any of those roles, jump to Fixes → RBAC allows creating/updating bindings or uses bind/escalate.
- Check impersonation.
kubectl get clusterroles,roles -A -o json | jq -r '
.items[]
| select(any(.rules[]?; ((.verbs // []) | index("impersonate"))))
| [.kind, (.metadata.namespace // "-"), .metadata.name, (.rules | tostring)] | @tsv'
If a subject can impersonate system:masters, admin users, or privileged service accounts, that is effective admin. Jump to Fixes → Impersonation rights.
- Check service account token and secret access.
kubectl get clusterroles,roles -A -o json | jq -r '
.items[]
| select(any(.rules[]?;
((((.resources // []) | index("serviceaccounts/token")) and ((.verbs // []) | index("create")))
or (((.resources // []) | index("secrets")) and ((.verbs // []) | any(.=="get" or .=="list" or .=="watch"))))
))
| [.kind, (.metadata.namespace // "-"), .metadata.name] | @tsv'
Then inspect whether any privileged service accounts are bound to admin-like roles:
kubectl get clusterrolebindings,rolebindings -A -o json | jq -r '
.items[]
| select(any((.subjects // [])[]?; .kind=="ServiceAccount"))
| select(.roleRef.kind=="ClusterRole" and (.roleRef.name=="cluster-admin" or .roleRef.name|test("admin")))
| [.kind, (.metadata.namespace // "-"), .metadata.name, .roleRef.name, ((.subjects // []) | map(select(.kind=="ServiceAccount") | .namespace + "/" + .name) | join(","))] | @tsv'
If users can mint tokens for or read secrets from those service accounts, jump to Fixes → Access to privileged service account tokens or secrets.
- Validate external auth mapping. Run as the suspect identity if possible:
kubectl auth can-i --list
If output includes broad rights you cannot explain from in-cluster RBAC, your cloud/IAM auth mapping is likely granting a privileged group. Jump to Fixes → External IAM/group mapping grants admin.
- Review aggregated ClusterRoles.
kubectl get clusterroles -o yaml | sed -n '/^---/,$p' | grep -nE 'aggregationRule|rbac.authorization.k8s.io/aggregate-to-'
If custom roles aggregate into admin/edit/view unexpectedly, jump to Fixes → Aggregated ClusterRoles.
Fixes
Direct cluster-admin ClusterRoleBinding or RoleBinding
Delete or narrow the binding.
⚠️ Removing the wrong binding can lock out your team. Keep one break-glass admin path open in a separate terminal and test before deleting the next binding.
Inspect first:
kubectl describe clusterrolebinding <name>
kubectl describe rolebinding -n <namespace> <name>
Delete broad bindings:
kubectl delete clusterrolebinding <name>
kubectl delete rolebinding -n <namespace> <name>
Replace with a narrower role, for example read-only cluster access:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: platform-auditors-view
subjects:
- kind: Group
name: platform-auditors
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: view
apiGroup: rbac.authorization.k8s.io
Apply:
kubectl apply -f platform-auditors-view.yaml
Verify it worked:
kubectl auth can-i '*' '*' --as=<user-or-sa>
# no
RBAC allows creating/updating bindings or uses bind/escalate
Edit the offending role and remove dangerous verbs/resources.
kubectl get clusterrole <name> -o yaml > /tmp/<name>.yaml
Change rules from broad to explicit. Example bad rule:
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["clusterrolebindings", "rolebindings"]
verbs: ["create", "update", "patch"]
Replace with nothing, or with namespace-scoped read-only if truly needed:
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["rolebindings"]
verbs: ["get", "list", "watch"]
Apply:
kubectl apply -f /tmp/<name>.yaml
If the role exists only for one team, create a separate limited role and rebind subjects to it. Verify it worked:
kubectl auth can-i create clusterrolebindings --as=<user-or-sa>
# no
Impersonation rights
Remove impersonate unless there is a documented support workflow. Typical dangerous rule:
- apiGroups: [""]
resources: ["users", "groups", "serviceaccounts"]
verbs: ["impersonate"]
Patch by editing the role:
kubectl edit clusterrole <name>
Or replace with a tightly scoped rule limited to a single low-risk identity if you absolutely need it:
- apiGroups: [""]
resources: ["serviceaccounts"]
resourceNames: ["build-bot"]
verbs: ["impersonate"]
Verify it worked:
kubectl auth can-i impersonate users --as=<user-or-sa>
# no
Access to privileged service account tokens or secrets
First, identify and remove admin bindings from service accounts that do not need them. Then remove token-minting and secret-read access from humans and CI roles.
List service accounts with admin-like bindings:
kubectl get clusterrolebindings,rolebindings -A -o json | jq -r '
.items[] | select(any((.subjects // [])[]?; .kind=="ServiceAccount"))
| select(.roleRef.kind=="ClusterRole" and (.roleRef.name=="cluster-admin" or .roleRef.name|test("admin")))
| .metadata.name'
Delete or replace those bindings, then remove token creation rights from user roles:
- apiGroups: [""]
resources: ["serviceaccounts/token", "secrets"]
verbs: ["get", "list", "watch", "create"]
Delete that rule from the role and apply the edited manifest. On newer clusters, prefer short-lived projected service account tokens and disable any legacy secret-based workflows in your apps/manifests. Verify it worked:
kubectl auth can-i create serviceaccounts/token --as=<user-or-sa>
# no
External IAM/group mapping grants admin in-cluster
The fix is outside RBAC objects: remove the user from the mapped admin group in your identity provider or cloud auth config, then keep only a small break-glass group mapped to Kubernetes admin.
In your provider’s dashboard, find the Kubernetes auth mapping for the cluster and inspect which external groups map to system:masters or an admin-bound Kubernetes group. Remove broad groups like “engineering” or “developers”; keep a dedicated group such as k8s-breakglass-admins with approval-based membership.
After changing the external mapping, re-test from a fresh login session because cached credentials can hide the result. Verify it worked:
kubectl auth can-i '*' '*' --as=<user-or-sa>
# no
Aggregated ClusterRoles accidentally pull admin-like rules together
Find labels like rbac.authorization.k8s.io/aggregate-to-admin: "true" on custom ClusterRoles and remove them if the role includes sensitive rules.
kubectl edit clusterrole <name>
Delete labels such as:
metadata:
labels:
rbac.authorization.k8s.io/aggregate-to-admin: "true"
rbac.authorization.k8s.io/aggregate-to-edit: "true"
If you need aggregation, split sensitive rules into a separate non-aggregated role. Verify it worked:
kubectl get clusterrole admin -o yaml | grep -n '<sensitive-resource-or-verb>'
# no output
Prevention
- Add a CI check that fails on dangerous RBAC rules in manifests:
grep -RInE 'cluster-admin|system:masters|verbs: \["\*"\]|resources: \["\*"\]|\b(bind|escalate|impersonate)\b' k8s/ && exit 1
- Snapshot effective admin paths nightly and diff them:
kubectl get clusterrolebindings,rolebindings,clusterroles,roles -A -o yaml > rbac-snapshot-$(date +%F).yaml
git diff --no-index rbac-snapshot-prev.yaml rbac-snapshot-$(date +%F).yaml
- Alert on creation of ClusterRoleBindings and privileged token requests from audit logs. Example audit policy fragment:
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
verbs: ["create", "update", "patch"]
resources:
- group: "rbac.authorization.k8s.io"
resources: ["clusterrolebindings", "rolebindings"]
- level: Metadata
verbs: ["create"]
resources:
- group: ""
resources: ["serviceaccounts/token"]
- Replace standing human admin with group-based, time-bound access. Keep one dedicated external group for break-glass, and automate membership expiry in your IdP instead of binding individual users in Kubernetes.
- Add a periodic review script for escalation verbs and wildcard rules:
kubectl get clusterroles,roles -A -o json | jq -r '
.items[] | select(any(.rules[]?; ((.verbs // []) | any(.=="*" or .=="bind" or .=="escalate" or .=="impersonate")) or ((.resources // []) | any(.=="*")))) | [.kind, (.metadata.namespace // "-"), .metadata.name] | @tsv'
- For service accounts, keep admin bindings out of application namespaces. If a controller truly needs elevated rights, bind a dedicated service account to a custom ClusterRole with only the exact resources and verbs it uses, then verify with:
kubectl auth can-i --as=system:serviceaccount:<ns>:<sa> --list
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI