Skip to main content

Pod Security Admission (PSA)

Key Takeaways for AI & Readers
  • Simplified Security: Pod Security Admission (PSA) replaces the complex and deprecated PodSecurityPolicies (PSPs) with a simpler, more manageable approach based on predefined Pod Security Standards (PSS). PSA is built into Kubernetes as a stable feature since v1.25.
  • Three Security Levels: The Pod Security Standards define three profiles — Privileged (unrestricted, for system components), Baseline (prevents known privilege escalations, suitable for most workloads), and Restricted (hardened best practices, for security-sensitive workloads).
  • Three Enforcement Modes: Each level can be applied in enforce (reject violating pods), audit (allow but log violations), or warn (allow but display warnings to the user) mode. This enables gradual rollout without disrupting running workloads.
  • Namespace-Based Configuration: Security levels are applied to namespaces via simple labels, making configuration declarative and GitOps-friendly. No admission controller configuration or webhook setup is required.
  • securityContext Fields: PSA evaluates specific securityContext fields including runAsNonRoot, readOnlyRootFilesystem, allowPrivilegeEscalation, capabilities, seccompProfile, and volume types. Understanding these fields is essential for passing PSA checks.
  • Migration Path: Teams migrating from PSP should use audit and warn modes to identify violations before enabling enforce, minimizing disruption.

Previously, Kubernetes used PodSecurityPolicies (PSP) to control security-sensitive aspects of pod specifications. PSPs were powerful but notoriously difficult to manage: they were cluster-scoped, applied via RBAC binding (which was unintuitive), had complex precedence rules, and were prone to misconfiguration. PSPs were deprecated in v1.21 and removed in v1.25.

The replacement is Pod Security Admission (PSA), which uses the Pod Security Standards (PSS) — a set of predefined security profiles that are enforced at the namespace level via simple labels. PSA is built into every Kubernetes cluster v1.23+ and requires no additional installation.

1. The Three Security Levels

PSA defines three progressively restrictive security profiles. Each profile specifies which pod security fields are checked and what values are permitted.

😈
Privileged Pod
Runs as root, has host access.
🛡️
Baseline Pod
Standard security, no root escalations.
Admission Result
🔥
pod-security.kubernetes.io/enforce: privileged

Privileged

The Privileged level is completely unrestricted. No security checks are applied. Use this only for:

  • kube-system namespace (core Kubernetes components)
  • CNI plugins (Calico, Cilium) that require host networking and privileged containers
  • Storage drivers and CSI node plugins
  • Monitoring DaemonSets that need host PID/network access (e.g., node-exporter)

Baseline

The Baseline level prevents known privilege escalation vectors while remaining broadly compatible with most workloads. It blocks:

  • Privileged containers (privileged: true)
  • Host namespaces (hostPID, hostIPC, hostNetwork)
  • Dangerous volume types (hostPath)
  • Adding dangerous Linux capabilities (NET_RAW, SYS_ADMIN, etc.)
  • Setting procMount to anything other than Default

It allows:

  • Running as root (not ideal, but many legacy applications require it)
  • Writable root filesystem
  • Most capabilities if not explicitly dangerous

Restricted

The Restricted level follows current pod hardening best practices. In addition to everything Baseline blocks, it requires:

  • Running as non-root (runAsNonRoot: true)
  • Dropping ALL capabilities and only adding back NET_BIND_SERVICE if needed
  • A seccomp profile (RuntimeDefault or Localhost)
  • No privilege escalation (allowPrivilegeEscalation: false)
  • Restricted volume types (only configMap, secret, emptyDir, projected, downwardAPI, csi, persistentVolumeClaim, ephemeral)

2. Modes of Operation

Each security level can be applied in three modes, allowing gradual adoption:

