Pod Security

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

A container is a process on a shared kernel, so what that process is allowed to do — which user it runs as, which Linux capabilities it holds, which system calls it may make — decides how much damage a compromised application can cause. Kubernetes exposes those controls through the securityContext field and enforces cluster-wide minimums through Pod Security Admission. This page is written against Kubernetes 1.37; see the section index for the version baseline. Who may call the API is covered separately in Authentication, Authorization and RBAC.

securityContext

securityContext exists at two levels: on the Pod (spec.securityContext, applying to all containers and volumes) and on each container (spec.containers[].securityContext, overriding the Pod for that container).

Field Effect

runAsNonRoot: true

The kubelet refuses to start a container whose image would run as UID 0.

runAsUser, runAsGroup

Numeric UID/GID of the container process. Use a non-zero value that matches the image.

fsGroup

Group ownership applied to supported volumes so a non-root process can write to them (Pod level). Together with fsGroupChangePolicy: OnRootMismatch it avoids slow recursive re-ownership of large volumes.

supplementalGroups

Extra group IDs for the process.

readOnlyRootFilesystem: true

The image filesystem is read-only; give the application writable emptyDir mounts for /tmp and similar (see Volumes and Ephemeral Storage).

allowPrivilegeEscalation: false

Sets no_new_privs, so setuid binaries cannot gain privileges.

capabilities: { drop: ["ALL"] }

Removes every Linux capability; add back only specific ones (for example NET_BIND_SERVICE).

privileged

Gives near-host access. Avoid.

seccompProfile: { type: RuntimeDefault }

Applies the container runtime’s default system-call filter.

appArmorProfile

Selects an AppArmor profile (RuntimeDefault, Localhost, Unconfined); a first-class field since 1.30.

seLinuxOptions

SELinux label (level, role, type, user) on hosts that use SELinux.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: catalog-api
  namespace: bookshelf
spec:
  replicas: 2
  selector:
    matchLabels:
      app: catalog-api
  template:
    metadata:
      labels:
        app: catalog-api
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        fsGroup: 10001
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: catalog-api
          image: ghcr.io/example/bookshelf-catalog-api:1.0.0
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir: {}
kubectl exec deploy/catalog-api -n bookshelf -- id
kubectl exec deploy/catalog-api -n bookshelf -- touch /etc/probe   # fails on a read-only root
uid=10001 gid=10001 groups=10001
touch: /etc/probe: Read-only file system

Pod Security Standards

The Pod Security Standards (PSS) define three cumulative profiles:

Profile Meaning

privileged

Unrestricted; for system and infrastructure workloads such as CNI or storage drivers.

baseline

Blocks known privilege escalations: no hostNetwork, hostPID, hostIPC, host paths, privileged containers, or added capabilities beyond a small default set. Suitable for most existing applications.

restricted

Current hardening best practice: everything in baseline plus non-root, no privilege escalation, capabilities dropped, seccompProfile of RuntimeDefault or Localhost, and limited volume types.

Pod Security Admission

Pod Security Admission (PSA) is a built-in admission controller, enabled by default, that applies a PSS profile to a namespace according to labels. Three modes can be set independently, each with an optional -version label to pin the policy to a Kubernetes release (latest by default):

Mode label On violation

pod-security.kubernetes.io/enforce

Reject the Pod.

pod-security.kubernetes.io/audit

Allow, but record an audit-log annotation.

pod-security.kubernetes.io/warn

Allow, but return a warning to the client.

The standard rollout pattern is to warn and audit at the target level first, fix workloads, then enforce.

apiVersion: v1
kind: Namespace
metadata:
  name: bookshelf
  labels:
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/enforce-version: v1.37
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: latest
    pod-security.kubernetes.io/audit: restricted
# Dry-run the effect of enforcing "restricted" on an existing namespace
kubectl label --dry-run=server --overwrite ns bookshelf \
  pod-security.kubernetes.io/enforce=restricted
Warning: existing pods in namespace "bookshelf" violate the new PodSecurity enforce level "restricted:latest"
Warning: postgres-0: runAsNonRoot != true, seccompProfile
namespace/bookshelf labeled (server dry run)

PSA only checks Pod specs (workload controllers are checked through their Pod templates via warnings) and applies per namespace; exemptions and a default per-cluster configuration go in an AdmissionConfiguration file. To try the flow on kind, the official namespace-level and cluster-level tutorials walk through creating a cluster with PSA configured and deploying compliant and non-compliant Pods. For rules PSA cannot express, see Admission Control and Policy.

PodSecurityPolicy is gone

PodSecurityPolicy (PSP) was deprecated in 1.21 and removed in 1.25; any tutorial or book that uses policy/v1beta1 PSPs is obsolete. Migrate by mapping each PSP to the closest PSS level, enabling PSA in warn/audit mode, fixing workloads, then switching to enforce; use ValidatingAdmissionPolicy or Kyverno for policies PSS cannot express.

User namespaces

With hostUsers: false the Pod runs in its own Linux user namespace: UID 0 inside the container maps to an unprivileged high UID on the host, so a container breakout yields no host root. User-namespace support reached GA in Kubernetes 1.36 (beta and on by default from 1.33). It needs a supported runtime (recent containerd or CRI-O), a kernel with ID-mapped mounts, and does not combine with hostNetwork, hostPID or hostIPC (host network support was being added under a separate gate; verify before relying on it). Pods using it can satisfy parts of the restricted profile’s non-root requirement, which is why the standards treat them specially.

apiVersion: v1
kind: Pod
metadata:
  name: importer
  namespace: bookshelf
spec:
  hostUsers: false
  containers:
    - name: importer
      image: ghcr.io/example/bookshelf-importer:1.0.0

Seccomp and the wider checklist

Enabling seccompProfile: RuntimeDefault on every Pod blocks several dozen rarely needed system calls with very little compatibility risk; the seccomp tutorial shows custom Localhost profiles on kind. For a production review, work through the official security checklist (API server and etcd hardening, RBAC, network policies, audit logging, node configuration) and the application security checklist (secrets, image provenance, least privilege). Related controls elsewhere in this section: Network Policies for traffic isolation, Secrets for credentials, and RuntimeClass sandboxes in Dynamic Resource Allocation and Devices.

Image supply chain

A hardened Pod still runs whatever image it is given. Pin images by digest, scan them, sign and verify them, build from minimal bases, and run as non-root by construction; the Docker pages cover this in depth (see Image Best Practices and Security). Admission policy can then require trusted registries or signatures cluster-wide. The imagePullPolicy and imagePullSecrets mechanics are in Pods.