Pod Security Admission
Introduction¶
PodSecurityPolicy was removed in Kubernetes 1.25. Its built-in replacement is Pod Security Admission (PSA) — a validating admission controller that enforces the Pod Security Standards based on namespace labels. It's simpler than PSP (no RBAC binding puzzle) but less flexible (no custom policy — for that, use Kyverno or Gatekeeper).
The three levels (Pod Security Standards)¶
privileged— no restrictions. For infrastructure/system workloads that genuinely need it (CNI, CSI, node agents).baseline— blocks known privilege escalations: noprivileged: true, no host namespaces, no hostPath (mostly), limited capabilities, no host ports (mostly). A minimal bar most apps already clear.restricted— hardened: must run as non-root,runAsNonRoot: true, drop ALL capabilities (may add backNET_BIND_SERVICE),seccompProfile: RuntimeDefault,allowPrivilegeEscalation: false, no writable hostPath,readOnlyRootFilesystemencouraged. The target for application workloads.
The three modes¶
Applied per level, per namespace:
enforce— reject pods that violate the level.audit— allow, but record a violation in the audit log.warn— allow, but return a warning to the user/client (kubectlshows it).
Namespace labels¶
apiVersion: v1
kind: Namespace
metadata:
name: team-a
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.30
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
kubectl label namespace team-a \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted
Pin enforce-version to your cluster version so a cluster upgrade doesn't
silently tighten the policy under you.
The rollout pattern¶
Don't jump straight to enforce: restricted — you'll break deploys.
- Label with
warn+auditatrestricted,enforcestillprivileged(or unset). Deploy nothing new — just observe. - Watch warnings on
kubectl applyand the audit log for violations. - Fix each workload's
securityContext(below). - Once clean, flip
enforcetobaseline, then torestricted.
What a restricted-compliant pod looks like¶
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:1.4.2
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
# add: ["NET_BIND_SERVICE"] # only if binding a port < 1024
readOnlyRootFilesystem: true
volumeMounts:
- { name: tmp, mountPath: /tmp } # give it a writable /tmp if RO root
volumes:
- name: tmp
emptyDir: {}
The image must also actually work as non-root — many need a rebuild
(USER 1000, writable dirs owned by that UID).
Exemptions¶
Some namespaces (kube-system) and specific users/runtimeClasses can be exempt,
configured in the AdmissionConfiguration file passed to the API server (not
per-namespace):
exemptions:
usernames: ["system:serviceaccount:kube-system:..."]
namespaces: ["kube-system"]
runtimeClasses: []
Keep exemptions minimal and audited.
Verification and troubleshooting¶
# Dry-run a namespace label change to see what would be rejected
kubectl label --dry-run=server ns team-a pod-security.kubernetes.io/enforce=restricted
# it prints warnings for every existing pod that would violate
kubectl get events -n team-a --field-selector reason=FailedCreate
kubectl apply -f pod.yaml # PSA warnings appear inline
- Deploy rejected: "violates PodSecurity restricted:latest" — the message
lists exactly which fields (
runAsNonRoot != true,unrestricted capabilities,seccompProfile). Add thesecurityContextabove. - Pod created via a controller (Deployment) not blocked, but the Deployment
"works" with 0 ready pods — PSA rejects the pod, so the ReplicaSet can't
create any.
kubectl describe rsshows the rejection. The Deployment itself applies fine. warnshows violations butenforcedoesn't block —enforceis at a lower level (baseline/privileged) thanwarn(restricted). That's the intended staged rollout; tightenenforcewhen ready.- Image won't run as non-root after adding
runAsNonRoot— the image's default user is root or it writes to root-owned paths. Rebuild with a non-rootUSERand fix directory ownership, or usefsGroup+ writableemptyDirmounts. - Need per-workload policy, not per-namespace — PSA can't. Use Kyverno / Gatekeeper for fine-grained or custom rules; PSA + one of those is a common combo.
- Cluster upgrade tightened things — you didn't pin
enforce-version. Pin it.
Related tools and reading¶
- On-site: YAML ↔ JSON converter.
- Related posts: Kubernetes RBAC: Roles and Bindings, Kubernetes version upgrades without drama.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.