ModeBehaviorUse Case
enforceRejects pods that violate the policy. The pod is not created.Production namespaces with tested workloads
auditAllows the pod but records the violation in the audit log.Testing phase — identify violations without breaking workloads
warnAllows the pod but sends a warning message to the user via kubectl.Developer feedback — show what would fail under enforcement

You can combine modes. A common pattern is to enforce Baseline while auditing and warning on Restricted:

apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
# Enforce Baseline — block known privilege escalation
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: latest
# Audit Restricted — log violations for future hardening
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: latest
# Warn Restricted — show developers what to fix
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: latest

The *-version label pins the policy to a specific Kubernetes version's definition of the standard. Using latest always applies the current version's rules.

3. Configuring Namespaces

Restricted Namespace (Security-Sensitive Workloads)

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

Privileged Namespace (System Components)

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

A Pod That Passes Restricted

This is a fully compliant pod for a Restricted namespace:

apiVersion: v1
kind: Pod
metadata:
name: secure-app
namespace: payments
spec:
# Pod-level security context
securityContext:
runAsNonRoot: true # pod must not run as UID 0
seccompProfile:
type: RuntimeDefault # required by Restricted
containers:
- name: app
image: registry.example.com/app:v2.0.0
ports:
- containerPort: 8080
# Container-level security context
securityContext:
allowPrivilegeEscalation: false # required by Restricted
readOnlyRootFilesystem: true # best practice (not required, but recommended)
runAsNonRoot: true # redundant with pod level, but explicit
runAsUser: 1000 # run as non-root user
runAsGroup: 1000
capabilities:
drop:
- ALL # required by Restricted: drop all
# add:
# - NET_BIND_SERVICE # only capability allowed to add back
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
volumeMounts:
- name: tmp
mountPath: /tmp # writable temp directory
volumes:
- name: tmp
emptyDir: {} # allowed volume type

4. securityContext Fields in Detail

Understanding the key securityContext fields is essential for passing PSA checks:

runAsNonRoot

Ensures the container process does not run as UID 0. The container image must specify a non-root USER in its Dockerfile, or you must set runAsUser to a non-zero value.

allowPrivilegeEscalation

When set to false, prevents a process from gaining more privileges than its parent (blocks setuid binaries, sudo, etc.). This is required by the Restricted level.

readOnlyRootFilesystem

Makes the container's root filesystem read-only. Applications that need writable directories should use emptyDir volumes mounted at specific paths (e.g., /tmp, /var/cache). Not required by PSA, but strongly recommended.

capabilities

Linux capabilities provide fine-grained control over what privileged operations a process can perform. The Restricted level requires dropping ALL capabilities and optionally adding back only NET_BIND_SERVICE (needed to bind to ports below 1024).

securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE # only if your app binds to ports < 1024

seccompProfile

Seccomp (Secure Computing Mode) restricts which system calls a process can make. The Restricted level requires at least RuntimeDefault, which blocks dangerous syscalls while allowing common ones.

securityContext:
seccompProfile:
type: RuntimeDefault # use the container runtime's default seccomp profile

Volume Type Restrictions

The Restricted level limits allowed volume types to prevent access to the host filesystem:

AllowedBlocked
configMap, secret, emptyDir, projectedhostPath (access to host filesystem)
downwardAPI, csi, persistentVolumeClaimnfs, iscsi, fc (direct network storage)
ephemeralgitRepo (deprecated)

5. Common Violations and How to Fix Them

"Container must not run as root"

# VIOLATION
securityContext: {} # no runAsNonRoot set

# FIX
securityContext:
runAsNonRoot: true
runAsUser: 1000

If your container image runs as root by default, you need to either rebuild it with a non-root USER or set runAsUser explicitly.

"Privilege escalation must be disallowed"

# VIOLATION
securityContext:
allowPrivilegeEscalation: true # or omitted (defaults to true)

# FIX
securityContext:
allowPrivilegeEscalation: false

"Container must drop ALL capabilities"

