Kubernetes Pod Security Standards: Restricted and Baseline

Pod Security Standards (PSS) replace the deprecated PodSecurityPolicy (PSP) that was removed in Kubernetes 1.25. PSS defines three human-readable security profiles — Privileged, Baseline, and Restricted — that cover the most important container security settings without requiring custom admission webhook code. The built-in Pod Security Admission controller enforces these profiles at the namespace level, making it straightforward to harden workloads across an entire cluster.

The Three Pod Security Profiles

Kubernetes defines three Pod Security Standard profiles, ordered from most permissive to most restrictive:

  • Privileged: Completely unrestricted. Allows hostPID, hostNetwork, privileged containers, and all capabilities. Appropriate only for trusted system-level workloads like CNI plugins and monitoring agents that must access host resources.
  • Baseline: Prevents the most common privilege escalation paths while remaining compatible with most containerised applications. Blocks hostNetwork, hostPID, privileged containers, and dangerous capabilities, but allows running as root and using host ports above 1024.
  • Restricted: The most secure profile. Enforces all Baseline controls plus: running as non-root, dropping all Linux capabilities, disabling privilege escalation, using a seccomp profile, and requiring a read-only root filesystem. Most production application pods should target this level.
Progressive adoption: Start by applying Baseline in enforce mode across all application namespaces, which catches the most dangerous misconfigurations. Then incrementally move namespaces to Restricted as applications are updated to meet the stricter requirements.

Pod Security Admission Controller

Pod Security Admission (PSA) is a built-in admission controller that enforces PSS profiles. It operates in three modes per namespace:

  • enforce: Pods that violate the policy are rejected. The pod is not created.
  • audit: Violations are recorded in the audit log but pods are still created. Use this to identify violations without breaking workloads.
  • warn: Violations produce a warning returned to the API client (visible in kubectl output) but pods are still created.

These modes are independent — you can enforce Baseline while auditing and warning on Restricted violations simultaneously:

apiVersion: v1
kind: Namespace
metadata:
  name: team-payments
  labels:
    # Enforce Baseline — reject non-compliant pods
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/enforce-version: v1.30
    # Audit Restricted — log violations without rejecting
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: v1.30
    # Warn Restricted — show warnings in kubectl output
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.30

The -version labels pin the policy version to a specific Kubernetes release, ensuring that cluster upgrades don't automatically tighten policies and break existing workloads.

Applying Profiles to Namespaces

Apply profiles to all existing namespaces at once using a label selector, or add labels during namespace creation with a Helm template or GitOps manifests.

# Apply Baseline enforcement to all non-system namespaces
for ns in $(kubectl get namespaces -o jsonpath='{.items[*].metadata.name}'); do
  case $ns in
    kube-system|kube-public|kube-node-lease)
      echo "Skipping system namespace: $ns"
      ;;
    *)
      kubectl label namespace "$ns" \
        pod-security.kubernetes.io/enforce=baseline \
        pod-security.kubernetes.io/enforce-version=v1.30 \
        --overwrite
      echo "Labelled: $ns"
      ;;
  esac
done

# Dry-run: simulate enforcing Restricted on a namespace to see violations
kubectl label namespace team-payments \
  pod-security.kubernetes.io/enforce=restricted \
  --dry-run=server --overwrite
Dry-run simulation: The dry-run command above checks all existing pods in the namespace against the target policy and reports violations without making any changes. Run this before enforcing Restricted on any namespace to identify which pods need to be updated.

Restricted Profile Requirements

A pod must satisfy all of these requirements to pass the Restricted profile:

apiVersion: v1
kind: Pod
metadata:
  name: restricted-compliant-pod
spec:
  securityContext:
    runAsNonRoot: true          # Pod must run as non-root
    seccompProfile:
      type: RuntimeDefault      # Must have seccomp profile
  containers:
    - name: app
      image: myapp:latest
      securityContext:
        allowPrivilegeEscalation: false   # REQUIRED by Restricted
        runAsNonRoot: true
        runAsUser: 1000
        readOnlyRootFilesystem: true       # Strongly recommended
        capabilities:
          drop:
            - ALL                          # Drop ALL Linux capabilities
          add: []                          # Add back only what you need
        seccompProfile:
          type: RuntimeDefault
      # Mount writable volumes for app data (not filesystem)
      volumeMounts:
        - name: tmp
          mountPath: /tmp
        - name: cache
          mountPath: /app/cache
  volumes:
    - name: tmp
      emptyDir: {}
    - name: cache
      emptyDir: {}

