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.More in Docker & Kubernetes
Kustomize Basics
Kustomize customizes plain YAML with overlays instead of templating. Here's bases vs overlays, the patch types, generators, and the gotchas around name suffixes.
September 13, 2026EKS vs Self-Managed Kubernetes
EKS runs the control plane for you; you still own nodes, networking, and add-ons. Here's the division of responsibility, the node options, and when self-managed is worth it.
September 11, 2026The AWS Load Balancer Controller
On EKS, the AWS Load Balancer Controller turns Ingress objects into ALBs and LoadBalancer Services into NLBs. Here's target types, ALB sharing, and the IAM/subnet-tag gotchas.
September 7, 2026