Security domains & threat model
The six domains and what an attacker does with a foothold.
Most cluster compromises don't begin with a broken control plane. They begin with one pod. Someone finds a remote code execution (RCE) bug in your app, or scrapes a service-account token off a mounted secret, or slips a backdoor into a dependency you pulled last Tuesday. Now there's a shell inside a container. Everything you call cluster security comes down to one question from that moment: what can this person do next, and how far do they get before something stops them or tells you they're there?
Securing a cluster is the Certified Kubernetes Administrator (CKA) syllabus with a security lens bolted on. You already know how the pieces fit together. The new job is knowing how they fail, and who is trying to make them fail. The Certified Kubernetes Security Specialist (CKS) curriculum cuts that job into six domains. Read them as six layers an attacker has to punch through, each one mapped to controls you set by hand rather than buy off a shelf.
The exam that mirrors this is hands-on and unforgiving: two hours, fifteen to twenty tasks on live clusters, 67% to pass, with the Kubernetes docs open in a second tab. So learn it as muscle memory, not trivia. Nobody asks you to define a NetworkPolicy. You get dropped on a cluster with 'default-deny the payments namespace but keep DNS working' and a running clock. Three domains cover the cluster before a workload runs: cluster setup, cluster hardening, and system hardening, weighted 15/15/10. The other three cover a workload while it runs, and they carry 60% of the score at 20/20/20, because a running workload is where most real incidents actually happen.
Start from the foothold
Every control in this course answers one specific step the attacker takes after landing in that first pod. So the useful question is never 'is the cluster secure,' which has no answer. It's 'what can someone do from inside this one container, and how far can they reach before a control blocks them or a sensor flags them?' Hold that question and each domain stops being a checklist. It becomes an obstacle you're placing in someone's way.
Defense in depth, made concrete
Defense in depth assumes every single layer will eventually fail, so the next one has to cost the attacker real effort. A pod that gets popped should already be non-root on a read-only filesystem, so there's little to grab. It should sit on a default-deny network, so there's nowhere to pivot. It should run a signed image, so it couldn't have been swapped for a poisoned one. None of these trusts the others to be enough. Let's prove each one actually holds.
Start with the container itself. A pod that can't become root and can't write to its own disk is like a hotel room with the minibar bolted shut and the windows painted over: an intruder gets in and finds almost nothing to use. In Kubernetes that's a securityContext with runAsNonRoot, a dropped capability set, and a read-only root filesystem, plus a seccomp (secure computing mode) profile. Think of seccomp as a contact whitelist for system calls: the kernel only answers the ones on the list.
apiVersion: v1kind: Podmetadata:name: webnamespace: shopspec:securityContext:runAsNonRoot: truerunAsUser: 1000runAsGroup: 1000seccompProfile:type: RuntimeDefaultcontainers:- name: webimage: ghcr.io/acme/web@sha256:9f2e3b7csecurityContext:allowPrivilegeEscalation: falsereadOnlyRootFilesystem: truecapabilities:drop: ["ALL"]
$ kubectl apply -f restricted-pod.yamlpod/web created# runs as the unprivileged user, not root$ kubectl -n shop exec web -- iduid=1000 gid=1000 groups=1000# an attacker's usual persistence trick is blocked by the read-only root fs$ kubectl -n shop exec web -- touch /etc/cron.d/xtouch: /etc/cron.d/x: Read-only file systemcommand terminated with exit code 1
That hardens one room. Lateral movement is about the hallways. A flat pod network is an open-plan office where anyone can walk to any desk, and a default-deny NetworkPolicy puts a keycard reader on every door. Apply one to the namespace, then prove a pod that shouldn't reach the payments API simply can't. Deny is a silent drop, so a blocked probe hangs until its timeout rather than getting refused.
$ kubectl -n shop apply -f default-deny.yamlnetworkpolicy.networking.k8s.io/default-deny-all created# a pod that was never allowed to talk to payments-api now can't$ kubectl run probe --rm -it --image=nicolaka/netshoot -n shop -- \curl -m 3 payments-api:8080curl: (28) Connection timed out after 3001 mspod "probe" deleted
The last link is persistence, and a lot of it arrives through the image. A signature is a tamper-evident seal on the box: change what's inside and the seal stops matching. Sign images in your pipeline with cosign, then have the cluster refuse anything that doesn't verify. Verify one by hand and you get both outcomes. A signed build passes. The unsigned tag fails shut.
$ cosign verify ghcr.io/acme/web@sha256:9f2e3b7c \--certificate-identity-regexp '.*@acme\.com' \--certificate-oidc-issuer https://token.actions.githubusercontent.comVerification for ghcr.io/acme/web@sha256:9f2e3b7c --The following checks were performed on each of these signatures:- The cosign claims were validated- Existence of the claims in the transparency log was verified offline- The signing certificate was verified against the Fulcio roots# an image nobody signed does not slip through$ cosign verify ghcr.io/acme/web:pr-1337 \--certificate-identity-regexp '.*@acme\.com' \--certificate-oidc-issuer https://token.actions.githubusercontent.comError: no signatures found for image
Two habits make the rest of this course faster. First, learn where controls live, because they sit in a small fixed set of places: /etc/kubernetes/manifests/*.yaml for control-plane flags, /var/lib/kubelet/config.yaml for the kubelet, namespace labels for Pod Security Admission (PSA), and an EncryptionConfiguration for secrets at rest. On a timed exam or at 3am, hunting for where a setting lives is how the minutes vanish. Second, don't forget the last domain. Runtime detection is the layer that assumes prevention already failed and makes sure you find out anyway.
Treat every lesson in this course as an answer to the same question: after the first pod falls, what still works for the attacker? If the answer is "read Secrets," fix RBAC and automount. If the answer is "reach the database," fix NetworkPolicy. If the answer is "escape to the node," fix securityContext and host mounts. The domains are not a checklist for a slide deck. They are a triage order for an incident that has already started.
Defense in depth fails when teams pick a favorite control and ignore the rest. A perfect NetworkPolicy next to a privileged pod is still a privileged pod. A locked-down securityContext next to a cluster-admin service account is still a credential factory. Walk the path end to end once with kubectl and a throwaway namespace, and you will feel which layer is actually holding.
On the exam and in production, time pressure pushes people toward the flashy fix. Resist that. Start with identity and network, because they are cheap to verify and expensive to leave open. Then harden the node and the admission path. Runtime detection comes last on purpose: it is the smoke detector, not the lock.
When you rehearse an incident, write the path on a whiteboard: foothold, credential, lateral move, persistence. Each box maps to a control in this course. If a box is empty, that is your next ticket, not a future nice-to-have. Teams that only track CVE counts still lose clusters to open NetworkPolicies and privileged pods that never appeared in an image scan. Write the verification command next to the control in the same pull request, and keep the sample output so the next on-call person can tell pass from fail without guessing.
Try this
On a lab cluster, map the attack surface the way the CKS exam forces you to: start from a compromised app pod and ask which door opens next. You are not hunting CVEs yet. You are listing the foothold, the lateral move, and the persistence path.
$ kubectl get pods -A -o wide | head -20NAMESPACE NAME READY STATUS IP NODEpayments payments-api-7d9c4b-xk2m1 1/1 Running 10.244.1.18 worker-akube-system coredns-5d78c9869d-abc12 1/1 Running 10.244.0.9 worker-a$ kubectl auth can-i --list --as=system:serviceaccount:payments:default | head -15Resources Non-Resource URLs Resource Names Verbsselfsubjectaccessreviews.authorization.k8s.io [] [] [create]...secrets [] [] [get list]$ kubectl get networkpolicy -n paymentsNo resources found in payments namespace.
Takeaway
CKS work is layered: harden the pod, constrain the network, shrink identity, then detect what slips through. If you only ship one control, the attacker walks the other doors.