Security Contexts: Key Settings Explained

Understanding each security context field helps you write compliant manifests and explain rejections to your development team:

  • runAsNonRoot: true: Kubernetes rejects the pod if the container image's USER instruction is root (UID 0). The image must be built with a non-root user.
  • allowPrivilegeEscalation: false: Prevents a process in the container from gaining more privileges than its parent (e.g., via setuid binaries). Always set this to false.
  • readOnlyRootFilesystem: true: The container's root filesystem is mounted read-only. The application must write to mounted volumes only. This prevents malware from writing to the container filesystem.
  • capabilities.drop: [ALL]: Drops all Linux capabilities. Most applications do not need any capabilities. Add back only what is strictly required (e.g., NET_BIND_SERVICE to bind ports below 1024).
  • seccompProfile.type: RuntimeDefault: Applies the container runtime's default seccomp filter, which blocks ~300 rarely-used and dangerous syscalls.
# Check which pods in a namespace violate Restricted
kubectl get pods -n team-payments -o json | \
  jq '.items[] | select(.spec.containers[].securityContext.allowPrivilegeEscalation != false)
    | .metadata.name'

Migrating from PodSecurityPolicy

PodSecurityPolicy was removed in Kubernetes 1.25. If you are running a cluster older than 1.25 with PSP enabled, you must migrate before upgrading. The migration path is:

  1. Map each PodSecurityPolicy to the closest PSS profile (most restrictive PSPs → Restricted; permissive PSPs → Baseline)
  2. Add the equivalent namespace labels in warn mode alongside existing PSPs
  3. Fix any violations surfaced by the warnings
  4. Switch namespace labels from warn to enforce
  5. Disable PSP in the API server admission plugins before upgrading to 1.25
# List all PodSecurityPolicies currently in use
kubectl get psp

# Find which service accounts are bound to each PSP
kubectl get clusterrolebindings -o json | \
  jq '.items[] | select(.roleRef.kind == "ClusterRole") |
    {binding: .metadata.name, role: .roleRef.name, subjects: .subjects}'

Exemptions for System Workloads

Some legitimate system workloads require privileged access: monitoring agents (Falco, Datadog), CNI plugins, log collectors (Fluentbit DaemonSet), and CSI drivers. These must be exempted from Restricted or Baseline enforcement.

# kube-apiserver flag — configure PSA exemptions
--admission-control-config-file=/etc/kubernetes/admission-config.yaml
# /etc/kubernetes/admission-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
  - name: PodSecurity
    configuration:
      apiVersion: pod-security.admission.config.k8s.io/v1
      kind: PodSecurityConfiguration
      defaults:
        enforce: "baseline"
        enforce-version: "latest"
        audit: "restricted"
        audit-version: "latest"
        warn: "restricted"
        warn-version: "latest"
      exemptions:
        # Exempt system namespaces (already privileged)
        namespaces:
          - kube-system
          - monitoring
          - falco
        # Exempt specific runtime classes (e.g., gVisor)
        runtimeClasses: []
        # Exempt specific usernames (e.g., cluster-admin)
        usernames: []

OPA Gatekeeper as an Alternative

For organisations that need more granular policy control than PSS provides, OPA Gatekeeper is the most popular policy-as-code solution for Kubernetes. Gatekeeper lets you write custom Rego policies for any aspect of a resource manifest.

# Gatekeeper ConstraintTemplate: require non-root containers
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequirenonroot
spec:
  crd:
    spec:
      names:
        kind: K8sRequireNonRoot
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequirenonroot
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not container.securityContext.runAsNonRoot
          msg := sprintf("Container %v must set runAsNonRoot: true", [container.name])
        }

---
# Apply the constraint to all namespaces except system ones
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireNonRoot
metadata:
  name: require-non-root-containers
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    excludedNamespaces:
      - kube-system
      - monitoring
PSS vs Gatekeeper: Use Pod Security Standards for most clusters — they require no additional components and cover the most critical security controls. Add Gatekeeper when you need policies beyond what PSS provides: image registry allowlists, required labels, specific port restrictions, or custom business rules.