CKSKubernetes 1.35

Lessons

securityContext: the hardening ladder

updated 2026-10-22 by seed

The ladder, in the order that makes sense

spec:
  securityContext:                 # pod level: applies to all containers
    runAsNonRoot: true
    runAsUser: 2000
    runAsGroup: 3000
    fsGroup: 3000
    seccompProfile: {type: RuntimeDefault}
  containers:
  - name: app
    image: registry.example.com/app:1.4.2
    securityContext:
      allowPrivilegeEscalation: false
      privileged: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]
        add: ["NET_BIND_SERVICE"]   # only if the app binds a low port

Each field answers a specific attack:

fieldwhat it stops
runAsNonRoot: truea container running as UID 0 — the image is rejected at start if it would
runAsUser/runAsGroupchoosing the identity yourself instead of the image's USER
allowPrivilegeEscalation: falsesudo, setuid binaries, no_new_privs
privileged: falsethe container getting all capabilities and all devices
capabilities.drop: [ALL]NET_RAW (packet sniffing), SYS_ADMIN (mount), CHOWN, …
readOnlyRootFilesystemdropping a binary into the container and running it
seccompProfilethe dangerous syscall surface

runAsNonRoot fails on real images, and that is the lesson

nginx:1.27 runs as root by default. Adding runAsNonRoot: true gives
Error: container has runAsNonRoot and image will run as root. Two honest fixes:

  • runAsUser: 101 (nginx's own user) plus a writable cache directory, or
  • use an image built to run non-root (bitnami/nginx, distroless, nginxinc/nginx-unprivileged).

The exam expects you to notice the difference and pick one.

readOnlyRootFilesystem and the three emptyDirs nginx needs

    securityContext:
      readOnlyRootFilesystem: true
    volumeMounts:
    - {name: run, mountPath: /var/run}
    - {name: cache, mountPath: /var/cache/nginx}
    - {name: tmp, mountPath: /tmp}
  volumes:
  - {name: run, emptyDir: {}}
  - {name: cache, emptyDir: {}}
  - {name: tmp, emptyDir: {}}

Without those mounts nginx exits with mkdir() "/var/cache/nginx/client_temp" failed (30: Read-only file system).
The marker starts the Pod: a CrashLoop is a zero.

Finding the Pods that break the rules

The exam loves "find every Pod that runs privileged / as root / without a
read-only root and fix or delete it":

kubectl get pods -A -o json | jq -r '
  .items[] | select(
    .spec.containers[].securityContext.privileged == true
    or .spec.hostNetwork == true
    or .spec.hostPID == true
  ) | "\(.metadata.namespace)/\(.metadata.name)"'

Then patch:

kubectl patch deployment app -n prod --type=strategic -p \
 '{"spec":{"template":{"spec":{"containers":[{"name":"app","securityContext":{"privileged":false,"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"runAsNonRoot":true,"readOnlyRootFilesystem":true}}]}}}}'

Pod-level vs container-level, again

  • runAsUser, runAsGroup, fsGroup, seccompProfile, supplementalGroups exist at both levels; container wins.
  • allowPrivilegeEscalation, capabilities, readOnlyRootFilesystem, privileged are container-only.
  • fsGroup drives volume ownership — it is what makes a runAsUser Pod able to write to a mounted volume.