securityContext: the hardening ladder
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:
| field | what it stops |
|---|---|
runAsNonRoot: true | a container running as UID 0 — the image is rejected at start if it would |
runAsUser/runAsGroup | choosing the identity yourself instead of the image's USER |
allowPrivilegeEscalation: false | sudo, setuid binaries, no_new_privs |
privileged: false | the container getting all capabilities and all devices |
capabilities.drop: [ALL] | NET_RAW (packet sniffing), SYS_ADMIN (mount), CHOWN, … |
readOnlyRootFilesystem | dropping a binary into the container and running it |
seccompProfile | the dangerous syscall surface |
runAsNonRoot fails on real images, and that is the lesson
nginx:1.27 runs as root by default. Adding runAsNonRoot: true givesError: 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,supplementalGroupsexist at both levels; container wins.allowPrivilegeEscalation,capabilities,readOnlyRootFilesystem,privilegedare container-only.fsGroupdrives volume ownership — it is what makes arunAsUserPod able to write to a mounted volume.