Search across all documentation pages
securityContext is the block on a pod or container that constrains the process at admission and start time: which user it runs as, which Linux capabilities it keeps, and whether it can escalate privileges.runAsNonRoot, runAsUser, allowPrivilegeEscalation, readOnlyRootFilesystem, capabilities.drop, seccompProfile.securityContext on a bloated root-built image still leaves attack surface.Set the pod-level fields, then the container-level fields.
Pod level: runAsNonRoot: true and seccompProfile.type: RuntimeDefault.
Container level: allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, and capabilities.drop: ["ALL"].
Give writable paths an emptyDir mount so the read-only root does not break the app.
This Deployment satisfies the restricted profile and runs a real web server.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels: { app: web }
template:
metadata:
labels: { app: web }
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: web
image: nginxinc/nginx-unprivileged@sha256:abc123
ports:
- containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache/nginx
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}The pod-level securityContext sets identity for every container. The container-level block hardens the one process. The emptyDir mounts give the read-only root the two writable paths the server needs.
Verify it after rollout:
kubectl exec deploy/web -- id
# expect uid=10001 gid=10001, not uid=0runAsNonRoot: true tells the kubelet to refuse to start the container if the image's effective user is UID 0. It is a guardrail that fails closed.
runAsUser: 10001 pins a specific non-root UID. Set both: runAsUser picks the identity, runAsNonRoot guarantees the pod fails loudly if an image change reintroduces root.
Prefer images that already declare a non-root USER in the Dockerfile. runAsUser then just confirms intent.
Linux capabilities split root's power into units like NET_BIND_SERVICE, NET_ADMIN, and SYS_ADMIN. A default container keeps a set of them even as non-root.
drop: ["ALL"] removes every capability. If the workload needs exactly one, add it back explicitly.
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]NET_BIND_SERVICE is the one you occasionally need, to bind a port below 1024. Better still, listen on a high port and avoid the capability entirely.
allowPrivilegeEscalation: false sets the no_new_privs flag, so the process cannot gain more privileges than its parent through setuid binaries. Restricted requires it.
Note that privileged: true is a separate, far more dangerous field. It disables most confinement and should appear only on trusted infrastructure pods.
readOnlyRootFilesystem: true mounts the container filesystem read-only. An attacker cannot drop tools or modify binaries on disk.
Almost every app needs one or two writable paths. Mount emptyDir volumes at those paths (commonly /tmp and a cache directory) and keep the root read-only.
fsGroup: 10001 makes the kubelet set group ownership on mounted volumes so a non-root process can write to them. Use it when a persistent volume needs to be writable by your chosen UID.
Pod rejected: "container has runAsNonRoot and image will run as root". The image defaults to UID 0. Rebuild it with a USER directive, or add runAsUser with a valid non-root UID and confirm the image supports it.
App crashes with "read-only file system". It is writing somewhere under the root mount. Find the path from the logs and mount an emptyDir there rather than dropping the read-only setting.
Removed a capability the app needed. After drop: ["ALL"], some apps lose the ability to bind low ports or manage networking. Add back the single capability with evidence, not the whole default set.
Container-level overrides pod-level. If both set the same field, the container value wins for that container. Set safe defaults at the pod level and only override deliberately.
Setting these but ignoring the image. securityContext cannot fix a distroful image full of CVEs and shells. Pair it with a minimal, scanned, digest-pinned base.
Pod Security Admission. PSA does not set these fields; it enforces that you set them. Use PSA (restricted) to require a correct securityContext, and set the fields yourself.
Policy engines with mutation. Kyverno or OPA Gatekeeper can inject a default securityContext into pods that omit one. Good for retrofitting a fleet, but explicit fields in your manifests are clearer and reviewable.
Sandboxed runtimes. gVisor or Kata add a runtime-level boundary via RuntimeClass. They complement securityContext for untrusted workloads; they do not replace it.
Pod-level or container-level? Use both. Identity and seccomp at the pod level so all containers inherit them; capability drops and readOnlyRootFilesystem per container.
What is the minimum for restricted? runAsNonRoot: true, allowPrivilegeEscalation: false, seccompProfile: RuntimeDefault, and capabilities.drop: ["ALL"].
Do init and sidecar containers need this too? Yes. Every container in the pod is evaluated, so init and sidecar containers each need a compliant securityContext.
Does this apply to my Docker Compose services? These are Kubernetes fields. Compose has its own user, read_only, and cap_drop options for local dev, but pods on nodes use securityContext.
How do I check what a running pod actually has? kubectl get pod <name> -o jsonpath='{.spec.containers[*].securityContext}', and confirm identity with kubectl exec ... -- id.
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026