The Privilege Escalation Paths Nobody Audits
Most organizations secure their Kubernetes clusters by writing roles and bindings, running through a checklist, and moving on. That's the first mistake.
RBAC is not a configuration you complete: it's an attack surface you maintain. A single over-permissive binding, an auto-mounted ServiceAccount token, or a misconfigured IRSA trust policy can hand an attacker a path from low-privilege foothold to full cluster compromise. Once there, the blast radius is total: every workload, every secret, every connected cloud account.
What makes this hard isn't complexity: it's invisibility. The most dangerous escalation paths don't look dangerous in isolation. pods/exec seems like a reasonable developer permission. Granting secrets: get seems harmless. But combined, these become a step-by-step path to node takeover, and standard security tooling rarely flags them.
What this post covers:
The permission combinations that enable privilege escalation, and why they're so commonly granted
How ServiceAccount tokens and IRSA misconfigurations bridge Kubernetes and cloud IAM
Practical audit commands and detection rules you can run today
Remediation patterns that enforce least-privilege without breaking developer workflows
The RBAC Mental Model: And Where It Breaks Down
Kubernetes RBAC operates on a simple allow-only model: subjects (users, groups, ServiceAccounts) are granted permissions via RoleBindings or ClusterRoleBindings. There are no explicit deny rules. Any permission granted, even unintentionally, is permanently open unless actively revoked.
The problem is not that RBAC is broken. It is that:
Defaults are permissive.
The
system:authenticatedgroup gets read access to many non-sensitive-looking APIs that can still be abused.
Operators grant broad permissions for convenience.
cluster-adminbindings are routinely created during debugging sessions and never removed.
Indirect escalation paths exist.
Some permission combinations that look innocuous individually combine to form full privilege escalation chains.
The cluster-admin ClusterRoleBinding Problem
The most common misconfiguration in production clusters is the over-assignment of cluster-admin. This happens through:
# Classic 'just get it working' binding — seen constantly in the wild kubectl create clusterrolebinding developer-admin \ --clusterrole=cluster-admin \ --user=developer@company.com # Or worse — binding to a service account used by a CI deployment kubectl create clusterrolebinding ci-admin \ --clusterrole=cluster-admin \ --serviceaccount=ci:github-runner
⚠️ High Risk: The CI runner binding is particularly dangerous: if your runner pod is compromised via a malicious GitHub Action or supply chain attack, the attacker immediately inherits cluster-admin privileges.
Detection: Audit existing ClusterRoleBindings
kubectl get clusterrolebindings -o json | jq ' .items[] | select(.roleRef.name == "cluster-admin") | { name: .metadata.name, subjects: .subjects } '
Detection with Falco
- rule: Cluster Admin Binding Created desc: Detect creation of ClusterRoleBinding to cluster-admin condition: > kube_audit and ka.verb in (create, update) and ka.target.resource = clusterrolebindings and ka.req.binding.role = cluster-admin output: > Cluster admin binding created (user=%ka.user.name binding=%ka.target.name) priority: CRITICAL
Dangerous Permission Combinations
Individual permissions often seem harmless. The risk emerges when combined. Here are the most commonly overlooked combinations.
pods/exec: The Interactive Shell Gift
The pods/exec permission allows a subject to exec into any running pod in the permitted namespace. This is routinely granted to developers for debugging.
# This looks reasonable... rules: - apiGroups: [""] resources: ["pods/exec"] verbs: ["create"]
The escalation path:
Attacker gains access to any principal with
pods/execLists pods to find one running as root or with a privileged security context
Execs into the pod
Escapes to the node via volume mounts,
/proc/1/root, or privileged capabilities
Gains node-level access and can reach the kubelet API
Find privileged pods:
kubectl get pods -A -o json | jq ' .items[] | select( .spec.containers[].securityContext.runAsUser == 0 or .spec.containers[].securityContext.privileged == true ) | {namespace: .metadata.namespace, name: .metadata.name} '
Once inside a privileged pod:
# Mount the host filesystem and escape to node nsenter --target 1 --mount --uts --ipc --net --pid -- bash # Result: root shell on the underlying node
secrets GET: Credential Harvesting at Scale
The ability to get or list secrets in a namespace, or cluster-wide, is a high-value permission frequently granted without full awareness of its impact. In a typical production cluster, secrets routinely contain database credentials, API keys, cloud provider credentials, TLS private keys, and service account tokens.
# Dump all secrets in a namespace kubectl get secrets -n production -o json | jq ' .items[] | { name: .metadata.name, type: .type, keys: (.data | keys) }' # Decode a specific secret value kubectl get secret db-credentials \ -o jsonpath='{.data.password}' | base64 -d
Audit who has secrets access with kubectl-who-can (krew plugin):
kubectl who-can get secrets -n production
create pods + serviceAccountName: Token Hijacking
If a subject can create pods and the namespace contains ServiceAccounts with elevated permissions, they can create a pod that mounts a privileged ServiceAccount token. This is one of the most overlooked privilege escalation vectors.
# Attacker creates a pod targeting a high-privilege ServiceAccount apiVersion: v1 kind: Pod metadata: name: escalation-pod spec: serviceAccountName: high-privilege-service-account containers: - name: attacker image: curlimages/curl command: ["sleep", "infinity"]
Inside the pod, the mounted SA token can call the Kubernetes API with elevated rights:
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) curl -H "Authorization: Bearer $TOKEN" \ https://kubernetes.default.svc/api/v1/namespaces/kube-system/secrets
Required permissions for this attack:
pods: createin target namespace + knowledge of a high-privilege ServiceAccount name (easily listed withserviceaccounts: list).
ServiceAccount Token Abuse
Auto-mounted Tokens
By default, Kubernetes automatically mounts a ServiceAccount token into every pod. This remains a misconfiguration risk when automountServiceAccountToken is not explicitly set to false.
# Check which pods have tokens auto-mounted kubectl get pods -A -o json | jq ' .items[] | select(.spec.automountServiceAccountToken != false) | { namespace: .metadata.namespace, pod: .metadata.name, serviceAccount: .spec.serviceAccountName } '
From inside a compromised pod, the token is immediately usable:
APISERVER=https://kubernetes.default.svc TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt # Enumerate what this SA can access curl --cacert $CACERT -H "Authorization: Bearer $TOKEN" \ $APISERVER/api/v1/namespaces/default/pods
OIDC / IRSA Abuse in Cloud Environments
Modern cloud Kubernetes platforms (EKS, GKE, AKS) support Workload Identity / IRSA, mapping ServiceAccounts to cloud IAM roles. Misconfigured trust policies create a bridge between Kubernetes and cloud infrastructure that attackers can cross from any pod.
Overly permissive EKS IRSA trust policy (allows any pod in any namespace):
{ "Condition": { "StringLike": { "oidc.eks.us-east-1.amazonaws.com/.../id/EXAMPLE:sub": "system:serviceaccount:*:*" // Wildcard — never use this } } }
Correct trust policy, scoped to a specific namespace and ServiceAccount:
"StringEquals": { "oidc.eks.us-east-1.amazonaws.com/.../id/EXAMPLE:sub": "system:serviceaccount:production:my-specific-app" }
The kubectl exec Pivot Chain
Beyond permission abuse, kubectl exec is a powerful lateral movement tool for an attacker who has compromised developer credentials or a CI/CD kubeconfig.
Compromised developer credentials (kubeconfig) → kubectl exec into running application pod → Read mounted secrets / environment variables → Use credentials to access databases, S3, external APIs → Or: escape to node if pod is privileged → Access kubelet API on the node → List and exec into ALL pods on that node
Detecting exec with Falco
- rule: Terminal Shell in Container desc: A shell was spawned via exec in a container condition: > spawned_process and container and shell_procs and proc.pname in (docker, runc, containerd-shim) output: > Shell spawned in container (user=%user.name container=%container.name image=%container.image.repository pod=%k8s.pod.name ns=%k8s.ns.name cmdline=%proc.cmdline) priority: WARNING
Auditing Your Cluster: A Practical Checklist
RBAC Audit with rakkess and kubectl-who-can
# Install via krew kubectl krew install access-matrix kubectl krew install who-can # Full access matrix for your current identity kubectl access-matrix # Who can delete pods in kube-system? kubectl who-can delete pods -n kube-system # Who can create ClusterRoleBindings? kubectl who-can create clusterrolebindings
Find Escalation Paths with rbac-police
git clone https://github.com/PaloAltoNetworks/rbac-police cd rbac-police && go build -o rbac-police . # Evaluate against live cluster ./rbac-police eval lib/police.rego # Example output: # CRITICAL: ServiceAccount 'github-runner' in 'ci' can escalate to cluster-admin # via: create pods + serviceAccountName targeting 'admin-sa' in 'kube-system'
CIS Benchmark with kube-bench
docker run --pid=host --userns=host --rm \ -v /etc:/etc:ro -v /var:/var:ro \ aquasec/kube-bench:latest \ --benchmark cis-1.8 2>/dev/null # Focus on CIS controls 5.1.1 through 5.1.6 for RBAC
Remediation: Least-Privilege RBAC Patterns
Disable Auto-mounting at the ServiceAccount Level
apiVersion: v1 kind: ServiceAccount metadata: name: my-app namespace: production automountServiceAccountToken: false # Opt out by default
Use Short-lived Projected Service Account Tokens
volumes: - name: kube-api-token projected: sources: - serviceAccountToken: path: token expirationSeconds: 1800 # 30 minutes audience: kubernetes.default.svc
Enforce RBAC Policy with OPA/Gatekeeper
package kubernetes.admission deny[msg] { input.request.kind.kind == "ClusterRoleBinding" input.request.object.roleRef.name == "cluster-admin" not is_break_glass_subject(input.request.object.subjects) msg := "cluster-admin bindings are prohibited outside break-glass accounts" } is_break_glass_subject(subjects) { subject := subjects[_] subject.name == "break-glass-admin" }
Restrict pods/exec via Admission Webhook
apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: name: restrict-exec webhooks: - name: restrict-exec.security.internal rules: - apiGroups: [""] apiVersions: ["v1"] operations: ["CONNECT"] resources: ["pods/exec"] namespaceSelector: matchExpressions: - key: environment operator: In values: ["production"]
MITRE ATT&CK for Containers Mapping
Summary: What Good Looks Like
A mature Kubernetes RBAC posture includes:
Regular automated audits of all ClusterRoleBindings and RoleBindings, flagging
cluster-adminassignments for human review
No wildcard permissions (
*) in any role that is not a system role
ServiceAccount token auto-mounting disabled by default, enabled only where explicitly required
Runtime detection via Falco or similar, with alerting on exec, secret access, and new ClusterRoleBinding creation
IRSA / Workload Identity trust policies scoped to specific namespace + ServiceAccount pairs, never wildcarded
OPA/Gatekeeper policies enforcing RBAC hygiene at admission time, preventing dangerous configurations from reaching the cluster
Bottom Line: RBAC is a strong foundation, but it requires continuous auditing, not a one-time setup. The escalation paths described here are not theoretical; they are consistently found in production clusters and actively exploited in the wild.
Ready to see Aviatrix in action?
Get a personalized live demo walkthrough or explore our latest deep-dive cloud threat research intelligence.



