Labs — Phase 10, security & supply chain + CKS¶
Twenty-six exercises in the order they are meant to run, ending on an incident that destroys the cluster it ran on. The phase teaches security as a stack of controls read one at a time — an RBAC verb, a projected token, an admission webhook, an encrypted etcd byte, a signed digest, a kernel syscall — and then, in the capstone, fires all of them at once against a real CVE in a privileged DaemonSet. Nothing about the exploit is softened and nothing about it is committed: the working payload lives only on the isolated cluster and dies with teardown, and what survives in the repo is a defanged postmortem and a Falco rule that had to beat an unseen variant to earn its place.
Each file states one claim to test or one artifact to produce, links its topology rather than restating a footprint, and ends with a teardown that deletes what it created and says whether the topology stays or goes. The framing — why each module exists, what to read and the question to answer from it — stays in phases/10-security.md. These files hold only what you type and what you should see.
| # | Exercise | Why it exists |
|---|---|---|
| 1 | A Role that grants one verb on one resource | RBAC is additive and closed — a Role with get pods refuses list pods, pods/log and secrets, and the refusals are the lesson. Provisions pair for the phase. |
| 2 | The node that cannot read its neighbour | A stolen kubelet credential reads its own node's Secrets and is refused the neighbour's — the graph edge that becomes the capstone's blast-radius boundary. |
| 3 | A token that expires | A bound projected token returns 401 sixty seconds in; a legacy Secret token is good forever. The blast-radius argument the capstone needs. |
| 4 | An authorizer chain a flag list cannot express | A CEL matchConditions + per-authorizer failurePolicy in a config file — the first node edit, the CKS reality the drill block rehearses. |
| 5 | 10.C1 — a wildcard and the path it opens | A * verb on rolebindings enables self-binding to admin; narrowing to explicit verbs still refuses it because bind was the gated verb. |
| 6 | restricted rejects a pod you can name |
PSA names all four violated controls on a plain nginx and admits the hardened variant — admission control you can read the output of. |
| 7 | Kyverno on the chain you already read | A mutating and a validating webhook on the P3 chain, ordered so the validator checks what the mutator produced. Kyverno arrives here and stays. |
| 8 | seccomp and AppArmor, two blocked operations | EPERM from a BPF filter and Permission denied from an LSM hook — two kernel controls that name themselves differently in an incident. |
| 9 | Rego that decides off-cluster | opa eval returns a set-valued deny on a laptop — the policy language, learned before its embedded form. Costs no cluster RAM. |
| 10 | 10.C2 — a policy that matches the wrong kind | A Pod-scoped rule admits a root Deployment whose template is equally root — the most common self-inflicted policy gap, caused on purpose. |
| 11 | kube-bench, a finding you can fix | The CIS loop that matters: find → locate the file → fix → re-run and watch one line flip. A benchmark number you moved, not screenshotted. |
| 12 | A Secret no longer plaintext on disk | The same etcd key, read raw, before and after EncryptionConfiguration — k8s:enc: on disk, hunter2 through the API. |
| 13 | An exec that writes an audit line | One kubectl exec, one Metadata-level audit line naming who — the control-plane view of the act the Falco rule catches from the kernel. |
| 14 | A NetworkPolicy that blocks the metadata IP | A pod reaches the internet but not 169.254.169.254 — a block expressed as an allow-with-except, because NetworkPolicy has no deny. |
| 15 | 10.C3 — encryption that lies | identity first in the provider list writes plaintext while every check that does not read the disk says encryption is on. The config that lies. |
| 16 | An SBOM that shrinks | The same binary on a full vs distroless base — a package count that falls by an order of magnitude. The cheapest CVE reduction there is. No cluster. |
| 17 | A scan that names a CVE | Two scanners on an old image agree on a CVE the fixed version clears — and disagree at the edges, because each ships its own feed. No cluster. |
| 18 | A manifest a linter refuses | A Deployment the API admits and kube-linter rejects — including a resource-limit finding PSA does not enforce, which is why the linter is not redundant. No cluster. |
| 19 | A signature with no private key | Keyless cosign — identity from OIDC, an ephemeral Fulcio cert, a public Rekor entry, no key to leak. The signature exercise 20 will demand. No cluster. |
| 20 | verifyImages rejects the unsigned | Kyverno admits the signed image on a pinned digest and refuses the unsigned one at the door — the supply chain landing on the cluster. |
| 21 | 10.C4 — a digest that no longer matches | An unsigned image and a tampered signed one fail identically, because a signature binds to content, not to a name. |
| 22 | Falco reads the syscall | A default rule fires on a kubectl exec from the kernel, with the pod named — no sidecar. Falco arrives, pinned to the worker to halve its cost. |
| 23 | 10.C5 — a string rule a variant evades | A rule keyed on proc.name = nc catches nc and misses the renamed copy — the exact failure the capstone's variant gate exists to prevent. |
| 24 | A Falco rule that names the container | A rule keyed on container context fires on the renamed binary exercise 23 missed — the capstone's detector, and its seed. |
| 25 | The CKS drill block | A timed harness where half the clock is ssh+vim on the node. Runs last, after the capstone. Provisions its own fresh pair. |
| 26 | The capstone: the CVE incident | A 9.8 in a privileged DaemonSet, detected by a rule that survives a variant, postmortem committed with no payload in it. Destroys the topology. |
Order, topology and what arrives when¶
Six exercises need no cluster. Exercise 9 (opa eval) and the four supply-chain exercises 16–19 run entirely on hopper/forge — the supply-chain module is zero-cluster-cost by design, which is what makes this phase affordable next to Falco. pair may be up and idle beside them.
One cluster runs everything else. pair is provisioned at exercise 1 and held for the whole phase; the capstone is what destroys it, and the CKS drill block then provisions its own fresh pair.
Three tools take up residence and stay. Kyverno arrives at exercise 7 (~320Mi, one replica of each controller, cleanup and reports controllers off) and is still there for exercise 20. The EncryptionConfiguration and audit policy from exercises 12–13 are control-plane keepers. Falco arrives at exercise 22 — 512Mi/node, so it is pinned to the worker only and every workload after it is scheduled there, halving the footprint to one node — and stays through the capstone.
Exceptions¶
- The capstone commits no working exploit. Exercise 26 exploits
CVE-2025-59359on the isolated cluster and commits only a defanged postmortem and a Falco rule; the payload dies with teardown, and the repo's privacy is explicitly not load-bearing. Egress is dropped for the exploit window and the PoC is self-contained — no second-stage fetch. - 10.C5 is not the last exercise of its module. The other four drills close their modules (10.C1, 10.C2, 10.C3, 10.C4), but 10.C5 sits before exercise 24, because the drill exposes the string-rule weakness that exercise 24 fixes — and 24, the capstone's detector, has to come last.
- Four exercises leave a control-plane component edited mid-file. The authorizer chain, encryption at rest, the audit policy and 10.C3 all edit the apiserver static manifest. Each confirms the apiserver came back (
kubectl get --raw /healthz) before declaring success — a malformed edit takes the control plane down, and "passed" and "not answering" are different states. - 10.C3 ends in a repair, not a delete. Exercise 15 leaves the cluster writing plaintext Secrets; its teardown restores the correct provider order and re-verifies one Secret encrypts, because leaving the lying config in place would poison every later Secret.
- Two numbers are recorded, not quoted. The kube-bench check ID tracks the CIS version your report actually cites, and exercise 8 records which AppArmor pod-spec field (
appArmorProfilevs the beta annotation) your Kubernetes version honours. Both are version-dependent; assuming instead of recording defeats the exercise. - Nothing observability-shaped is installed. Evidence is an etcd hexdump, an audit line, a
cosign verifyoutput, a Falco alert, acurlthat times out. Falco is the only agent, and it is there to be studied, not to watch.