The cluster attack surface
Exposed API/kubelet/etcd, IMDS, and blast radius.
Most people who break into a Kubernetes cluster don't start with an exploit. They start with a map. A burglar casing a building does the same thing: before touching a single lock, they write down every way in. The front door, the loading dock, the roof hatch, the fire escape, the side door by the smoking area that someone always props open. A Kubernetes cluster (the system that runs and coordinates your containers across a fleet of machines) has that exact set of entrances. And several of them will happily answer questions about themselves before you've proven who you are. That's what an attacker's scanner is counting on. This lesson walks the perimeter the way an attacker walks it. We'll name every door and the port it listens on, then pair each one with how to catch someone rattling it and how to lock it.
The doors, and what each one is
Six things are worth knowing by name. Each one listens on a port, a numbered channel that anything on the network can knock on, and each is something an attacker's scanner will look for. The API server (port 6443) is the reception desk. Every action in the cluster goes through it. A human running kubectl, a pod asking for a password, all of it walks up to that desk and gets checked three ways: who are you (authentication), are you allowed to do this (authorization), and does cluster policy accept it (admission control). etcd (port 2379) is the records room behind the desk. It's a key-value database, basically one giant filing cabinet, and it holds every piece of cluster state. Secrets live there too, stored base64-encoded. Base64 is encoding, not encryption. It scrambles nothing and anyone can reverse it. So a copy of etcd, or an old backup sitting forgotten in some bucket, is a copy of every password and token you have. The kubelet (port 10250) is the building super on each node. A node is just one worker machine in the fleet. The kubelet starts containers, stops them, and can open a shell inside them. So a kubelet that accepts anonymous callers hands all of that power to anyone who can reach it over the network. Its read-only sibling (port 10255) is dangerous in a quieter way: it gives up the full list of pods with no authentication at all, and teams forget it's even switched on. The container runtime (containerd, reached over a CRI socket, where CRI stands for Container Runtime Interface) is effectively root on the node for anyone who can touch it. And from inside any pod there's one last door: the cloud metadata endpoint, known as IMDS, the Instance Metadata Service, sitting at the address 169.254.169.254. It's the concierge that will read the node's cloud credentials out loud to any caller who asks from inside the building.
Casing the cluster with kubectl
Start where a real operator starts: discovery. kubectl cluster-info prints the control-plane URLs, the web addresses of the cluster's front desk. kubectl get --raw / asks the API server for its own index, the list of every path it serves, and that's the fastest read you'll get on the shape of the attack surface. Neither command is an attack, and that's the whole point. These two are step one for the person defending the cluster and step one for the person casing it. Same keystrokes, different intent.
kubectl cluster-infokubectl get --raw /
Kubernetes control plane is running at https://10.0.7.1:6443CoreDNS is running at https://10.0.7.1:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy{"paths":["/api","/api/v1","/apis","/apis/apps/v1","/healthz","/livez","/metrics","/openapi/v3","/readyz","/version"]}
Now run it with no identity at all. Point plain curl at port 6443 with no credentials and no token. On almost every cluster the /version path still answers, which quietly leaks your exact Kubernetes version to anyone who can reach the port. Then ask for something real, like the Secrets in a namespace. A healthy cluster hands back a clean 403 that names system:anonymous, the identity the API server gives to any caller it doesn't recognize. Get actual data back instead, and you've found a front door left unlocked. That's how more than one real breach has started.
curl -sk https://10.0.7.1:6443/versioncurl -sk https://10.0.7.1:6443/api/v1/namespaces/default/secrets
{"major":"1","minor":"31","gitVersion":"v1.31.4","platform":"linux/amd64"}{"kind": "Status","status": "Failure","message": "secrets is forbidden: User \"system:anonymous\" cannot list resource \"secrets\" in API group \"\" in the namespace \"default\"","reason": "Forbidden","code": 403}
The same walk from inside a pod
Say a vulnerable web app hands an attacker a shell inside one pod. Now the perimeter walk starts over from inside the building, because the pod ships with its own set of doors. It carries a mounted service-account token, an identity the API server will honor. So kubectl auth can-i --list reads back exactly what that identity is allowed to do: its RBAC permissions, the rules that decide which identity can touch which resource. The metadata endpoint is one HTTP call away and will recite the node's cloud role and its temporary keys. And if the local kubelet still takes anonymous callers, a single curl to port 10250 lists every pod on the node, each one carrying its own token to steal.
kubectl auth can-i --listcurl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/eks-node-instance-rolecurl -sk https://10.0.7.23:10250/pods | jq -r '.items[].metadata.name'
Resources Non-Resource URLs Resource Names Verbsselfsubjectreviews [] [] [create]pods [] [] [get list]secrets [] [] [get list]eks-node-instance-role{"Code": "Success","AccessKeyId": "ASIA24EXAMPLE7QANHUP","SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/EXAMPLEKEY","Token": "IQoJb3JpZ2luX2VjEND...truncated...","Expiration": "2026-07-16T15:44:02Z"}nginx-frontend-6b9c4payments-worker-77f5d
See it, then shut it
Every door you just walked leaves a trace somewhere. Two logs matter most here: the API server's audit log, and a runtime sensor like Falco, a tool that watches what processes actually do inside your containers. Anonymous reconnaissance shows up in the audit log as a user named system:anonymous. A scanner rattling one resource after another shows up as a burst of 403 responses from a single source IP address. The IMDS pivot and the token theft are different animals. They happen entirely inside the pod and never reach the API server, so the audit log never sees them. That's the runtime sensor's job: it watches for a process opening a connection to 169.254.169.254, or reading the service-account token file straight off disk.
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","verb":"list","user":{"username":"system:anonymous","groups":["system:unauthenticated"]},"sourceIPs":["10.0.7.23"],"objectRef":{"resource":"secrets","namespace":"default"},"responseStatus":{"metadata":{},"code":403},"requestReceivedTimestamp":"2026-07-16T09:14:02.113451Z"}
09:14:07.442110001: Notice Outbound connection to EC2 instance metadata service(command=curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/connection=10.0.7.23:51234->169.254.169.254:80container=nginx-frontend pod=nginx-frontend-6b9c4 namespace=shop image=shop/web:1.4)Rule: Contact EC2 Instance Metadata Service From Container
Closing a door is usually one config flag or one policy, and the order you work in matters. Shut anything that answers before authentication first. Turn off the kubelet's anonymous auth and its read-only port. Let a public /version be the most an unauthenticated caller ever gets back. Put a real auth proxy, or a private-only endpoint, in front of etcd and any Dashboard. Then, and only then, start shrinking what each authenticated identity is allowed to reach, which is the whole back half of this course. The map you just drew feeds straight into the next lesson, where each of these doors becomes a named technique in the ATT&CK matrix, the public catalog of how real attackers operate. That lets you measure two things at once: whether a door is shut, and whether you'd even notice someone trying it.
apiVersion: kubelet.config.k8s.io/v1beta1kind: KubeletConfigurationauthentication:anonymous:enabled: false # 10250 now returns 401 to unauthenticated callerswebhook:enabled: true # authenticate bearer tokens against the API serverauthorization:mode: Webhook # authorize each call via SubjectAccessReviewreadOnlyPort: 0 # kill the unauthenticated 10255 pod-list port
Try this
Work through “See it, then shut it” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
The trap worth remembering here: networkPolicy usually does not block the metadata endpoint. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.