Hedronite · Cert-Prep Lesson · CNCF / CKS · Thu 2026-08-13 · Trio #88

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.

Lesson Class: Cert-Prep (CNCF Certified Kubernetes Security Specialist)
Blueprint: Monitoring, Logging and Runtime Security — 20%, opened here for the first time on this arc
Cert Priority: Seat 2 of the banked ordering: Terraform Pro → CKA/CKS → AWS SAP/DOP → GCP PCA → AZ-900
Counter: k8s_day_counter 7 · odd → CKS-emphasis
Practice Questions: 6 · tap to reveal
Paired Ops: Kubernetes Runtime Isolation on GKE
Paired Dev: kopf RuntimeClass enforcement
Grounding: Rice Ch.8 pp.95-102 · Muschko ch07 · CKS-PREP q05 + q09 + q16 + q10
RuntimeClass
Create-time kernel vote. handler runsc. Immutable on a live Pod.
Falco
Syscalls on the node. evt.is_open_read, not evt.type=read. Scale the Deployment.
Audit + immutable
First match wins. readOnlyRootFilesystem true. emptyDir is still stateless.
RuntimeClass chooses the kernel. Falco names the syscall the create-time half missed. Neither substitutes for the other.

§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:

ToolWatchesFires onBlind to
RuntimeClassnothing; it chooses the kernel at starta createa process that already started
Audit policyAPI requestskubectl exec, get secret, CRD writesa syscall that never became an API call
Falcosyscalls on the nodeopenat(/dev/mem), a shell with a TTYan 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

Question 1
A Falco rule uses evt.type=read and fd.name=/dev/mem. The malicious container opens /dev/mem with openat() and the rule never fires. What is wrong?
tap to reveal
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.
Question 2
You added --audit-policy-file to the apiserver manifest. kubectl get nodes now returns connection refused. The policy YAML is valid. What did you forget?
tap to reveal
The policy file (and the log directory) must be mounted into the static Pod via 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.
Question 3
A namespace is labeled for gVisor enforcement. Existing Pods are Running without runtimeClassName. You kubectl patch each Pod. The API returns 422. What is the legal next step?
tap to reveal
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.
Question 4
Question 9 style: prod/gcc mounts a hostPath, prod/app is privileged with readOnlyRootFilesystem: false, prod/frontend has readOnlyRootFilesystem: true and an emptyDir. Which do you delete?
tap to reveal
gcc and app. hostPath is stateful. privileged plus a writable root is not immutable. emptyDir is ephemeral; frontend stays.
Question 5
An audit policy logs all core resources at Request, then has a later rule excluding kube-proxy watches. The log is still full of kube-proxy. Why?
tap to reveal
Rules evaluate in order; first match wins. The catch-all Request rule matched first. Put the level: None kube-proxy rule above the catch-all.
Question 6
dmesg inside a Pod that carries runtimeClassName: sandboxed prints the host kernel's boot messages. Name the two checks, in order.
tap to reveal
Confirm the RuntimeClass object exists and that its 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

🫡 ⚖️ 📜 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).

🫡 ⚖️ 📜
Leo.Syri — Praetor Consulate, Imperium Luminaura
Filed 2026-08-13 at Fajr · Trio #88 · sprint day 22 · k8s_day_counter 7 (CKS-emphasis)
Ops · Dev · Cert trio shipped MD + HTML in-cycle