Skip to main content

Set security contexts

Inspect 1.36

Add the per-pod and per-container settings that tell the kernel what a container may do: run as non-root, drop capabilities, block privilege escalation, and make the root filesystem read-only. A security context is a block in the pod or container spec that tells the Linux kernel what the container is allowed to do (it does not modify the container image). The platform sets none for you; you add them to your own workloads.

Coming from the workload side, this is the per-pod step in the .

Warning

Platform components need elevated host access to function. Never apply restrictive Pod Security Standards to platform namespaces.

Pod-level and container-level settings#

You can place a securityContext block in two places in a pod spec:

Pod level (spec.securityContext): applies to all containers in the pod. Use this for settings that must be the same across all containers, such as the user and group IDs or the seccomp profile.

Container level (spec.containers[*].securityContext): applies to one container only and overrides the pod-level field for the same setting. Use this for per-container settings: privilege escalation, capabilities, and the read-only filesystem flag.

Some fields exist at only one level. allowPrivilegeEscalation, capabilities, and readOnlyRootFilesystem are container-only. seccompProfile works at either level; set it at the pod level to cover all containers.

Run as a non-root user#

Most images default to root (UID 0). Running as root gives a compromised container full control of the container filesystem.

runAsNonRoot: true tells Kubernetes to reject the container at startup if the image would run as root. Set it at the pod level:

yaml
		spec:
  securityContext:
    runAsNonRoot: true
	

Use runAsUser and runAsGroup to set a specific numeric user ID and group ID. This is useful when the image does not declare its own user, or when a mounted volume requires a specific owner:

yaml
		spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 1000
	
Tip

If an image must run as root and you cannot change it, use a user namespace (hostUsers: false) instead. A user namespace maps container root to an unprivileged host user, so a container escape does not land as root on the node. See .

Block privilege escalation#

Privilege escalation lets a process inside a container gain more permissions after it starts, for example by calling a setuid binary. Almost no application needs this.

Set allowPrivilegeEscalation: false at the container level:

yaml
		securityContext:
  allowPrivilegeEscalation: false
	

Linux capabilities#

Linux capabilities are fine-grained kernel privileges that control specific operations, such as binding a port below 1024, loading a kernel module, or changing file ownership. A container can run as non-root and still hold dangerous capabilities, or run as root with nearly all of them removed.

Drop all capabilities, then add back only what a specific container needs:

yaml
		securityContext:
  capabilities:
    drop:
      - ALL
    add:
      - NET_BIND_SERVICE # only if this container must bind a port below 1024
	

The containerd configuration lets containers bind low ports (below 1024) without extra privileges. So most containers do not need NET_BIND_SERVICE.

Tip

Start by dropping ALL with nothing in add. Run the container and check what fails. Add capabilities back one at a time when a specific error confirms they are needed.

Read-only root filesystem#

readOnlyRootFilesystem: true mounts the container's own filesystem as read-only. The container can still write to volumes you mount explicitly. It cannot write to directories inside the image itself.

yaml
		securityContext:
  readOnlyRootFilesystem: true
	

This stops a compromised container from modifying image files at runtime. It also makes all writable paths explicit in the pod spec, which is easier to audit.

Some images write to paths like /tmp on startup. Mount an emptyDir volume there:

yaml
		volumes:
  - name: tmp
    emptyDir: {}
containers:
  - name: app
    volumeMounts:
      - name: tmp
        mountPath: /tmp
    securityContext:
      readOnlyRootFilesystem: true
	

Seccomp#

Seccomp (secure computing mode) is a Linux kernel feature that limits which system calls a container can make. A system call is a request from a process to the kernel, for example to open a file or create a network socket. Without a seccomp profile, a container can call any system call the kernel supports.

Every container runs under the RuntimeDefault seccomp profile by default. The kubelet sets seccompDefault: true, so a pod that names no profile gets the runtime's profile (it blocks around 44 dangerous syscalls) instead of running unconfined. Declaring it makes the node default explicit and portable:

yaml
		spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
	

Set this at the pod level so all containers in the pod get it. The restricted Pod Security Standard requires it. A workload that needs a blocked syscall opts out with seccompProfile.type: Unconfined, which baseline and restricted reject.

AppArmor#

AppArmor is mandatory access control: the kernel enforces a per-program policy that even a root process cannot bypass. Like the default seccomp profile, it is already on. Every non-privileged container runs under the runtime's enforcing profile without any field set.

The appArmorProfile field in the security context controls it, GA since Kubernetes 1.31. Its types: RuntimeDefault (the runtime's profile, same as leaving the field unset on this platform), Localhost (a custom profile loaded on the node), and Unconfined (no confinement).

yaml
		spec:
  securityContext:
    appArmorProfile:
      type: RuntimeDefault
	

See for what the default profile blocks and how to load custom profiles.

A complete example#

This Deployment satisfies the restricted Pod Security Standard:

yaml
		apiVersion: apps/v1
kind: Deployment
metadata:
  name: secure-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: secure-app
  template:
    metadata:
      labels:
        app: secure-app
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 1000
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: app
          image: your/image:tag
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop:
                - ALL
	

Common problems when applying this template:

  • The image runs as root and has no non-root user configured. Add a user in the Dockerfile, or use a user namespace.
  • The container writes to a directory inside the image at startup. Mount an emptyDir volume at that path.
  • The container needs a specific capability. Add it to capabilities.add and document the reason.

Enforce these across a namespace#

Setting these fields on every pod by hand does not scale, and a single manifest that forgets them passes without notice. Pod Security Standards move the check to the namespace: label it once, and Kubernetes rejects any pod that does not meet the level. The restricted level requires exactly the context this page builds (non-root, dropped capabilities, seccomp RuntimeDefault). See for the levels and an audit-first rollout that will not break running workloads.

Add a user namespace#

A security context tells the kernel what a container may do. A user namespace changes who the container is on the host: set hostUsers: false and container root (UID 0) maps to an unprivileged host user, so an escape from a root container lands as "nobody" instead of host root. This is the answer when an image must run as root and you cannot change it. Pair it with the context above for the strongest per-pod isolation you can get without a VM. How user namespaces work is ; the how-to is .

What the platform sets#

Two things are on already: the runtime applies its enforcing to every non-privileged container, and the kubelet defaults every container to the RuntimeDefault seccomp profile. Everything else here is your responsibility.

Every namespace runs at privileged until you label it. Platform components need privileges that the restricted level forbids. Applying a cluster-wide default that blocked privileged pods would break those platform components. You apply Pod Security Standards to your own namespaces, and you set security contexts on your own pods.