Pod Security Standards & Admission
PodSecurityPolicy was deprecated in Kubernetes 1.21 and removed in 1.25. Its replacement — Pod Security Admission (PSA) with Pod Security Standards (PSS) — ships built-in to every cluster as an admission controller. No webhook, no CRD, no third-party install required. Three levels, three enforcement modes, and a single kubectl label to activate them.
Why PSS Replaced PSP
PodSecurityPolicy had a fundamental usability problem: granting a PSP to a namespace required an RBAC binding — and the binding had to be to the ServiceAccount that created the pod (often a controller like the Deployment controller), not the pod's own ServiceAccount. This confused everyone, required extra RBAC grants, and made auditing hard.
PSA fixes this by operating at the namespace level with no RBAC entanglement. You label a namespace with the desired security level; the API server evaluates every pod creation against that level automatically.
Security Levels
PSS defines three levels, from most permissive to most restrictive:
| Level | Intent | Notable restrictions |
|---|---|---|
| privileged | No restrictions. Equivalent to no policy. | — |
| baseline | Prevents known privilege escalations. Minimal impact on typical workloads. | No hostPID/hostIPC/hostNetwork, no privileged containers, limited capabilities, restricted volume types, no host path mounts. |
| restricted | Strongly hardened. Current best practice for security-sensitive workloads. | Everything in baseline, plus: must run as non-root, must drop ALL capabilities, seccomp profile required, no privilege escalation allowed. |
- ✅ hostPID
- ✅ hostNetwork
- ✅ privileged containers
- ✅ any capabilities
- ✅ run as root
- ✅ any volume type
- ❌ hostPID/hostNetwork
- ❌ privileged containers
- ❌ hostPath volumes
- ✅ can run as root
- ✅ default capabilities
- ✅ emptyDir, PVC, CM, secret
- ❌ everything in baseline
- ❌ run as root
- ❌ privilege escalation
- ❌ capabilities (must drop ALL)
- ⚠️ seccomp required
- ⚠️ runAsNonRoot required
Enforcement Modes
Each level can be applied in three independent modes:
| Mode | Effect | When to use |
|---|---|---|
| enforce | Violating pods are rejected at creation time. | Production enforcement after testing. |
| audit | Violations are recorded in the audit log but pods are allowed. | Monitoring impact without blocking. |
| warn | kubectl prints a warning for violating pods but allows them. | Developer feedback during migration. |
Run all three simultaneously during migration. Set warn and audit to restricted to surface issues, while keeping enforce at baseline so nothing breaks yet.
Namespace Labels
Activation is purely via namespace labels. The label format is pod-security.kubernetes.io/<mode>: <level> with an optional version pin.
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
# Block pods that violate baseline
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: v1.31
# Warn devs about anything that would fail restricted
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.31
# Audit violations of restricted in the API server audit log
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.31
# Dry-run to preview violations before enforcing
kubectl label --dry-run=server --overwrite ns production \
pod-security.kubernetes.io/enforce=restricted
# Enforce baseline, warn at restricted
kubectl label --overwrite ns production \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/warn=restricted
The -version label pins the policy to a specific Kubernetes version's definition of that level. Without it, the policy upgrades automatically when you upgrade the cluster. Pinning prevents silent policy tightening mid-release.
securityContext Fields
To pass the restricted level, pods and containers need explicit securityContext fields. The check is at the pod/container spec level — the admission controller does not auto-inject defaults.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: production
spec:
template:
spec:
securityContext:
runAsNonRoot: true # required by restricted
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault # required by restricted (or Localhost)
containers:
- name: api
image: myapp:1.0.0
securityContext:
allowPrivilegeEscalation: false # required by restricted
readOnlyRootFilesystem: true # best practice
capabilities:
drop: ["ALL"] # required by restricted
add: [] # add back only what's needed
resources:
requests: { cpu: "100m", memory: "128Mi" }
limits: { cpu: "500m", memory: "256Mi" }
PSA evaluates the security context of every container in the pod spec — including initContainers and ephemeralContainers. A debug sidecar running as root will cause the whole pod to be rejected under restricted enforcement.
Exemptions
Some system workloads legitimately need elevated privileges (e.g. CNI DaemonSets, log collectors). Exemptions are configured globally in the API server's AdmissionConfiguration, not per-namespace:
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" # cluster-wide default
enforce-version: "latest"
audit: "restricted"
audit-version: "latest"
warn: "restricted"
warn-version: "latest"
exemptions:
usernames: []
runtimeClasses: []
namespaces:
- kube-system # system namespace exempted entirely
- calico-system
- monitoring
Migrating from PSP
PodSecurityPolicy was removed in Kubernetes 1.25. If you are upgrading a cluster that used PSP, the migration path is:
- Inventory existing PSPs —
kubectl get pspand map each policy to the closest PSS level. - Add
warnlabels — label each namespace withpod-security.kubernetes.io/warn: restrictedand observe warnings without blocking. - Fix violations — update Deployments to set
runAsNonRoot, drop capabilities, and addseccompProfile. - Add
auditlabels — direct evidence to API server audit logs. - Switch to
enforce— start withbaseline, then tighten torestrictednamespace by namespace. - Delete PSPs and their RBAC — after enforcement is live and stable.
# List all PSPs and their key settings
kubectl get psp -o custom-columns=\
'NAME:.metadata.name,PRIV:.spec.privileged,RUN_AS:.spec.runAsUser.rule,CAPS:.spec.allowedCapabilities'
# Find all pods using a specific PSP
kubectl get pods -A \
-o json | jq '.items[] |
select(.metadata.annotations["kubernetes.io/psp"] == "restricted") |
"\(.metadata.namespace)/\(.metadata.name)"'
kubectl Commands
# Check current PSA labels on all namespaces
kubectl get ns -o json | jq -r '
.items[] |
select(.metadata.labels | has("pod-security.kubernetes.io/enforce")) |
"\(.metadata.name): \(.metadata.labels["pod-security.kubernetes.io/enforce"])"'
# Dry-run enforce restricted — lists violations without blocking
kubectl label --dry-run=server --overwrite ns mynamespace \
pod-security.kubernetes.io/enforce=restricted
# Check what level a namespace is at
kubectl describe ns production | grep pod-security
# Simulate PSA for an existing workload by extracting and reapplying
kubectl get deploy myapp -n production -o yaml | \
kubectl apply --dry-run=server -f -