Kubernetes PodSecurityContext vs SecurityContext: Which One Applies
Both PodSecurityContext and SecurityContext control Linux security settings in Kubernetes — but they apply at different scopes. Get the scope wrong and your security settings either silently don't apply or get overridden by something you didn't expect.

Kubernetes has two places where you can set security settings for a workload: spec.securityContext (the pod-level PodSecurityContext) and spec.containers[*].securityContext (the container-level SecurityContext). They share similar names, some overlapping fields, and the same YAML path prefix — which is why they get conflated.
The rule is simple once you know it: pod-level settings are defaults for all containers in the pod. Container-level settings override pod-level for that specific container. Some fields only exist at one level.
The Pod-Level: PodSecurityContext
spec.securityContext applies to all containers in the pod, including init containers.
1spec:
2 securityContext:
3 runAsUser: 1000
4 runAsGroup: 3000
5 fsGroup: 2000
6 runAsNonRoot: true
7 seccompProfile:
8 type: RuntimeDefaultThe fields that only exist at the pod level (you can't set them per-container):
fsGroup — the GID assigned to all mounted volumes. The kubelet changes volume ownership to match this GID before mounting, and all processes in the pod can read/write those volumes. This can't be set per-container because volume ownership is a pod-level concept.
supplementalGroups — adds extra GIDs to all containers in the pod. Useful when processes need access to specific resources (e.g., a shared group for GPU access).
sysctls — kernel parameter settings that apply to the pod's network namespace. Safe sysctls are allowed by default; unsafe ones require explicit opt-in at the node level.
spec:
securityContext:
sysctls:
- name: net.core.somaxconn
value: "1024"The Container Level: SecurityContext
spec.containers[*].securityContext applies to a single container. This is where you control container-specific privileges.
1spec:
2 containers:
3 - name: app
4 securityContext:
5 allowPrivilegeEscalation: false
6 readOnlyRootFilesystem: true
7 runAsNonRoot: true
8 capabilities:
9 drop: ["ALL"]
10 add: ["NET_BIND_SERVICE"]Fields that only exist at the container level:
allowPrivilegeEscalation — prevents setuid binaries and sudo from gaining extra privileges. Defaults to true, which means any setuid binary in your container image can escalate. Set this to false on all containers unless you know you need it.
readOnlyRootFilesystem — mounts the container's root filesystem as read-only. Forces you to explicitly declare writable paths via emptyDir or tmpfs volumes. This is one of the best mitigations against container escape and post-compromise persistence.
capabilities — fine-grained Linux capabilities. The right pattern is drop: ["ALL"] then add back only what you need (usually nothing, occasionally NET_BIND_SERVICE for ports below 1024).
privileged — full host access. Never use this in production. If a container needs it, that's an architectural problem.
procMount — controls /proc visibility. Default (masked) is correct for almost everything.
What Happens When Both Are Set?
For fields that exist in both PodSecurityContext and container SecurityContext — runAsUser, runAsGroup, runAsNonRoot, seccompProfile, seLinuxOptions — the container-level value wins for that container.
1spec:
2 securityContext:
3 runAsUser: 1000 # applies to all containers by default
4 containers:
5 - name: app
6 securityContext:
7 runAsUser: 2000 # overrides pod-level for this container only
8 - name: sidecar
9 # no securityContext — inherits runAsUser: 1000 from podIn this example, app runs as UID 2000. sidecar runs as UID 1000.
This override behavior is per-field. If you set runAsUser at the container level, that overrides the pod-level runAsUser — but fsGroup is unaffected since it doesn't exist at the container level.
A Hardened Baseline
Here's the security context pattern I apply to most production workloads:
1spec:
2 securityContext:
3 runAsNonRoot: true
4 runAsUser: 1000
5 runAsGroup: 1000
6 fsGroup: 1000
7 seccompProfile:
8 type: RuntimeDefault
9 containers:
10 - name: app
11 image: my-app:latest
12 securityContext:
13 allowPrivilegeEscalation: false
14 readOnlyRootFilesystem: true
15 capabilities:
16 drop: ["ALL"]
17 volumeMounts:
18 - name: tmp
19 mountPath: /tmp
20 - name: cache
21 mountPath: /app/cache
22 volumes:
23 - name: tmp
24 emptyDir: {}
25 - name: cache
26 emptyDir: {}The readOnlyRootFilesystem: true typically breaks applications that write to their own directory. You fix this by explicitly mounting emptyDir volumes at the paths the app needs to write to. It's a one-time audit of your container's write patterns — worth doing.
Which Fields Live Where?
| Field | PodSecurityContext | SecurityContext |
|---|---|---|
runAsUser | ✓ | ✓ (overrides pod) |
runAsGroup | ✓ | ✓ (overrides pod) |
runAsNonRoot | ✓ | ✓ (overrides pod) |
fsGroup | ✓ only | — |
supplementalGroups | ✓ only | — |
sysctls | ✓ only | — |
seccompProfile | ✓ | ✓ (overrides pod) |
seLinuxOptions | ✓ | ✓ (overrides pod) |
allowPrivilegeEscalation | — | ✓ only |
readOnlyRootFilesystem | — | ✓ only |
capabilities | — | ✓ only |
privileged | — | ✓ only |
procMount | — | ✓ only |
Validating Your Settings
After applying security contexts, verify they took effect:
1# Check what user a container is actually running as
2kubectl exec my-pod -c app -- id
3# uid=1000(app) gid=1000(app) groups=1000(app),2000
4
5# Confirm no privilege escalation
6kubectl exec my-pod -c app -- cat /proc/1/status | grep -E "^(Cap|No)"
7
8# Check if filesystem is read-only
9kubectl exec my-pod -c app -- touch /test-write 2>&1
10# touch: /test-write: Read-only file systemIf you're using Pod Security Admission, the restricted policy enforces most of these settings and will reject pods that don't comply. For existing clusters, use audit or warn mode first before enforcing — it'll tell you which pods would fail without breaking anything.
Related Topics
Found this useful? Share it.