# VIOLATION
securityContext:
capabilities:
add:
- NET_ADMIN # not allowed in Restricted

# FIX
securityContext:
capabilities:
drop:
- ALL

"Seccomp profile must be set"

# VIOLATION — no seccomp profile specified
securityContext: {}

# FIX — set at pod or container level
securityContext:
seccompProfile:
type: RuntimeDefault

"HostPath volumes are not allowed"

# VIOLATION
volumes:
- name: host-data
hostPath:
path: /var/log

# FIX — use an appropriate volume type
volumes:
- name: app-logs
emptyDir: {}

6. Migration from PodSecurityPolicy

If you are migrating from PSP (removed in v1.25), follow this strategy:

  1. Audit first: Add audit: restricted and warn: restricted labels to all namespaces. This does not block anything but shows what would fail.

  2. Review violations: Check the API server audit log and kubectl warnings to identify all non-compliant workloads.

# Check for PSA warnings/violations in audit logs
kubectl get events -A --field-selector reason=FailedCreate | grep -i security
  1. Fix workloads: Update Deployments, StatefulSets, and DaemonSets to comply with the target security level.

  2. Enforce Baseline first: Set enforce: baseline on all namespaces. This blocks the most dangerous configurations without being overly restrictive.

  3. Graduate to Restricted: Once all workloads pass Baseline enforcement, begin enforcing Restricted on security-sensitive namespaces.

  4. Exempt system namespaces: Keep kube-system, CNI namespaces, and monitoring namespaces at the Privileged level.

Exemptions

For workloads that genuinely need elevated privileges (e.g., a log collector that needs hostPath), PSA supports exemptions configured at the API server level:

# AdmissionConfiguration — set via API server flag
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
exemptions:
usernames: []
runtimeClasses: []
namespaces:
- kube-system
- monitoring

Common Pitfalls

  1. Assuming PSA replaces all PSP functionality: PSA is deliberately simpler than PSP. It does not support mutation (auto-fixing pod specs), fine-grained per-field exemptions, or user-specific policies. For those, use a policy engine like Kyverno or Gatekeeper alongside PSA.
  2. Forgetting to version-pin: Without enforce-version, the policy follows the cluster's Kubernetes version. An upgrade could change what the standard allows, potentially breaking workloads.
  3. Not testing with warn and audit first: Jumping straight to enforce: restricted will break many workloads. Always validate first.
  4. Ignoring init containers: PSA evaluates init containers with the same rules as regular containers. An init container running as root will cause rejection.
  5. Misunderstanding runAsNonRoot: Setting runAsNonRoot: true without runAsUser requires the container image to have a non-root USER directive. If the image defaults to root, the pod will fail at runtime (not at admission time).
  6. Confusing namespace labels with pod labels: PSA labels go on the Namespace, not on individual pods.

Best Practices

  1. Enforce Baseline everywhere as a minimum. There is almost never a reason to allow hostPID, hostNetwork, or privileged containers for application workloads.
  2. Use Restricted for sensitive namespaces (payments, authentication, PII-handling services).
  3. Always set securityContext explicitly in your pod specs — do not rely on defaults.
  4. Combine PSA with a policy engine (Kyverno, Gatekeeper) for mutation, custom policies, and finer-grained control.
  5. Rebuild container images to run as non-root — this is the single most impactful security improvement.
  6. Use readOnlyRootFilesystem: true even though PSA does not require it. It prevents attackers from writing malicious files.
  7. Document exemptions: When a workload legitimately needs elevated privileges, document why and review regularly.
  8. Apply PSA labels via GitOps to ensure namespace security configuration is version-controlled and auditable.

What's Next?

  • Learn about Policy as Code for custom admission policies beyond what PSA provides.
  • Explore Secrets Management to protect sensitive data that pods consume.
  • See Multi-Tenancy for applying different security levels to different teams' namespaces.
  • Understand Security to complement pod security with network-level isolation.