ATT&CK for Kubernetes
The adversary playbook and coverage mapping.
Break-in crews don't improvise much. They work from a catalogue: pop the back window, cut the alarm, grab the jewelry, out through the garage. Kubernetes attackers run the same way. MITRE ATT&CK for Containers is that catalogue written down for clusters. MITRE is a nonprofit that documents how real intruders behave, and ATT&CK stands for Adversarial Tactics, Techniques, and Common Knowledge. It sorts every move an attacker makes into ordered columns called tactics, from getting through the door to cashing out. The payoff for a defender is plain. Once each move has a name, you can go down the list and ask two questions about it. Would we stop this? Would we even see it?
The catalogue, tactic by tactic
The columns sit in a rough order: Initial Access, Execution, Persistence, Privilege Escalation, Defense Evasion, Credential Access, Discovery, Lateral Movement, and Impact. Initial Access is the way in: an exposed API server (the cluster's control panel, every command flows through it), a kubelet listening on the network (the agent on each machine that actually starts your containers), a vulnerable app. Execution is running code where you shouldn't, usually a shell inside a pod. Credential Access is stealing keys, the tokens and Secrets (the Kubernetes objects that hold passwords and API keys) that unlock the next hop. Discovery is looking around to learn what your stolen identity can touch. Impact is the payday: cryptomining, data theft, ransomware. Real break-ins don't march straight across the columns, they loop back and skip around. So you never defend a whole column. You defend one named technique at a time, and that's where a running cluster teaches more than a diagram does. Here are three of those tactics as real commands, all from the seat of an attacker who already has a shell in one pod.
First thing you do after landing is figure out what you're holding. Every pod runs as a service account (SA for short, the robot login Kubernetes uses when the pod talks to the API). A smart attacker doesn't guess what that account can do. They ask the API server outright.
kubectl auth can-i --list
Resources Non-Resource URLs Verbsselfsubjectrulesreviews.authorization.k8s.io [] [create]pods [] [get list]pods/exec [] [create]secrets [] [get list]
That output is a confession waiting to happen. This checkout account can read every Secret in its namespace and open a shell in the pods it can see. Far too much for something that just takes payments. Each can-i check is a SelfSubjectRulesReview, and the API server logs it, so a pod's identity enumerating its own powers is itself a signal, carried on the same audit stream you'll query later. The real fix is upstream, in RBAC (Role-Based Access Control, the rules that decide which identity may touch which resource). Scope the account down until the honest answer to 'what can I do here' is 'almost nothing.'
Discovery said the token can create pods/exec. Execution is spending it. In plain terms, exec opens a terminal inside a container that's already running, the way you'd SSH into a server. Attackers reach for it because it adds no new image and no new pod, just a shell in something that already looks routine.
kubectl exec -it checkout-7c9f8-w4d2p -n shop -- /bin/sh
/ # iduid=0(root) gid=0(root) groups=0(root)/ # cat /etc/os-release | head -1PRETTY_NAME="Alpine Linux v3.20"
An exec is a create on the pods/exec subresource, and the audit log keeps the pod name, the user, and the command they ran. It's one of the loudest events in the whole cluster. People rarely shell into production, and robots never should. Prevention lives in admission control, the checkpoint every write passes through before it's saved: a policy that denies pods/exec in production, or Pod Security Admission (PSA, the built-in gate that blocks unsafe pod settings) stopping the container from running as root in the first place.
Now the move that makes the rest pay off. Credential Access is grabbing the key to the next door. By default Kubernetes drops a service-account token into every pod as a plain file. It's like a hotel keycard left on the nightstand of every room. The attacker just reads it. The token is a JWT (JSON Web Token, a signed blob with a readable middle section), so they decode that section to see exactly whose identity they've picked up.
cat /var/run/secrets/kubernetes.io/serviceaccount/token | cut -d. -f2 | base64 -d 2>/dev/null | jq .
{"aud": ["https://kubernetes.default.svc"],"exp": 1784198482,"iss": "https://kubernetes.default.svc","kubernetes.io": {"namespace": "shop","pod": { "name": "checkout-7c9f8-w4d2p" },"serviceaccount": { "name": "checkout" }},"sub": "system:serviceaccount:shop:checkout"}
Notice the token names its own pod, and there's an exp field about an hour out. Modern tokens are bound to the pod and carry a short expiry, so a stolen one stops working once the pod is deleted or the clock runs out. Good design. It still works right now though, and right now is all an attacker needs. Detection and prevention have to meet in the middle.
From catalogue to coverage
The catalogue earns its keep when you reduce each technique to two yes/no questions: can we stop it, can we see it. Take 'see it' for the exec you just watched. If you ship the API server's audit log (and you should), every action is one JSON line, and jq pulls the exec events straight out.
jq 'select(.objectRef.resource=="pods" and .objectRef.subresource=="exec") | {who:.user.username, pod:.objectRef.name, when:.requestReceivedTimestamp}' /var/log/kubernetes/audit.log
{"who": "system:serviceaccount:shop:checkout","pod": "checkout-7c9f8-w4d2p","when": "2026-07-16T09:41:22Z"}
The tell is the who. A service account, not a person, opened a shell. Route that line to an alert and you've covered the Execution technique. Teams without shipped audit logs get the same signal from Falco, a runtime sensor that watches the kernel and fires when a shell spawns inside a container. Detection is the after. Prevention is the before, and two small changes close most of this chain at once.
apiVersion: v1kind: ServiceAccountmetadata:name: checkoutnamespace: shopautomountServiceAccountToken: false---apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata:name: checkout-minnamespace: shoprules:- apiGroups: [""]resources: ["configmaps"]verbs: ["get"]---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata:name: checkout-minnamespace: shopsubjects:- kind: ServiceAccountname: checkoutnamespace: shoproleRef:kind: Rolename: checkout-minapiGroup: rbac.authorization.k8s.io
$ kubectl delete rolebinding checkout-wide -n shoprolebinding.rbac.authorization.k8s.io "checkout-wide" deleted$ kubectl apply -f checkout-hardening.yamlserviceaccount/checkout configuredrole.rbac.authorization.k8s.io/checkout-min createdrolebinding.rbac.authorization.k8s.io/checkout-min created
Roll the deployment so a fresh pod starts, then land in it the way the attacker did. The token file is gone, so the cat returns 'No such file or directory,' and with no token there's no identity to decode or reuse. Check the account's reach from the outside with kubectl auth can-i --list --as=system:serviceaccount:shop:checkout -n shop, and it comes back nearly empty. The keycard is off the nightstand, and the identity can barely open its own door. That's three catalogue techniques closed with a couple of small objects.
Try this
Work through “From catalogue to coverage” 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: on managed clusters the audit log is not where you think. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.