CKS Runtime Security — the syscall that should not have happened
Create-time isolation tries to keep the syscall from being possible. Falco names it when it happens anyway.
§I — Frame
Twenty percent. Monitoring, Logging and Runtime Security is the largest CKS domain still unopened on this arc, and it is the one the exam scores by whether you can catch a process that is already running.
Three prior CKS fires closed three other domains. Minimize Microservice Vulnerabilities came in on 07-26 with the securityContext and the restricted profile. Cluster Setup came in on 08-01 with kube-bench, ingress TLS, and metadata protection. Supply Chain Security came in on 08-07 with trivy, the SBOM pairing, and ImagePolicyWebhook. Those three together are fifty percent. Today's domain is the other large one, and it starts after admission has already said yes.
Call the event the syscall that should not have happened. Hold the name. Create-time isolation (RuntimeClass, readOnlyRootFilesystem, the restricted profile) is the half that tries to keep the syscall from being possible. Runtime detection (Falco) and the audit log are the half that names it when it happens anyway. The exam will hand you both halves in the same session and will not tell you which half the scenario is testing until you read the task twice.
Muschko's CKS Study Guide files the domain as Chapter 7, three directories: falco/, audit-log/, immutable-container/. The Bootcamp set scores the same three as Questions 16, 5, and 9, and scores the create-time runtime as Questions 10 and 22. That is the whole domain as a file listing.
§II — Create-time: RuntimeClass is a kernel vote, not a detector
Burns, Beda, Hightower, and Evenson introduce RuntimeClass as the API that lets a Pod select a container runtime the way it already selects a ServiceAccount (Ch. 14, RuntimeClass, pp. 242-245). The exam object is three fields:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: sandboxed
handler: runsc
The name is yours. The handler is runsc. Question 10 then asks you to patch every Deployment in a namespace so the Pod template carries runtimeClassName: sandboxed. The patch lands on spec.template.spec. A patch on the Deployment spec itself is a no-op the grader can see from the still-running unsandboxed Pods.
Question 22 is the proof. dmesg inside the Pod prints Starting gVisor instead of the host kernel's boot log. Rice describes why (Ch. 8, gVisor, pp. 100-102): gVisor intercepts syscalls in user space, implements a subset itself, and the host kernel sees runsc rather than the application. From the host, ps still listed the container's processes when there was no guest kernel (Ch. 4, Container Processes from the Host Perspective, pp. 50-52). After runsc, the shape of that tree changes. The exam wants the dmesg line, not the ps lecture.
Two traps, both from the Ops lesson, both examinable.
You cannot sandbox the default pool, and you cannot combine privileged: true with the sandbox. A task that asks you to "make this privileged Pod run under gVisor" is a refusal, not a patch. Drop privileged, or drop runtimeClassName.
runtimeClassName is immutable on a live Pod. The day's Dev lesson exists because of that sentence. If the task says the Pods are already Running and unsandboxed, you delete (or roll) so a new create can carry the field. Patching the live object is a 422.
RuntimeClass is create-time. It does not watch syscalls after start. A sandboxed Pod that still needs a syscall gVisor does not implement will fail in a different way, and Falco will never see that syscall on the host because the guest kernel swallowed or rejected it. Detection and isolation are two tools. Run both.
§III — Falco: name the syscall, then stop the workload
Falco consumes an eBPF (or kmod) event stream and matches it against a rule corpus. A rule has a name, a condition in Sysdig filter syntax, an output template, a priority, and tags. The exam asks you to complete a rule, run it, read the output, and act on the workload it names.
Question 16 is the canonical shape. A container is opening /dev/mem. Physical memory from inside a container is a kernel bypass. The incomplete rule lives at /home/candidate/falco-rule.yaml. You finish it, identify the Deployment, and scale it to 0.
- rule: detect dev mem access
desc: An attempt to read or write /dev/mem
condition: >
((evt.is_open_read=true or evt.is_open_write=true)
and fd.name contains /dev/mem)
output: >
Process %proc.name accessed /dev/mem
(command=%proc.cmdline user=%user.name
container=%container.id
image=%container.image.repository
pod_name=%k8s.pod.name
namespace=%k8s.ns.name)
priority: WARNING
tags: [security]
The condition is the whole question. evt.is_open_read=true matches open()/openat() with a read flag. evt.type=read matches the read() syscall, which is the common wrong answer and will miss the open. fd.name contains /dev/mem is Sysdig filter syntax, not regex. The output fields are the ones the task listed; omit k8s.pod.name and you cannot name the workload to scale.
Then:
falco -r /home/candidate/falco-rule.yaml
kubectl scale deployment mem-hacker --replicas=0
READY 0/0 is the grader's check. Deleting the Pod leaves the ReplicaSet to recreate it. Scale the Deployment.
Muschko's companion rule in ch07/falco/falco_rules.local.yaml is the other common exam shape: a shell spawned as the container entrypoint with a TTY (spawned_process and container and shell_procs and proc.tty != 0). Same discipline, different condition. Learn the macros (spawned_process, container, shell_procs) from the bundled falco_rules.yaml; the exam lets you read them.
Priority order, because a task will ask you to set one: EMERGENCY > ALERT > CRITICAL > ERROR > WARNING > NOTICE > INFORMATIONAL > DEBUG. Muschko uses ALERT for the shell. Question 16 uses WARNING. Match the task.
This is the syscall that should not have happened, made visible. RuntimeClass would have made /dev/mem unreachable from inside the guest. The Pod that Falco caught is the Pod the create-time half missed: unlabeled namespace, forgotten runtimeClassName, webhook failurePolicy: Ignore during a rollout. The Dev lesson's timer would have listed it. Falco tells you what it did in the meantime.
Three tools, three questions, none of them a substitute:
| Tool | Watches | Fires on | Blind to |
|---|---|---|---|
| RuntimeClass | nothing; it chooses the kernel at start | a create | a process that already started |
| Audit policy | API requests | kubectl exec, get secret, CRD writes | a syscall that never became an API call |
| Falco | syscalls on the node | openat(/dev/mem), a shell with a TTY | an API request that never became a syscall |
Question 19 (process monitor) and Question 38 (monitor a named Pod) are the same Falco skill pointed at a different condition. Question 17 is Question 5 with more resources. If the clock is short, 5 / 9 / 16 / 10 are the four that cover the domain; 17, 19, 22, 38 are the extras that confirm it.
§IV — Audit policy: first match wins, and the volume mount is the restart
The audit log is the API server's record of who asked for what. It is not Falco. Falco watches syscalls on a node. The audit log watches API requests. A kubectl exec that opens a shell will appear in both, for different reasons. A kubectl get secret appears only in the audit log. A process inside a container that opens /dev/mem appears only in Falco.
Question 5 is the setup task. Store logs at /var/log/kubernetes-logs.log. Retain 5 days, 10 old files, 100 MB per file. Extend the policy so CronJob changes log at RequestResponse, kube-system Deployments log request bodies, everything else in core and extensions logs at Request, and kube-proxy watches on endpoints and services are excluded.
Levels, in order of volume: None < Metadata < Request < RequestResponse. RequestResponse includes the object body both ways and is the expensive one. Use it where the task names a kind. Use None where the task names a noisy client.
Rules evaluate in order. First match wins. The kube-proxy exclusion must sit above the catch-all core/extensions Request rule, or the catch-all eats the watch traffic and the exclusion never fires. Muschko's sample in ch07/audit-log/audit-policy.yaml shows the same shape at smaller scale: RequestResponse on pods, then Metadata on pods/log and pods/status, with omitStages: [RequestReceived] to drop the duplicate inbound event.
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: None
users: ["system:kube-proxy"]
verbs: ["watch"]
resources:
- group: ""
resources: ["endpoints", "services"]
- level: RequestResponse
resources:
- group: "batch"
resources: ["cronjobs"]
- level: Request
namespaces: ["kube-system"]
resources:
- group: "apps"
resources: ["deployments"]
- level: Request
resources:
- group: ""
- group: "extensions"
Then the apiserver flags, on the static Pod, the way the 08-10 troubleshooting lesson taught you to edit a control-plane file:
- --audit-policy-file=/etc/audit/audit-policy.yaml
- --audit-log-path=/var/log/kubernetes-logs.log
- --audit-log-maxage=5
- --audit-log-maxbackup=10
- --audit-log-maxsize=100
Both the policy file and the log directory need volumeMounts plus volumes (File for the policy, DirectoryOrCreate for /var/log/). Missing either is the reason the apiserver dies on restart. Wait sixty seconds. tail the log. Create a CronJob and grep cronjob the file. If the file is empty, the flags pointed at a path the container cannot see.
Poulton files runtime security next to the rest of the production checklist (Ch. 16, pp. 226-232). The audit log is the legal record. Falco is the page. They do not substitute.
§V — Immutable containers: the root filesystem is a floor
Question 9 asks you to make every Pod in prod stateless and immutable, then delete the ones that are not. Stateless means no hostPath and no PVC; emptyDir dies with the Pod and counts as stateless. Immutable means privileged: false and readOnlyRootFilesystem: true.
Inspect:
kubectl get pods -n prod -o jsonpath="{range .items[*]}{.metadata.name}{': priv='}{.spec.containers[*].securityContext.privileged}{' ro='}{.spec.containers[*].securityContext.readOnlyRootFilesystem}{'\n'}{end}"
The Bootcamp fixture: app is privileged with a writable root; gcc uses hostPath; frontend is compliant. Delete app and gcc with --grace-period=0 --force. Leave frontend.
Muschko's ch07/immutable-container/read-only-filesystem-pod.yaml is the other half of the same task: an nginx Pod that sets readOnlyRootFilesystem: true and then mounts emptyDir on /var/run, /var/cache/nginx, and /usr/local/nginx. A process that cannot write its own image still has to write runtime sockets and caches. The emptyDirs are the allowed floor. A task that says "make this nginx immutable" and does not mention the mounts will fail the container with a CrashLoopBackOff on a failed open of /var/run/nginx.pid. Add the mounts. Do not relax the flag.
The 07-26 restricted profile already required readOnlyRootFilesystem: true, allowPrivilegeEscalation: false, runAsNonRoot: true, and dropped ALL capabilities. Today's question is the runtime half of that profile, scored by deletion rather than by a namespace label. A Pod that passes restricted admission can still be writable if someone later patched the live object (they cannot, for most of those fields) or if the Pod was created before the namespace was labeled. Same window as the Dev lesson. Same answer: delete, so create-time runs again.
Rice's seccomp and AppArmor sections (Ch. 8, pp. 95-99) remain examinable on unsandboxed nodes. They do not compose with gVisor on GKE, which the Ops lesson named as a product refusal. On the exam cluster, which is not GKE, a seccomp task and a RuntimeClass task can both appear. Do not apply a custom seccomp profile to a runsc Pod unless the task says to; the combination is undefined on the managed product and a waste of clock on the exam if the task did not ask.
§VI — Exam drill
evt.type=read and fd.name=/dev/mem. The malicious container opens /dev/mem with openat() and the rule never fires. What is wrong?evt.type=read matches the read() syscall, not an open-for-reading. Use evt.is_open_read=true (and evt.is_open_write=true if the task includes writes). fd.name contains /dev/mem is safer than exact equality if the path is a prefix.--audit-policy-file to the apiserver manifest. kubectl get nodes now returns connection refused. The policy YAML is valid. What did you forget?volumeMounts and volumes. Without the mount, the apiserver starts, cannot open the file, and exits. Wait, then check crictl ps -a and the apiserver container log. Do not edit the manifest a second time while the first restart is in flight.runtimeClassName. You kubectl patch each Pod. The API returns 422. What is the legal next step?spec.runtimeClassName is immutable. Delete the Pods (or roll the Deployment) so the recreate hits admission. A JSON patch cannot install a guest kernel under a process that already started.prod/gcc mounts a hostPath, prod/app is privileged with readOnlyRootFilesystem: false, prod/frontend has readOnlyRootFilesystem: true and an emptyDir. Which do you delete?gcc and app. hostPath is stateful. privileged plus a writable root is not immutable. emptyDir is ephemeral; frontend stays.level: None kube-proxy rule above the catch-all.dmesg inside a Pod that carries runtimeClassName: sandboxed prints the host kernel's boot messages. Name the two checks, in order.handler is runsc. Then confirm the Pod landed on a node that registered that handler. A name with no handler on that node is a Pod that should not have started; a Pod that started and still sees the host kernel never took the sandbox path.§VII — The Trio Interlock
Ops named the guest kernel and the GKE product shape: Autopilot votes per Pod, Standard votes per pool, the handler is runsc, the GKE name is gvisor. Dev wrote the field at create time with a kopf mutate, and named the 422 that a timer cannot fix. This lesson is the exam's version of the same wall, by hand, against a clock: RuntimeClass to choose the kernel, readOnlyRootFilesystem to take the floor away, the audit policy to record the API half, Falco to name the syscall the create-time half missed.
Coverage note for the next CKS day: Cluster Hardening (15%) and System Hardening (15%) remain. Blueprint after today is 70 percent. The Bootcamp set still has kube-bench leftovers, AppArmor, seccomp, and the CIS worker-upgrade questions against those two domains.
Set a twelve-minute timer. Write the Question 16 Falco rule from memory, then the Question 5 audit flags and the first-match ordering, then delete the two non-compliant Pods in Question 9 without looking at the names. The step you forget is almost certainly the kube-proxy rule sitting above the catch-all, or evt.type=read.
Related
- Prior arc: CKS Supply Chain Security
- Domain hub: Cross-References/Certifications-Roadmap
- Grounding tome: Container Security (Rice) (Ch. 8 Strengthening Container Isolation, gVisor, pp. 100-102)
- Kubestronaut companion: Muschko CKS Study Guide Ch. 7 (falco / audit-log / immutable-container)
- Paired Ops: Kubernetes Runtime Isolation on GKE
- Paired Dev: kopf RuntimeClass enforcement
🫡 ⚖️ 📜 Leo.Syri — Praetor Consulate, Imperium Luminaura Filed 2026-08-13 at Fajr. Trio #88, sprint day 22, K8s track, k8s_day_counter 7 (CKS-emphasis).