CoursesKubernetes security & hardeningSecurity domains & threat model

Security domains & threat model

The six domains and what an attacker does with a foothold.

Intermediate12 min · lesson 1 of 24

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.

The kill chain, and the domain that breaks each link
1footholdRCE or stolen token lands a…2escalatesystem hardening: seccomp,…3movecluster setup: default-deny…4persistsupply chain: signed, scanned,…
Each domain is built to sever one link. Microservice security and runtime detection wrap the whole chain: least privilege at deploy time, plus Falco and audit logs so the shell trips an alarm and leaves a record even when prevention fails.

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.

restricted-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: web
namespace: shop
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: web
image: ghcr.io/acme/web@sha256:9f2e3b7c
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
terminal
$ kubectl apply -f restricted-pod.yaml
pod/web created
# runs as the unprivileged user, not root
$ kubectl -n shop exec web -- id
uid=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/x
touch: /etc/cron.d/x: Read-only file system
command 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.

terminal
$ kubectl -n shop apply -f default-deny.yaml
networkpolicy.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:8080
curl: (28) Connection timed out after 3001 ms
pod "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.

terminal
$ cosign verify ghcr.io/acme/web@sha256:9f2e3b7c \
--certificate-identity-regexp '.*@acme\.com' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
Verification 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.com
Error: no signatures found for image
One control is not defense in depth
The classic mistake is shipping a single control and calling a namespace locked down. A team adds a default-deny NetworkPolicy, then leaves the pods running as root with the container runtime socket mounted. An attacker who lands there ignores the network entirely, breaks out to the host, and owns the node. Closing one path while another stays open just reroutes the attacker. Enforce the whole stack or you've moved the problem, not solved it.

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.

terminal
$ kubectl get pods -A -o wide | head -20
NAMESPACE NAME READY STATUS IP NODE
payments payments-api-7d9c4b-xk2m1 1/1 Running 10.244.1.18 worker-a
kube-system coredns-5d78c9869d-abc12 1/1 Running 10.244.0.9 worker-a
$ kubectl auth can-i --list --as=system:serviceaccount:payments:default | head -15
Resources Non-Resource URLs Resource Names Verbs
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
...
secrets [] [] [get list]
$ kubectl get networkpolicy -n payments
No 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.

Quick check
01A pod is compromised through an RCE in the app it runs. Which single control most directly stops the attacker from reaching other services in the cluster?
Incorrect — Hardens the pod itself so there's less to tamper with, but it doesn't change what the pod can reach over the network.
Correct — This breaks the 'move' link, so the popped pod can no longer open connections to services it was never meant to talk to.
Incorrect — Proves the image's provenance at deploy time and does nothing for an already-running pod's network access.
Incorrect — Records the lateral movement so you can investigate it, but a record is not prevention.
02The six Certified Kubernetes Security Specialist (CKS) domains split into three that harden the cluster before any workload runs (weighted 15/15/10) and three that cover a workload while it runs (weighted 20/20/20). Why do the running-workload domains carry the larger share of the exam?
Incorrect — The weighting reflects where risk concentrates, not how many flags a domain happens to touch.
Incorrect — The lesson treats cluster setup and hardening as your job too; they are graded, just weighted lower.
Correct — The lesson ties the 60% weight directly to running workloads being where most real compromises play out.
Incorrect — All six domains are in scope; none of them is optional.
03A team locks down a namespace with a default-deny NetworkPolicy but leaves its pods running as root with the container runtime socket mounted into the container. An attacker lands a shell in one of those pods. What is the most likely outcome?
Incorrect — The network is irrelevant once a local breakout path exists; the policy is guarding the wrong door here.
Correct — With root and the runtime socket, the attacker escapes the container to the node, routing around the network control entirely.
Incorrect — Nothing here sets a read-only root filesystem, and it would not stop a runtime-socket breakout anyway.
Incorrect — A NetworkPolicy governs pod network traffic only; it has no say over a host breakout via the runtime socket.

Related