| Tested on | Rocky Linux 10.2 (Red Quartz) workstation |
|---|---|
| Package | kubectl 1.36.3 |
| Applies to | Any host with kubectl configured; Kubernetes cluster with Linux nodes |
| Cert prep | CKA · CKAD · CKS |
| Lab environment | Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm |
| Privilege | Normal user (no sudo required on the workstation) |
| Scope | Pod and container SecurityContext fields, runAsUser, runAsGroup, runAsNonRoot, fsGroup, readOnlyRootFilesystem, allowPrivilegeEscalation, privileged overview, and RuntimeDefault seccomp. Does not cover capability add/drop YAML, Pod Security Admission, SELinux or AppArmor profile authoring, custom seccomp profiles, host namespaces, or node hardening. |
| Related guides | Authentication and admission control Kubernetes RBAC Kubernetes Pods and lifecycle |
SecurityContext controls how a container process runs on the node: which Linux user and group own the process, whether the root filesystem is writable, and how tightly the kernel isolates syscalls. This walkthrough hardens one Pod step by step in namespace secctx-lab, showing the failure each field addresses before combining the settings in a single application manifest.
Create the lab namespace with an idempotent apply:
kubectl create namespace secctx-lab --dry-run=client -o yaml | kubectl apply -f -Sample output on the first run:
namespace/secctx-lab createdA later run typically prints:
namespace/secctx-lab unchangedPod-Level vs Container-Level SecurityContext
Kubernetes exposes two placement points in the Pod spec:
spec:
securityContext: # PodSecurityContext
containers:
- name: app
securityContext: # container SecurityContext| Pod-level examples | Container-level examples |
|---|---|
runAsUser |
runAsUser |
runAsGroup |
runAsGroup |
runAsNonRoot |
runAsNonRoot |
fsGroup |
readOnlyRootFilesystem |
seccompProfile |
allowPrivilegeEscalation |
supplementalGroups |
privileged |
A container-level value overrides the matching Pod-level field when both are set. Fields such as readOnlyRootFilesystem and privileged exist only on the container SecurityContext.
Run Containers with Non-Root Identity
Set runAsUser and runAsGroup
Without any SecurityContext, a busybox container runs as root:
kubectl run sc-default -n secctx-lab --image=busybox:1.36 --restart=Never --command -- sleep 3600Wait for the Pod to become Ready before inspecting it:
kubectl wait --for=condition=Ready pod/sc-default -n secctx-lab --timeout=90sCheck the process identity:
kubectl exec -n secctx-lab sc-default -- idSample output:
uid=0(root) gid=0(root) groups=0(root),10(wheel)Save sc-uid-gid.yaml with explicit numeric IDs on the container:
apiVersion: v1
kind: Pod
metadata:
name: sc-uid-gid
namespace: secctx-lab
spec:
restartPolicy: Never
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
securityContext:
runAsUser: 1000
runAsGroup: 3000Apply the manifest:
kubectl apply -f sc-uid-gid.yamlkubectl wait --for=condition=Ready pod/sc-uid-gid -n secctx-lab --timeout=90skubectl exec -n secctx-lab sc-uid-gid -- idSample output:
uid=1000 gid=3000 groups=3000Manifest values override the image default. The chosen UID must be able to execute the image entrypoint and read any files the application needs. Picking an arbitrary UID on an image built for root often produces permission errors at startup.
Enforce runAsNonRoot
runAsNonRoot: true requires the container's effective UID to be non-zero. For a predictable result, use an image with a numeric non-root user or set an explicit non-zero runAsUser. Set runAsGroup separately when the primary group must also be non-zero.
Save sc-nonroot-fail.yaml to enable runAsNonRoot on an image that defaults to root:
apiVersion: v1
kind: Pod
metadata:
name: sc-nonroot-fail
namespace: secctx-lab
spec:
restartPolicy: Never
containers:
- name: app
image: alpine:3.20
command: ["sleep", "3600"]
securityContext:
runAsNonRoot: trueApply the manifest:
kubectl apply -f sc-nonroot-fail.yamlWait for the actual waiting reason:
kubectl wait pod/sc-nonroot-fail -n secctx-lab --for=jsonpath='{.status.containerStatuses[0].state.waiting.reason}'=CreateContainerConfigError --timeout=90sPrint the recorded message:
kubectl get pod sc-nonroot-fail -n secctx-lab -o jsonpath='{.status.containerStatuses[0].state.waiting.message}{"\n"}'Sample output:
container has runAsNonRoot and image will run as root (pod: "sc-nonroot-fail_secctx-lab(...)", container: app)Save sc-nonroot-ok.yaml with an explicit non-root UID and matching primary group:
apiVersion: v1
kind: Pod
metadata:
name: sc-nonroot-ok
namespace: secctx-lab
spec:
restartPolicy: Never
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000Apply the manifest:
kubectl apply -f sc-nonroot-ok.yamlkubectl wait --for=condition=Ready pod/sc-nonroot-ok -n secctx-lab --timeout=90skubectl exec -n secctx-lab sc-nonroot-ok -- idSample output:
uid=1000 gid=1000 groups=1000The container passes runAsNonRoot because the effective UID is not zero. The setting does not require a non-zero primary GID—Kubernetes documents runAsNonRoot as validation against UID 0. This article does not cover Dockerfile USER directives; focus on the Kubernetes fields and whether the running image supports the UID you choose.
Control Supplemental Groups
The default supplementalGroupsPolicy: Merge can add group memberships defined for the user inside the container image. Strict uses only the groups declared through the Pod SecurityContext:
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
supplementalGroupsPolicy: StrictStrict makes the effective group list more predictable, but the node's container runtime must support the feature. Fine-grained supplemental-group control graduated to GA in Kubernetes 1.35.
Configure Volume Access with fsGroup
fsGroup is a Pod-level field. Kubernetes applies it to mounted volumes that support ownership changes, adding a supplemental group to the container process and adjusting volume permissions.
Demonstrate a Permission Failure
Save sc-fsgroup-demo.yaml. An init container sets /data to root:root mode 755:
apiVersion: v1
kind: Pod
metadata:
name: sc-fsgroup-demo
namespace: secctx-lab
spec:
restartPolicy: Never
initContainers:
- name: prep
image: busybox:1.36
command: ["sh", "-c", "mkdir -p /data && chown root:root /data && chmod 755 /data"]
volumeMounts:
- name: data
mountPath: /data
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "ls -ld /data; touch /data/nofsg.txt 2>&1; sleep 3600"]
securityContext:
runAsUser: 1000
runAsGroup: 1000
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}Apply the manifest:
kubectl apply -f sc-fsgroup-demo.yamlkubectl wait --for=condition=Ready pod/sc-fsgroup-demo -n secctx-lab --timeout=90skubectl logs -n secctx-lab sc-fsgroup-demo -c appSample output:
drwxr-xr-x 2 root root 4096 Jul 27 02:11 /data
touch: /data/nofsg.txt: Permission deniedApply fsGroup
Save sc-fsgroup-after.yaml with Pod-level fsGroup:
apiVersion: v1
kind: Pod
metadata:
name: sc-fsgroup-after
namespace: secctx-lab
spec:
restartPolicy: Never
securityContext:
fsGroup: 2000
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "touch /data/test.txt && ls -ld /data; sleep 3600"]
securityContext:
runAsUser: 1000
runAsGroup: 1000
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}Apply the manifest:
kubectl apply -f sc-fsgroup-after.yamlkubectl wait --for=condition=Ready pod/sc-fsgroup-after -n secctx-lab --timeout=90sOn a fresh emptyDir mount with fsGroup: 2000, the directory shows group 2000 with the setgid bit, and UID 1000 receives supplemental group 2000:
kubectl logs -n secctx-lab sc-fsgroup-after -c appSample output:
drwxrwsrwx 2 root 2000 4096 Jul 27 02:11 /dataThe application writes successfully:
kubectl exec -n secctx-lab sc-fsgroup-after -- ls -la /dataSample output:
-rw-r--r-- 1 1000 2000 0 Jul 27 02:11 test.txtUnderstand fsGroupChangePolicy
fsGroupChangePolicy controls recursive ownership changes for supported volume types. Its default value is Always. Set it to OnRootMismatch when you want Kubernetes to skip recursive changes if the volume root already has the expected ownership and permissions. The field has no effect on emptyDir, ConfigMap, or Secret volumes, and it does not apply when a CSI driver handles the mount group through VOLUME_MOUNT_GROUP.
Optional YAML for a persistent volume workflow:
spec:
securityContext:
fsGroup: 2000
fsGroupChangePolicy: OnRootMismatchKeep this separate from the emptyDir demonstration so readers do not think the policy controls that lab volume.
Restrict Filesystem Writes
readOnlyRootFilesystem: true blocks writes anywhere on the container image filesystem that is not covered by a writable mount.
Save sc-readonly-fail.yaml:
apiVersion: v1
kind: Pod
metadata:
name: sc-readonly-fail
namespace: secctx-lab
spec:
restartPolicy: Never
containers:
- name: app
image: busybox:1.36
command:
- sh
- -c
- |
touch /readonly-test.txt 2>&1
sleep 3600
securityContext:
readOnlyRootFilesystem: trueApply the manifest:
kubectl apply -f sc-readonly-fail.yamlkubectl wait --for=condition=Ready pod/sc-readonly-fail -n secctx-lab --timeout=90skubectl logs -n secctx-lab sc-readonly-fail -c appSample output:
touch: /readonly-test.txt: Read-only file systemApplications still need paths for caches, temp files, or runtime state. Save sc-readonly-ok.yaml with an emptyDir at /tmp:
apiVersion: v1
kind: Pod
metadata:
name: sc-readonly-ok
namespace: secctx-lab
spec:
restartPolicy: Never
containers:
- name: app
image: busybox:1.36
command:
- sh
- -c
- |
touch /readonly-test.txt 2>&1
touch /tmp/writable.txt
ls -l /tmp/writable.txt
sleep 3600
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}Apply the manifest:
kubectl apply -f sc-readonly-ok.yamlkubectl wait --for=condition=Ready pod/sc-readonly-ok -n secctx-lab --timeout=90skubectl logs -n secctx-lab sc-readonly-ok -c appSample output:
touch: /readonly-test.txt: Read-only file system
-rw-r--r-- 1 root root 0 Jul 27 02:12 /tmp/writable.txtThe root path stays read-only while /tmp accepts writes through the mounted volume.
Control Container Privileges
Prevent Privilege Escalation
allowPrivilegeEscalation: false sets the Linux no_new_privs flag for an ordinary container. It cannot prevent escalation when the container is privileged or has CAP_SYS_ADMIN; in those cases privilege escalation is effectively enabled. It also does not replace capability controls.
securityContext:
allowPrivilegeEscalation: falseThis is a baseline hardening field on application containers. For capabilities.add and capabilities.drop YAML, see Linux capabilities in SecurityContext.
Understand Privileged Mode
Privileged mode is a container-level switch:
securityContext:
privileged: truePrivileged containers receive broad host-level access, bypass many isolation controls, and run with an unconfined seccomp profile. Adding a single capability through capabilities.add is materially narrower than enabling privileged.
Do not use privileged mode as a routine workaround for permission errors. Prefer explicit UIDs, volume mounts, and targeted capabilities.
Apply RuntimeDefault Seccomp
Seccomp restricts which syscalls a container may invoke. Save sc-seccomp.yaml with a Pod-level profile:
apiVersion: v1
kind: Pod
metadata:
name: sc-seccomp
namespace: secctx-lab
spec:
restartPolicy: Never
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]Apply the manifest:
kubectl apply -f sc-seccomp.yamlkubectl wait --for=condition=Ready pod/sc-seccomp -n secctx-lab --timeout=90sRuntimeDefault applies the profile supplied by the container runtime (containerd on this lab cluster). A container-level seccompProfile overrides the Pod-level setting for that container. Privileged containers run unconfined regardless of this field.
Confirm the field on the live object:
kubectl get pod -n secctx-lab sc-seccomp -o yaml | grep -A2 seccompProfileSample output:
seccompProfile:
type: RuntimeDefaultCustom Localhost profile authoring is out of scope for this article.
Build and Verify a Combined Manifest
After walking each field separately, save sc-hardened.yaml:
apiVersion: v1
kind: Pod
metadata:
name: sc-hardened
namespace: secctx-lab
spec:
restartPolicy: Never
securityContext:
runAsUser: 1000
runAsGroup: 3000
runAsNonRoot: true
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "id; touch /readonly-test.txt 2>&1; touch /tmp/app-state.txt && ls -ld /data /tmp; sleep 3600"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
- name: data
mountPath: /data
volumes:
- name: tmp
emptyDir: {}
- name: data
emptyDir: {}Capability add and drop lists belong in the capabilities guide, not in this baseline manifest.
Apply the manifest:
kubectl apply -f sc-hardened.yamlkubectl wait --for=condition=Ready pod/sc-hardened -n secctx-lab --timeout=90sCheck identity inside the running container:
kubectl exec -n secctx-lab sc-hardened -- idSample output:
uid=1000 gid=3000 groups=2000,3000Confirm the root filesystem rejects writes while mounted paths accept them:
kubectl logs -n secctx-lab sc-hardened -c appSample output:
uid=1000 gid=3000 groups=2000,3000
touch: /readonly-test.txt: Read-only file system
drwxrwsrwx 2 root 2000 4096 Jul 27 02:12 /data
drwxrwsrwx 2 root 2000 4096 Jul 27 02:12 /tmpVerify effective runtime flags, not only the submitted YAML:
kubectl exec -n secctx-lab sc-hardened -- grep -E '^(NoNewPrivs|Seccomp):' /proc/1/statusSample output:
NoNewPrivs: 1
Seccomp: 2NoNewPrivs: 1 confirms that privilege escalation was disabled for the initial process. Seccomp: 2 means seccomp filter mode is active. Privileged containers cannot use a requested seccomp profile and instead run unconfined.
Inspect the applied SecurityContext on the Pod object:
kubectl get pod -n secctx-lab sc-hardened -o yaml | grep -A15 "securityContext:"When a Pod fails during startup, read Events for runAsNonRoot, seccomp, or permission errors:
kubectl events -n secctx-lab --for pod/sc-hardenedTroubleshoot Common Failures
| Symptom | Likely cause | Fix |
|---|---|---|
CreateContainerConfigError with runAsNonRoot |
Image defaults to root | Set runAsUser to a non-zero UID or use a non-root image |
| Container exits immediately as non-root | Entrypoint not executable by chosen UID | Pick a UID the image supports or adjust image permissions |
Permission denied on a volume |
Missing fsGroup or unsupported volume driver |
Add Pod fsGroup when the driver supports it; verify with ls -ld on the mount |
App fails after readOnlyRootFilesystem |
Writes still target / or /var |
Mount emptyDir or a PVC at each writable path |
| Writable path still read-only | Mount missing or wrong mountPath |
Add the volume and volumeMount before enabling read-only root |
| Seccomp or privilege-escalation restrictions appear ineffective | Container is privileged or has CAP_SYS_ADMIN |
Remove privileged mode or the capability; privileged containers run seccomp unconfined and cannot effectively disable privilege escalation |
| App syscall errors after seccomp | RuntimeDefault blocks a required syscall |
Test with runtime logs; custom profiles are a separate topic |
What's Next
- Linux Capabilities in Kubernetes
- Kubernetes Pod Security Standards and Admission
- Kubernetes Secrets with Examples
References
- Configure a Security Context for a Pod or Container — Kubernetes documentation
- Seccomp — RuntimeDefault and profile types
- Linux capabilities — capability semantics referenced by container security
Summary
SecurityContext separates Pod-wide defaults from per-container overrides. Pod securityContext carries fsGroup, supplemental groups, and a default seccomp profile. Container securityContext carries readOnlyRootFilesystem, allowPrivilegeEscalation, and privileged.
You walked through each layer in secctx-lab: numeric runAsUser and runAsGroup, runAsNonRoot rejection of root images, fsGroup for shared volume access, read-only root with writable emptyDir mounts, and RuntimeDefault seccomp. The combined sc-hardened Pod shows how those fields fit together without stuffing every security knob into one unreadable manifest.
The main pitfalls are choosing a UID the image cannot run, forgetting writable mounts when the root filesystem is read-only, and assuming fsGroup fixes every storage backend. For Linux capability add and drop rules, continue with the capabilities guide.

