Make Containers Immutable: Non-Root and Read-Only Root FS
Modify the existing Deployment named lamp-deployment, running in namespace lamp, so that its containers: run with user ID 20000 use a read-only root filesystem forbid privilege escalation The…
Live lab access
This task description is public. Live practice requires full access unless this is the selected free weekly task. AI Lab Coach is available in single-task labs, including the free weekly task. See this week's free task.
Create an account to continue →
On a phone? Read the task and create your account here, then sign in on a desktop or laptop for the terminal lab. Nothing starts until you choose Start.
The task
Context
You must update an existing Pod to ensure the immutability of its containers.
Task
Modify the existing Deployment named lamp-deployment, running in namespace lamp, so that its containers:
- run with user ID
20000 - use a read-only root filesystem
- forbid privilege escalation
The Deployment's manifest file can be found at /home/candidate/finer-sunbeam/lamp-deployment.yaml.
Original Prepium guide · Written for Kubernetes v1.35 and checked against the official documentation on 6 October 2026. Use a disposable practice cluster.
Make a container immutable at runtime without breaking the app
An "immutable" container cannot change its own image contents while it runs. Three settings carry most of the weight: a read-only root filesystem, so an intruder cannot drop tools or edit binaries; a fixed non-root user, so a process escape starts without root; and allowPrivilegeEscalation: false, so a setuid binary cannot raise privileges again. Applications still need somewhere to write temporary files, which is what an emptyDir volume is for. On the CKS exam the hard part is rarely the fields; it is keeping the workload running afterwards.
image layers (read-only, readOnlyRootFilesystem: true)
└─ /tmp ← emptyDir (writable, lives as long as the Pod)
process: uid 31000, runAsNonRoot checked by kubelet, no_new_privs set, capabilities dropped
1. Apply the hardened Deployment and watch it fail
Save this as cks-immutable-deploy.yaml. The container appends a timestamp to /tmp every few seconds and exits if the write fails.
apiVersion: apps/v1
kind: Deployment
metadata:
name: ledger
namespace: prepium-tutorial
spec:
replicas: 1
selector:
matchLabels:
app: ledger
template:
metadata:
labels:
app: ledger
spec:
securityContext:
runAsNonRoot: true
runAsUser: 31000
runAsGroup: 31000
containers:
- name: writer
image: busybox:1.36
command: ["sh", "-c", "while date >> /tmp/beat.log; do sleep 5; done; echo cannot write; exit 1"]
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
# step 2 adds a volumeMount for /tmp here
kubectl create namespace prepium-tutorial
kubectl apply -f cks-immutable-deploy.yaml
kubectl -n prepium-tutorial get pods -l app=ledger -w
kubectl -n prepium-tutorial logs deploy/ledger
The Pod starts, writes fail against the read-only root, the script prints cannot write and exits, and the Pod settles into CrashLoopBackOff. This is the normal first result of turning on a read-only root filesystem: the setting works, and the app was relying on a writable path you had not noticed.
2. Give the app only the writable paths it needs
Edit the file: add this under the container, at the level of securityContext,
volumeMounts:
- name: scratch
mountPath: /tmp
and this under the Pod spec, at the level of containers:
volumes:
- name: scratch
emptyDir:
sizeLimit: 64Mi
Apply again and wait for kubectl -n prepium-tutorial rollout status deployment/ledger. An emptyDir starts empty, survives container restarts, and is deleted when the Pod leaves the node, so nothing written there becomes part of the image. The sizeLimit keeps a compromised process from filling the node's ephemeral storage. Mount only the directories the app really writes (common ones are /tmp, a cache directory and a PID or socket directory).
3. Prove each property from inside
kubectl -n prepium-tutorial exec deploy/ledger -- id
kubectl -n prepium-tutorial exec deploy/ledger -- sh -c "grep ' / ' /proc/mounts"
kubectl -n prepium-tutorial exec deploy/ledger -- tail -2 /tmp/beat.log
kubectl -n prepium-tutorial exec deploy/ledger -- grep -E 'NoNewPrivs|CapEff' /proc/1/status
Expect uid 31000 from id, an ro option on the root (/) mount, fresh timestamps in the log, NoNewPrivs: 1 (the kernel flag behind allowPrivilegeEscalation: false) and an all-zero CapEff after dropping every capability.
4. Understand runAsNonRoot versus runAsUser
runAsUser chooses the UID. runAsNonRoot: true chooses nothing; it makes the kubelet check that the container would not run as UID 0 and refuse to start it otherwise. Remove runAsUser from a copy of the manifest and use an image whose default user is root: the Pod is created, but the container never starts and its events explain the refusal. Setting a numeric non-zero runAsUser satisfies the check. Values in the Pod spec take precedence over the user baked into the image, which is useful for images you do not control.
Common mistakes
- Mounting the emptyDir over a directory that holds files the app ships with. The mount hides them; the app then fails in new ways.
- Putting
readOnlyRootFilesystemorallowPrivilegeEscalationat Pod level. They exist only on containers, so set them on every container, init containers included. - Choosing a UID that cannot read the app's files. The container starts but the app logs permission errors.
- Editing only a manifest file and never applying it, or editing the live object and leaving the file stale. Make both agree.
| Example check | Evidence |
|---|---|
| Root filesystem is read-only | readOnlyRootFilesystem: true; the / mount is ro and writes outside volumes fail |
| Fixed non-root user | runAsUser in the spec; id prints that UID |
| No privilege escalation | allowPrivilegeEscalation: false; NoNewPrivs: 1 |
| Still healthy | Rollout complete, Pod Ready, restarts not climbing |
Cleanup: kubectl delete namespace prepium-tutorial. References: Configure a Security Context, emptyDir volumes, Linux kernel security constraints for Pods and containers. Next, try the restricted Pod Security Standard or Pod versus container securityContext.
What this tests
Apply Pod Security Standards, securityContext, and mTLS, and protect secrets at rest. On the CKS exam, Minimize Microservice Vulnerabilities tasks are graded purely on what you build in the cluster - not multiple choice - so the only way to get faster is to do them on a real cluster against a clock.
Practice it for real
The public description lets you study the task before signing up. Full access includes this live lab; the selected Task of the Week is free with an account. You solve the task in a real terminal, hit validate, and a programmatic checker scores exactly what you got right and wrong (with partial credit). You can open the solution while practising, then retry and use the failed checks to improve your score.