Server--:--:--You--:--:--

Pod Security Admission

By Prabath Thalangama· August 30, 2026· 3 min read
#kubernetes#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: no privileged: 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 back NET_BIND_SERVICE), seccompProfile: RuntimeDefault, allowPrivilegeEscalation: false, no writable hostPath, readOnlyRootFilesystem encouraged. 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 (kubectl shows 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.

  1. Label with warn + audit at restricted, enforce still privileged (or unset). Deploy nothing new — just observe.
  2. Watch warnings on kubectl apply and the audit log for violations.
  3. Fix each workload's securityContext (below).
  4. Once clean, flip enforce to baseline, then to restricted.

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 the securityContext above.
  • 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 rs shows the rejection. The Deployment itself applies fine.
  • warn shows violations but enforce doesn't blockenforce is at a lower level (baseline/privileged) than warn (restricted). That's the intended staged rollout; tighten enforce when 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-root USER and fix directory ownership, or use fsGroup + writable emptyDir mounts.
  • 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.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.