Kubernetes RBAC Privilege Escalation: Detect It in Logs
Find Kubernetes RBAC privilege escalation in audit logs: cluster-admin bindings, escalate, bind and impersonate verbs, impersonated calls, anonymous grants.
TL;DR. In the audit log, RBAC escalation is a write: a create, update or patch on clusterrolebindings, rolebindings, clusterroles or roles. The dangerous ones bind cluster-admin, grant the escalate, bind or impersonate verbs or * wildcards, or name system:anonymous / system:unauthenticated. You need request bodies (Request level) to see the roleRef and subjects. Also look at the impersonatedUser field, and remember that cloud IAM can grant cluster access without any Kubernetes RBAC object.
A new binding to cluster-admin is the single most reliable sign of a serious Kubernetes intrusion: it is how an attacker turns a borrowed token into a permanent identity. ATT&CK tracks it as T1098.006, Additional Container Cluster Roles.
RBAC in one table
RBAC has four object kinds:
| Object | Scope | Grants |
|---|---|---|
Role | Namespace | Rules (verbs on resources) inside one namespace |
ClusterRole | Cluster | Rules on cluster-scoped resources, or reusable across namespaces |
RoleBinding | Namespace | A Role or ClusterRole to subjects, inside one namespace |
ClusterRoleBinding | Cluster | A ClusterRole to subjects, everywhere |
The binding's roleRef names the role; subjects lists users, groups or service accounts. Both are in the request body, so a policy that logs RBAC writes at Metadata tells you "a ClusterRoleBinding named node-health-admin was created" and nothing about what it grants.
The escalation-prevention verbs
Kubernetes blocks the naive escalation: the RBAC documentation explains that you can only create or update a role with permissions you already hold, and only bind a role whose permissions you already hold. Three verbs lift those checks:
escalateon roles or clusterroles: write a role with more permissions than you have.bindon roles or clusterroles: bind a role you do not hold, including cluster-admin.impersonateon users, groups or service accounts: act as someone else.
The RBAC good practices list all three as privilege escalation risks, along with * wildcards, which also cover any resource type added in the future. A role that grants any of them to a person or workload is a path to cluster-admin.
Detections
The audit log analyzer checks RBAC writes with these rules:
| Rule | Severity | Logic |
|---|---|---|
| Binding to cluster-admin created | Critical | (Cluster)RoleBinding with roleRef.name: cluster-admin |
| Access granted to anonymous / unauthenticated users | Critical | Binding with subject system:anonymous or system:unauthenticated |
| Role grants escalate / bind / impersonate or wildcards | High | (Cluster)Role written with those verbs or * verbs/resources |
| Request made while impersonating another identity | Medium | impersonatedUser present |
| Anonymous request allowed | High | system:anonymous allowed beyond health, version and OIDC discovery endpoints |
| Cluster-wide role binding created | Low | Any ClusterRoleBinding written by a non-system identity |
The low-severity rule exists because of the logging gap above: when the body is missing, a new ClusterRoleBinding is still worth a look, and the authorization.k8s.io/reason annotation of later requests will name it when it gets used.
Reading a malicious binding
A fictional example, as logged at RequestResponse level:
{
"verb": "create",
"user": { "username": "system:serviceaccount:kubernetes-dashboard:kubernetes-dashboard" },
"sourceIPs": ["203.0.113.45"],
"userAgent": "kubectl/v1.30.2 (linux/amd64) kubernetes/3968350",
"objectRef": { "resource": "clusterrolebindings", "name": "node-health-admin",
"apiGroup": "rbac.authorization.k8s.io" },
"requestObject": {
"kind": "ClusterRoleBinding",
"metadata": { "name": "node-health-admin" },
"roleRef": { "apiGroup": "rbac.authorization.k8s.io", "kind": "ClusterRole", "name": "cluster-admin" },
"subjects": [ { "kind": "ServiceAccount", "name": "node-health", "namespace": "kube-system" } ]
},
"responseStatus": { "code": 201 }
}
Three things stand out: a service account creating RBAC objects, from an internet address, with kubectl; a name chosen to look like a system component; and a new service account in kube-system as subject. The next step for the attacker is usually a TokenRequest for that service account, which gives them a credential independent of the one they stole first. The fictional walkthrough follows exactly this sequence.
Impersonation
A caller with impersonate permission can send Impersonate-User, Impersonate-Group (and related) headers; the request is then authorised as the impersonated identity. The audit event keeps both: user is who really called, impersonatedUser is who they claimed to be. Always report both, and check who holds impersonate at all: in most clusters that list should be very short.
kubectl --as=<user> and --as-group use these headers; some access proxies and dashboards do too, which is why the analyzer flags impersonation at medium severity and leaves the judgement to you.
Anonymous access
Unless disabled, requests without credentials are authenticated as system:anonymous in group system:unauthenticated, as the authentication documentation describes. Health and discovery endpoints are commonly open. A binding that grants these subjects anything more means anyone who can reach the API server has those rights. Check the system:anonymous glossary entry for the settings that control it.
Escalation paths without an RBAC write
Not every escalation shows up as a binding:
- Cloud IAM. On EKS, access entries and the
aws-authConfigMap map IAM principals to Kubernetes identities; associating an admin access policy with an access entry happens in the AWS API (CloudTrail), not in the Kubernetes audit log. GKE grants cluster access through Google Cloud IAM roles, AKS through Azure RBAC role assignments when Azure RBAC for Kubernetes is enabled. Check the cloud control-plane logs for these changes. - ClusterRole aggregation. A ClusterRole with labels matching an
aggregationRulegets its rules merged into the aggregated role. Adding such a role quietly extends every binding of the aggregated role. - Workload creation. Whoever can create pods in a namespace can mount any service account of that namespace and act as it. See secrets and token theft.
- Certificates. Permission to approve CertificateSigningRequests can mint client certificates for arbitrary identities.
- Admission webhooks. Control of webhook configurations lets an attacker see or rewrite objects as they are created.
Hunting queries
Raw JSON lines, bindings to cluster-admin:
jq -c 'select((.objectRef.resource == "clusterrolebindings" or .objectRef.resource == "rolebindings")
and (.verb == "create" or .verb == "update" or .verb == "patch")
and .requestObject.roleRef.name == "cluster-admin")
| {t: .requestReceivedTimestamp, user: .user.username, ip: .sourceIPs,
name: .objectRef.name, subjects: .requestObject.subjects}' audit.log
EKS, CloudWatch Logs Insights, every RBAC write:
fields @timestamp, user.username, verb, objectRef.resource, objectRef.name
| filter @logStream like "kube-apiserver-audit"
| filter objectRef.apiGroup = "rbac.authorization.k8s.io"
| filter verb in ["create", "update", "patch", "delete"]
| sort @timestamp desc
Remediation
- Save the offending objects (
kubectl get clusterrolebinding <name> -o yaml) as evidence, then delete them. - Delete and recreate any service account that received cluster-admin, so tokens minted for it stop working.
- Review every binding to cluster-admin and to roles that can write RBAC, read secrets or exec into pods.
- Remove
escalate,bind,impersonateand wildcards from roles that do not strictly need them. - Disable anonymous authentication or restrict it to health endpoints.