Kubernetes SecurityContext Generator
Build a pod and container securityContext that applies, starts, and passes the namespace profile you are actually running. It emits the whole Deployment, puts each field at the level it belongs to, and mounts the emptyDir a read-only root filesystem needs.
pod-security.yaml
updates as you type Common mistakes
These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.
Setting runAsNonRoot at the pod level and a root user at the container level
The container-level securityContext wins for fields both define, so the container setting silently overrides the pod's intent.
Instead:Know which fields exist at each level and that the container level takes precedence.
Setting runAsNonRoot without runAsUser
The kubelet then refuses to start any image whose USER is numeric-unknown, because it cannot verify the id is non-zero.
Instead:Set runAsUser explicitly alongside it.
Dropping all capabilities on a container that binds a low port
Binding below 1024 needs NET_BIND_SERVICE, and dropping ALL removes it.
Instead:Drop ALL then add back only what is needed, or bind a high port and map it in the Service.
What securityContext actually controls, level by level
securityContext is two different schemas that share a name. One sits on the pod and one sits on each container, they overlap in three fields, and the container wins where both apply. Almost every mistake on this object is a field written at the wrong level, or a field that does less than its name suggests. What follows is the set that bites people, with what the failure looks like when it arrives.
Two schemas, one name, and no warning when you mix them
fsGroup, fsGroupChangePolicy, supplementalGroups and seccompProfile exist only on the pod. allowPrivilegeEscalation, readOnlyRootFilesystem, capabilities and privileged exist only on the container. runAsUser, runAsGroup and runAsNonRoot exist in both, and the container value overrides the pod value for that container only. Put a container field at pod level and modern kubectl refuses the file; an older client, or one told not to validate, drops the field and applies an object that looks hardened and is not.
spec:
template:
spec:
securityContext: # pod level
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001 # pod only
seccompProfile: # pod only
type: RuntimeDefault
containers:
- name: api
securityContext: # container level
allowPrivilegeEscalation: false # container only
readOnlyRootFilesystem: true # container only
capabilities: # container only
drop: [ALL]
# readOnlyRootFilesystem written at pod level:
error: error validating data: strict decoding error:
unknown field "spec.template.spec.securityContext.
readOnlyRootFilesystem" runAsNonRoot does not pick a user
It is an admission check, not an assignment. The kubelet resolves the user the image will run as and refuses to start the container if that is uid 0. If the image's USER is a number, the check passes. If it is a name, the kubelet has no way to resolve it against the image's /etc/passwd before the container exists, so it gives up and the pod never starts. The failure is CreateContainerConfigError, which reads like a broken config rather than a working image with the wrong USER line. Pair it with an explicit runAsUser and the answer no longer depends on how the image was written.
# Dockerfile: works
USER 10001
# Dockerfile: fails at pod start
RUN adduser -D app
USER app
kubectl get pod
NAME READY STATUS RESTARTS
api-7d9f 0/1 CreateContainerConfigError 0
kubectl describe pod api-7d9f
Error: container has runAsNonRoot and image
has non-numeric user (app), cannot verify
user is non-root A read-only root filesystem breaks on the first temp file
readOnlyRootFilesystem: true is the highest-value line on the container and the one most likely to be reverted an hour later, because nearly every image writes something early: a temp file, a pid file, a unix socket, an unpacked template cache. The write fails with EROFS from inside the application, so the error comes out in the application's own words rather than as anything Kubernetes reports. An emptyDir at each writable path is the fix, and the generator emits one per path so the manifest is not correct-but-broken.
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir:
sizeLimit: 64Mi # not a default: without it a
# runaway file fills the node
# Common extra paths, per stack:
# /var/run, /run pid files and unix sockets
# /var/cache/nginx nginx temp paths
# /home/app/.cache anything that uses XDG paths Drop ALL, then add back as little as possible
Containers start with a capability set nobody chose, including CHOWN, SETUID, SETGID and NET_RAW. NET_RAW on its own lets a compromised container craft raw packets and ARP spoof its neighbours on the node. Dropping ALL and adding back only what fails is the reviewable version of that decision. NET_BIND_SERVICE is the one people genuinely need, and only to bind below port 1024, which is usually a config change away from not being needed at all. allowPrivilegeEscalation: false belongs next to it: it sets no_new_privs so a setuid binary cannot climb out, and dropping ALL plus running non-root implies it without recording it anywhere.
capabilities:
drop: [ALL]
add: [NET_BIND_SERVICE] # only for ports < 1024
# Better: listen on 8080 and let the Service map it,
# then the add list is empty.
ports:
- port: 80
targetPort: 8080
# privileged: true is not "one more capability":
# every capability, every host device under /dev,
# no seccomp or AppArmor confinement.
# It is a root shell on the node with extra steps. seccomp: RuntimeDefault, and the annotation that stopped working
RuntimeDefault applies the container runtime's own syscall filter, which blocks the obscure calls container escapes tend to be built on. It lives on the pod, at securityContext.seccompProfile.type. The trap is the field it replaced: seccomp.security.alpha.kubernetes.io/pod was an annotation, annotations do not fail validation, and it stopped being honoured in 1.25. A pod that carries only the annotation applies cleanly, reports nothing, and runs with no seccomp profile at all. The kubelet flag that would have covered it, --seccomp-default=RuntimeDefault, is off by default on managed distributions.
# Honoured. Pod level.
securityContext:
seccompProfile:
type: RuntimeDefault
# Ignored since 1.25, and silently:
metadata:
annotations:
seccomp.security.alpha.kubernetes.io/pod: runtime/default
# Check what a running pod actually has:
kubectl get pod api-7d9f \
-o jsonpath='{.spec.securityContext.seccompProfile}' fsGroup rewrites the whole volume, every start
fsGroup sets group ownership on mounted volumes so a non-root process can write to them, and the default way it does that is a recursive chown of the entire volume on every pod start. On a volume with a lot of files that is minutes of ContainerCreating on every restart, which usually gets diagnosed as a slow image pull. fsGroupChangePolicy: OnRootMismatch checks the top-level directory first and skips the walk when the group already matches. It is also not universal: hostPath is unaffected, and CSI drivers opt in or out through fsGroupPolicy, so fsGroup is not a guarantee that a volume ends up writable.
securityContext:
fsGroup: 10001
fsGroupChangePolicy: OnRootMismatch
# Always (the default):
# chown -R on every file, every start
# OnRootMismatch:
# check the volume root's gid, skip if it matches
# An emptyDir needs none of this: the kubelet
# creates it world-writable, so any uid can write. Pod Security Admission is namespace labels, not pod fields
baseline and restricted are enforced by labels on the namespace. Nothing inside a pod opts into a profile, which means the same manifest is enforced in one namespace and unenforced in the next, and a carefully hardened pod in an unlabelled namespace is checked by nobody. When it does reject something, it rejects the pod rather than the Deployment, so kubectl apply succeeds and the workload sits at 0 ready with the real message on the ReplicaSet. restricted requires runAsNonRoot, allowPrivilegeEscalation: false, capabilities dropping ALL, and a seccompProfile. It does not require readOnlyRootFilesystem, which is worth setting anyway.
apiVersion: v1
kind: Namespace
metadata:
name: apps
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/warn: restricted
# What a rejection looks like from the outside:
kubectl get deploy api
NAME READY UP-TO-DATE AVAILABLE
api 0/2 0 0
kubectl describe replicaset api-5c8d
Error creating: pods "api-5c8d-" is forbidden:
violates PodSecurity "restricted:latest":
allowPrivilegeEscalation != false