Kubernetes SecurityContext with Practical Examples

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:

bash
kubectl create namespace secctx-lab --dry-run=client -o yaml | kubectl apply -f -

Sample output on the first run:

output
namespace/secctx-lab created

A later run typically prints:

output
namespace/secctx-lab unchanged

Pod-Level vs Container-Level SecurityContext

Kubernetes exposes two placement points in the Pod spec:

yaml
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:

bash
kubectl run sc-default -n secctx-lab --image=busybox:1.36 --restart=Never --command -- sleep 3600

Wait for the Pod to become Ready before inspecting it:

bash
kubectl wait --for=condition=Ready pod/sc-default -n secctx-lab --timeout=90s

Check the process identity:

bash
kubectl exec -n secctx-lab sc-default -- id

Sample output:

output
uid=0(root) gid=0(root) groups=0(root),10(wheel)

Save sc-uid-gid.yaml with explicit numeric IDs on the container:

yaml
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: 3000

Apply the manifest:

bash
kubectl apply -f sc-uid-gid.yaml
bash
kubectl wait --for=condition=Ready pod/sc-uid-gid -n secctx-lab --timeout=90s
bash
kubectl exec -n secctx-lab sc-uid-gid -- id

Sample output:

output
uid=1000 gid=3000 groups=3000

Manifest 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:

yaml
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: true

Apply the manifest:

bash
kubectl apply -f sc-nonroot-fail.yaml

Wait for the actual waiting reason:

bash
kubectl wait pod/sc-nonroot-fail -n secctx-lab --for=jsonpath='{.status.containerStatuses[0].state.waiting.reason}'=CreateContainerConfigError --timeout=90s

Print the recorded message:

bash
kubectl get pod sc-nonroot-fail -n secctx-lab -o jsonpath='{.status.containerStatuses[0].state.waiting.message}{"\n"}'

Sample output:

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:

yaml
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: 1000

Apply the manifest:

bash
kubectl apply -f sc-nonroot-ok.yaml
bash
kubectl wait --for=condition=Ready pod/sc-nonroot-ok -n secctx-lab --timeout=90s
bash
kubectl exec -n secctx-lab sc-nonroot-ok -- id

Sample output:

output
uid=1000 gid=1000 groups=1000

The 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:

yaml
spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000
    supplementalGroupsPolicy: Strict

Strict 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:

yaml
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:

bash
kubectl apply -f sc-fsgroup-demo.yaml
bash
kubectl wait --for=condition=Ready pod/sc-fsgroup-demo -n secctx-lab --timeout=90s
bash
kubectl logs -n secctx-lab sc-fsgroup-demo -c app

Sample output:

output
drwxr-xr-x    2 root     root          4096 Jul 27 02:11 /data
touch: /data/nofsg.txt: Permission denied

Apply fsGroup

Save sc-fsgroup-after.yaml with Pod-level fsGroup:

yaml
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:

bash
kubectl apply -f sc-fsgroup-after.yaml
bash
kubectl wait --for=condition=Ready pod/sc-fsgroup-after -n secctx-lab --timeout=90s

On a fresh emptyDir mount with fsGroup: 2000, the directory shows group 2000 with the setgid bit, and UID 1000 receives supplemental group 2000:

bash
kubectl logs -n secctx-lab sc-fsgroup-after -c app

Sample output:

output
drwxrwsrwx    2 root     2000          4096 Jul 27 02:11 /data

The application writes successfully:

bash
kubectl exec -n secctx-lab sc-fsgroup-after -- ls -la /data

Sample output:

output
-rw-r--r--    1 1000     2000             0 Jul 27 02:11 test.txt

Understand 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:

yaml
spec:
  securityContext:
    fsGroup: 2000
    fsGroupChangePolicy: OnRootMismatch

Keep 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:

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: true

Apply the manifest:

