Pod Security Admission (PSA)
- 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
securityContextfields includingrunAsNonRoot,readOnlyRootFilesystem,allowPrivilegeEscalation,capabilities,seccompProfile, and volume types. Understanding these fields is essential for passing PSA checks. - Migration Path: Teams migrating from PSP should use
auditandwarnmodes to identify violations before enablingenforce, 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
The Privileged level is completely unrestricted. No security checks are applied. Use this only for:
kube-systemnamespace (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
procMountto anything other thanDefault
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_SERVICEif needed - A seccomp profile (
RuntimeDefaultorLocalhost) - 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:
| Mode | Behavior | Use Case |
|---|---|---|
| enforce | Rejects pods that violate the policy. The pod is not created. | Production namespaces with tested workloads |
| audit | Allows the pod but records the violation in the audit log. | Testing phase — identify violations without breaking workloads |
| warn | Allows 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:
| Allowed | Blocked |
|---|---|
configMap, secret, emptyDir, projected | hostPath (access to host filesystem) |
downwardAPI, csi, persistentVolumeClaim | nfs, iscsi, fc (direct network storage) |
ephemeral | gitRepo (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:
-
Audit first: Add
audit: restrictedandwarn: restrictedlabels to all namespaces. This does not block anything but shows what would fail. -
Review violations: Check the API server audit log and
kubectlwarnings 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
-
Fix workloads: Update Deployments, StatefulSets, and DaemonSets to comply with the target security level.
-
Enforce Baseline first: Set
enforce: baselineon all namespaces. This blocks the most dangerous configurations without being overly restrictive. -
Graduate to Restricted: Once all workloads pass Baseline enforcement, begin enforcing Restricted on security-sensitive namespaces.
-
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
- 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.
- 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. - Not testing with
warnandauditfirst: Jumping straight toenforce: restrictedwill break many workloads. Always validate first. - Ignoring init containers: PSA evaluates init containers with the same rules as regular containers. An init container running as root will cause rejection.
- Misunderstanding
runAsNonRoot: SettingrunAsNonRoot: truewithoutrunAsUserrequires the container image to have a non-rootUSERdirective. If the image defaults to root, the pod will fail at runtime (not at admission time). - Confusing namespace labels with pod labels: PSA labels go on the Namespace, not on individual pods.
Best Practices
- Enforce Baseline everywhere as a minimum. There is almost never a reason to allow
hostPID,hostNetwork, or privileged containers for application workloads. - Use Restricted for sensitive namespaces (payments, authentication, PII-handling services).
- Always set
securityContextexplicitly in your pod specs — do not rely on defaults. - Combine PSA with a policy engine (Kyverno, Gatekeeper) for mutation, custom policies, and finer-grained control.
- Rebuild container images to run as non-root — this is the single most impactful security improvement.
- Use
readOnlyRootFilesystem: trueeven though PSA does not require it. It prevents attackers from writing malicious files. - Document exemptions: When a workload legitimately needs elevated privileges, document why and review regularly.
- 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.