🎉 DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
All Articles

What Is Pod Security Admission in Kubernetes? (2026)

Pod Security Admission replaced PodSecurityPolicy in Kubernetes 1.25. Here's what it does, how the three security levels work, and how to enforce it in your cluster.

DevOpsBoysApr 30, 20263 min read
Share:Tweet

Pod Security Admission (PSA) is Kubernetes' built-in way to control what security settings pods can use. It replaced the deprecated PodSecurityPolicy in Kubernetes 1.25.


What Problem Does It Solve?

Without pod security controls, any workload in your cluster can run as root, mount the host filesystem, or use host networking — all major security risks.

PSA lets you set cluster-wide rules like:

  • "No pod in this namespace can run as root"
  • "No pod can use privileged mode"
  • "All pods must have a non-root user and read-only filesystem"

The Three Security Levels

PSA has three levels, from least to most restrictive:

Privileged — no restrictions. Everything is allowed. Use this only for system namespaces (kube-system, gpu-operator, etc.).

Baseline — prevents known privilege escalations. Blocks:

  • Privileged containers
  • Host namespaces (hostPID, hostIPC, hostNetwork)
  • Most HostPath volume mounts
  • Running as root with certain capabilities

Good for general-purpose workloads.

Restricted — follows the hardening best practices. On top of Baseline, also requires:

  • Running as non-root user
  • Read-only root filesystem
  • No privilege escalation
  • Dropped all capabilities (only specific ones can be added back)

Good for production workloads handling sensitive data.


The Three Enforcement Modes

For each level, you can set three modes:

enforce — pods that violate the policy are rejected.

audit — violations are logged in the audit log but pods still run. Good for testing.

warn — violations trigger a warning shown to the user (kubectl apply shows a warning) but pods still run.


How to Apply PSA to a Namespace

PSA is applied via namespace labels:

yaml
apiVersion: v1
kind: Namespace
metadata:
  name: my-app
  labels:
    # Enforce restricted policy
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    
    # Warn about restricted violations (same level, catches edge cases)
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: latest
    
    # Audit restricted violations to the audit log
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: latest

Or label an existing namespace:

bash
kubectl label namespace my-app \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=latest

Test Before Enforcing

Don't jump straight to enforce. Use warn mode first:

bash
# Label namespace with warn only
kubectl label namespace my-app \
  pod-security.kubernetes.io/warn=restricted
 
# Try to deploy your app
kubectl apply -f deployment.yaml
# Warning: would violate "restricted" policy
# → tells you what needs fixing without blocking anything

Fix all warnings, then switch to enforce.


A Compliant Pod (Restricted Level)

Here's what a pod that passes the restricted policy looks like:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: secure-app
  namespace: my-app
spec:
  securityContext:
    runAsNonRoot: true          # must not run as root
    runAsUser: 1000             # specific non-root UID
    runAsGroup: 3000
    fsGroup: 2000
    seccompProfile:
      type: RuntimeDefault      # required for restricted
  containers:
  - name: app
    image: myapp:latest
    securityContext:
      allowPrivilegeEscalation: false   # must be false
      readOnlyRootFilesystem: true       # strongly recommended
      capabilities:
        drop:
          - ALL                 # drop all capabilities
        add:
          - NET_BIND_SERVICE    # only add back what you need

What Breaks With Restricted Policy

Many standard containers fail restricted because they run as root by default. Common examples:

  • nginx:latest — runs as root by default. Use nginxinc/nginx-unprivileged instead.
  • redis — runs as root. Override with securityContext.runAsUser: 999
  • Most Helm charts — often haven't set security contexts properly

Fix for nginx:

yaml
containers:
- name: nginx
  image: nginxinc/nginx-unprivileged:latest  # already non-root
  securityContext:
    allowPrivilegeEscalation: false
    readOnlyRootFilesystem: true
    capabilities:
      drop: [ALL]

Cluster-Wide Default with Admission Configuration

Apply a default PSA level to all new namespaces:

yaml
# /etc/kubernetes/admission-configuration.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:
      namespaces:
      - kube-system
      - gpu-operator
      - cert-manager

PSA vs Kyverno vs OPA Gatekeeper

PSA is built-in and zero-config but limited. For more control:

ToolBest for
Pod Security AdmissionSimple cluster-wide baseline, no extra components
KyvernoRich policies, auto-mutation, easy YAML syntax
OPA GatekeeperComplex enterprise policies, Rego language

Most teams use PSA as a first layer and Kyverno for custom policies on top.


Quick reference:

  • privileged — system namespaces only
  • baseline — general workloads, safe default
  • restricted — production, sensitive workloads
  • Always use warn first, then enforce
  • Exempt kube-system and system namespaces from restrictions
Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments