Pod Security Standards#

Kubernetes Pod Security Standards define three security profiles that control what pods are allowed to do. Pod Security Admission (PSA) enforces these standards at the namespace level. This is the replacement for PodSecurityPolicy, which was removed in Kubernetes 1.25.

The Three Levels#

Privileged – Unrestricted. No security controls applied. Used for system-level workloads like CNI plugins, storage drivers, and logging agents that genuinely need host access.

Baseline – Prevents known privilege escalations. Blocks hostNetwork, hostPID, hostIPC, privileged containers, and most host path mounts. Allows most workloads to run without modification.

Restricted – Maximum security. Requires running as non-root, drops all capabilities, enforces read-only root filesystem, requires seccomp profile, and blocks privilege escalation. This is the target for all application workloads.

Pod Security Admission#

PSA is built into Kubernetes 1.23+ and enabled by default. It works through namespace labels. There are three modes per security level:

  • enforce – Rejects pods that violate the standard.
  • audit – Allows the pod but logs the violation in the audit log.
  • warn – Allows the pod but returns a warning to the user.

Applying Standards to a Namespace#

apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

This enforces the restricted standard in the production namespace. Any pod that does not meet the restricted requirements is rejected on creation.

For a staged rollout, start with audit and warn before enforcing:

metadata:
  labels:
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

This enforces baseline (blocking the most dangerous configurations) while logging and warning about restricted violations. Once all workloads pass restricted, switch enforce to restricted.

Exemptions#

Some namespaces need privileged access. Label them explicitly:

apiVersion: v1
kind: Namespace
metadata:
  name: kube-system
  labels:
    pod-security.kubernetes.io/enforce: privileged

Keep the list of privileged namespaces as small as possible: kube-system, ingress-nginx, istio-system, and similar infrastructure namespaces.

Writing Secure Pod Specs#

A pod that passes the restricted standard looks like this:

apiVersion: v1
kind: Pod
metadata:
  name: secure-app
  namespace: production
spec:
  automountServiceAccountToken: false
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 1000
    fsGroup: 1000
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: myapp:1.0.0
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop:
            - ALL
        seccompProfile:
          type: RuntimeDefault
      volumeMounts:
        - name: tmp
          mountPath: /tmp
  volumes:
    - name: tmp
      emptyDir: {}

Key settings explained:

  • runAsNonRoot: true – The container must run as a non-root user. If the container image has USER root, the pod will fail to start.
  • readOnlyRootFilesystem: true – The container filesystem is read-only. Applications that need to write temporary files must use an emptyDir volume mounted at the write path.
  • allowPrivilegeEscalation: false – Prevents a process from gaining more privileges than its parent. This blocks setuid binaries and other escalation vectors.
  • capabilities.drop: ALL – Removes all Linux capabilities. Most applications do not need any. If your application needs to bind to a port below 1024, add NET_BIND_SERVICE back.
  • seccompProfile: RuntimeDefault – Applies the container runtime’s default seccomp profile, which blocks dangerous system calls like unshare, mount, and reboot.
  • automountServiceAccountToken: false – Prevents the service account token from being mounted in the pod. Most application pods do not need Kubernetes API access.

Common Violations and Fixes#

“container must not set runAsUser to 0” – The container image runs as root. Add USER 1000 to the Dockerfile, or set runAsUser: 1000 in the pod spec.

“container must set readOnlyRootFilesystem to true” – Add readOnlyRootFilesystem: true and mount emptyDir volumes for write paths like /tmp, /var/cache, or /var/run.

“container must drop ALL capabilities” – Add capabilities: {drop: [ALL]}. If the application needs specific capabilities, add only the minimum required:

securityContext:
  capabilities:
    drop:
      - ALL
    add:
      - NET_BIND_SERVICE

“container must set seccompProfile” – Add seccompProfile: {type: RuntimeDefault} to the container’s securityContext.

Migrating from PodSecurityPolicy#

PodSecurityPolicy (PSP) was removed in Kubernetes 1.25. Migration to PSA:

  1. Audit existing PSPs to understand what they enforce.
  2. Label namespaces with audit and warn for the equivalent PSA level.
  3. Review audit logs and warnings to identify non-compliant workloads.
  4. Fix workload specs to meet the target standard.
  5. Switch namespace labels to enforce.
  6. Remove PSP resources and the PSP admission controller flag.

OPA Gatekeeper and Kyverno#

PSA covers the common cases but does not support custom policies. For requirements like “all images must come from our private registry” or “all pods must have resource limits,” use a policy engine.

Kyverno#

Kyverno uses Kubernetes-native YAML for policies:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-image-registry
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-registry
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "Images must come from registry.example.com"
        pattern:
          spec:
            containers:
              - image: "registry.example.com/*"

OPA Gatekeeper#

Gatekeeper uses Rego policies, which are more powerful but harder to write:

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: require-team-label
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]
  parameters:
    labels:
      - key: team
        allowedRegex: "^[a-z]+-[a-z]+$"

Choose Kyverno for straightforward policies that fit YAML patterns. Choose Gatekeeper when you need complex logic, external data, or mutation policies that Kyverno cannot express.

Both tools complement PSA rather than replacing it. Use PSA for the baseline security posture and a policy engine for organization-specific requirements.