bash
kubectl apply -f sc-readonly-fail.yaml
bash
kubectl wait --for=condition=Ready pod/sc-readonly-fail -n secctx-lab --timeout=90s
bash
kubectl logs -n secctx-lab sc-readonly-fail -c app

Sample output:

output
touch: /readonly-test.txt: Read-only file system

Applications still need paths for caches, temp files, or runtime state. Save sc-readonly-ok.yaml with an emptyDir at /tmp:

yaml
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:

bash
kubectl apply -f sc-readonly-ok.yaml
bash
kubectl wait --for=condition=Ready pod/sc-readonly-ok -n secctx-lab --timeout=90s
bash
kubectl logs -n secctx-lab sc-readonly-ok -c app

Sample output:

output
touch: /readonly-test.txt: Read-only file system
-rw-r--r--    1 root     root             0 Jul 27 02:12 /tmp/writable.txt

The 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.

yaml
securityContext:
  allowPrivilegeEscalation: false

This 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:

yaml
securityContext:
  privileged: true

Privileged 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:

yaml
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:

bash
kubectl apply -f sc-seccomp.yaml
bash
kubectl wait --for=condition=Ready pod/sc-seccomp -n secctx-lab --timeout=90s

RuntimeDefault 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:

bash
kubectl get pod -n secctx-lab sc-seccomp -o yaml | grep -A2 seccompProfile

Sample output:

output
seccompProfile:
      type: RuntimeDefault

Custom 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:

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:

bash
kubectl apply -f sc-hardened.yaml
bash
kubectl wait --for=condition=Ready pod/sc-hardened -n secctx-lab --timeout=90s

Check identity inside the running container:

bash
kubectl exec -n secctx-lab sc-hardened -- id

Sample output:

output
uid=1000 gid=3000 groups=2000,3000

Confirm the root filesystem rejects writes while mounted paths accept them:

bash
kubectl logs -n secctx-lab sc-hardened -c app

Sample output:

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 /tmp

Verify effective runtime flags, not only the submitted YAML:

bash
kubectl exec -n secctx-lab sc-hardened -- grep -E '^(NoNewPrivs|Seccomp):' /proc/1/status

Sample output:

output
NoNewPrivs:	1
Seccomp:	2

NoNewPrivs: 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:

bash
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:

bash
kubectl events -n secctx-lab --for pod/sc-hardened

Troubleshoot 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


References


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.


Frequently Asked Questions

1. What is SecurityContext in Kubernetes?

SecurityContext defines privilege and access settings for a Pod or container, including the Linux user and group, filesystem permissions on volumes, read-only root filesystem, privilege escalation, privileged mode, and seccomp profiles.

2. What is the difference between Pod and container SecurityContext?

Pod securityContext sets defaults for every container in the Pod, such as fsGroup and pod-wide seccompProfile. Container securityContext overrides matching Pod fields for that container only, and holds container-only settings such as readOnlyRootFilesystem and privileged.

3. What does runAsNonRoot do?

When runAsNonRoot is true, Kubernetes refuses to start a container whose effective UID would be zero. Pair it with an explicit non-zero runAsUser or a non-root image default. The check validates the UID, not the primary GID.

4. What is fsGroup used for?

fsGroup sets a supplemental group ID for volume ownership. Kubernetes adjusts mounted volume permissions so processes in the Pod can access shared storage when the volume type supports fsGroup changes.

5. Does readOnlyRootFilesystem block all writes?

It makes the container image root filesystem read-only. The process can still write to writable mounts such as emptyDir or a read-write PersistentVolumeClaim. ConfigMap, Secret, and other projected configuration volumes remain read-only.

6. What is the difference between privileged and allowPrivilegeEscalation?

privileged: true grants broad privileges and forces the container to use an unconfined seccomp profile. allowPrivilegeEscalation controls Linux no_new_privs for non-privileged containers, but it is effectively true for privileged containers and containers with CAP_SYS_ADMIN.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)