securityContext
Summary
securityContextis 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.- Insight: It is the cheapest, highest-leverage hardening you can apply. A handful of fields moves a workload from "root with all capabilities" to "unprivileged with none".
- Key Concepts:
runAsNonRoot,runAsUser,allowPrivilegeEscalation,readOnlyRootFilesystem,capabilities.drop,seccompProfile. - When to Use: On every workload. These fields are what the restricted Pod Security Standard requires.
- Limitations: It constrains the process, not the image or the network. A hardened
securityContexton a bloated root-built image still leaves attack surface.
Recipe
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.
Working Example
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=0Deep Dive
runAsNonRoot vs runAsUser
runAsNonRoot: 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.
Dropping capabilities
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
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
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 and volume ownership
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.
Gotchas
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.
Alternatives
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.
FAQs
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.
Related
- The Layers of Pod Security
- Pod Security Basics
- seccomp & AppArmor
- Why Containers Aren’t a Security Boundary
- Pod Security Best Practices
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).