Security context
By default a container runs as root, with a broad set of Linux capabilities, and writes wherever it likes in its own filesystem. That is convenient for building images and dangerous for running them.
A security context is where you take those defaults away. Most of the benefit comes from three settings, and they cost nothing when your image is built for them.
Reference
- https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
- https://kubernetes.io/docs/concepts/security/pod-security-admission/
Terminology
runAsNonRoot: true— Refuses to start the container if the image would run as root. A refusal at admission is much better than a surprise at runtime.runAsUser/runAsGroup— The numeric UID and GID to run as. Numbers, not names: the kubelet cannot resolve users from/etc/passwdin an image it has not started.fsGroup— The group applied to mounted volumes, so a non-root process can write to them. This is the usual reason a non-root container fails to start on a volume.readOnlyRootFilesystem: true— The container’s own filesystem becomes read-only. Anything that needs to write must use a mounted volume.- Capabilities — The fine-grained privileges that make up “root”. Dropping
ALLand adding back only what is needed —NET_BIND_SERVICEfor a port below 1024, for example — is the tightest common configuration. allowPrivilegeEscalation: false— Blockssetuidbinaries and similar routes to more privilege than the process started with.seccompProfile— Restricts the system calls the container may make.RuntimeDefaultis the safe, portable choice.
Pod Level Versus Container Level
securityContext appears in two places, and settings do not always exist in both:
spec:
securityContext: # applies to the Pod
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: docker.io/library/nginx:1.27
securityContext: # applies to this container
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]Pod-level settings apply to every container as a default; container-level settings win where they overlap. fsGroup and runAsNonRoot are Pod-level; capabilities and readOnlyRootFilesystem are container-level. Setting one in the wrong place is silently ignored, which is worth knowing before you conclude that a setting “does not work”.
Exercises
See the default. Run a container and look at who you are:
kubectl run whoami --image=docker.io/library/busybox --restart=Never -- sleep 3600 kubectl exec whoami -- iduid=0(root). This is what runs in most clusters unless someone changes it.Make it non-root.
spec: securityContext: runAsNonRoot: true runAsUser: 10001The container now reports a non-zero uid — unless the image’s own
USERinstruction conflicts, in which caserunAsNonRootrefuses to start it and the error says so. That error is the feature.Break an application with a read-only root filesystem, then fix it. Set
readOnlyRootFilesystem: trueon something that writes to its own directory. It fails. Now give it somewhere to write:volumeMounts: - name: tmp mountPath: /tmp - name: cache mountPath: /var/cache/nginx volumes: - name: tmp emptyDir: {} - name: cache emptyDir: {}This is the whole pattern: a read-only root plus explicit writable volumes is both more secure and a precise description of what the application actually needs to write.
Drop capabilities and keep one. Drop
ALL, then addNET_BIND_SERVICEback and run a container listening on port 80. Remove it and watch the bind fail.
Pod Security Admission
Enforcing this by hand across every workload does not scale, so clusters apply Pod Security Admission with labels on the namespace:
| Label value | What it enforces |
|---|---|
privileged | Nothing. The default before anyone thinks about it |
baseline | Blocks the obvious dangers: host networking, host paths, privileged containers |
restricted | The hardened set: non-root, no privilege escalation, dropped capabilities, seccomp |
metadata:
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/warn: restrictedwarn lets you see what would break before enforce breaks it, which is how you roll this out to an existing cluster without a maintenance window. This is covered properly on Day 4.
Gotchas Worth Knowing
runAsNonRootwith norunAsUsermakes the kubelet inspect the image’sUSER; if it cannot tell, the Pod is rejected. Being explicit is more predictable.fsGroupis usually required for a non-root process to write to a mounted volume — the volume arrives owned by root until you say otherwise.- Ports below 1024 need a capability, unless you set
net.ipv4.ip_unprivileged_port_startor run as root.NET_BIND_SERVICEis the narrow fix. - A setting in the wrong scope is ignored without an error. Check the API reference for where each field lives.
privileged: trueis not a security context tweak — it is a container with the host’s capabilities. Treat it as a red flag in review.
What to Take Away
- The default is root with broad capabilities. Say what you want instead, explicitly.
runAsNonRoot,readOnlyRootFilesystemandcapabilities: drop: [ALL]are the three that pay for themselves immediately.fsGroupand writableemptyDirmounts are what make hardened containers actually work.- Enforce it with Pod Security Admission rather than by remembering.