TL;DR

Pod Security Admission applies built-in Pod Security Standards at namespace level: privileged, baseline, or restricted. It replaced the old PodSecurityPolicy admission controller. Use warn and audit first, then enforce restricted for normal app namespaces. Keep privileged platform agents isolated in dedicated namespaces.

Mental Model

RBAC controls who can ask Kubernetes to create Pods. Pod Security Admission controls whether the Pod spec is acceptable at admission time. It checks runtime-sensitive fields such as privileged mode, host namespace access, hostPath mounts, Linux capabilities, seccomp, root user settings, and privilege escalation.

PSA is intentionally simple: it uses namespace labels and built-in profiles. If a client needs custom policy logic, they usually add Kyverno, Gatekeeper, Kubewarden, or a platform admission controller alongside PSA.

kubectl applyPod templateAPI ServeradmissionPSA labelsenforce/warn/auditAdmit PodReject PodPSA evaluates Pod specs before they are persisted or scheduled.

Pod Security Admission blocks unsafe Pod specs at API admission time.

Security Levels

LevelMeaningUse
privilegedUnrestricted profile.Dedicated infrastructure namespaces for CNI, CSI, node agents, or debug tooling.
baselineBlocks known privilege escalation paths while allowing common app patterns.Migration step for legacy workloads.
restrictedStrongest built-in profile: non-root, no privilege escalation, dropped capabilities, seccomp, tight volumes.Target for normal production app namespaces.

Namespace Labels

PSA reads labels on the Namespace. enforce rejects violating Pods, warn shows warnings to clients such as kubectl, and audit records violations in audit logs.

yamlnamespace-psa.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: app
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: latest
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: latest
bashpsa-labels.sh
kubectl label namespace app \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=latest \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/audit-version=latest \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/warn-version=latest --overwrite

kubectl get namespace app --show-labels

Restricted-Friendly Workload

This pattern works for many web workloads: non-root image, no privilege escalation, all Linux capabilities dropped, runtime seccomp, read-only root filesystem, and writable emptyDir only where needed.

yamlrestricted-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      securityContext:
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: web
          image: nginxinc/nginx-unprivileged:1.27
          ports:
            - containerPort: 8080
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir: {}

securityContext Fields

FieldWhy It MattersRestricted-Friendly Default
runAsNonRootPrevents UID 0 containers.true
allowPrivilegeEscalationBlocks gaining more privileges than parent process.false
capabilities.dropRemoves Linux capabilities.["ALL"]
seccompProfileLimits syscalls.RuntimeDefault
readOnlyRootFilesystemReduces writeable attack surface.true when app supports it.
privilegedGives broad host-level access.Do not use in app namespaces.

Common Violations

ViolationWhy It FailsFix
privileged: truePrivileged containers are not allowed in baseline/restricted.Move to platform namespace or remove privileged need.
hostNetwork, hostPID, hostIPCAccesses host namespaces.Use normal pod networking/process isolation.
hostPathMounts host filesystem.Use PVC, ConfigMap, Secret, or emptyDir where possible.
Runs as rootRestricted expects non-root.Use non-root image or set valid non-root UID.
Added capabilitiesRestricted requires all capabilities dropped.Drop all; redesign if special capability is needed.
No seccompRestricted expects seccomp profile.Set RuntimeDefault.

Debug Admission Failures

bashpsa-debug.sh
NS=<namespace>

kubectl get ns "$NS" --show-labels
kubectl apply --dry-run=server -f deployment.yaml
kubectl get events -n "$NS" --sort-by=.lastTimestamp | grep -Ei 'forbidden|violates|podsecurity|admission'
kubectl describe rs -n "$NS" <replicaset>

# Find security-sensitive fields in a rendered workload.
kubectl get deploy <deploy> -n "$NS" -o yaml | grep -E 'privileged|hostNetwork|hostPID|hostIPC|hostPath|runAs|allowPrivilege|capabilities|seccomp' -n

Platform Exceptions

Some workloads genuinely need privileged access: CNI plugins, CSI drivers, node exporters, log collectors, backup agents, security agents, and low-level troubleshooting tools. Do not weaken app namespaces for them. Put them in dedicated namespaces with clear ownership and labels.

yamlplatform-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: platform-agents
  labels:
    pod-security.kubernetes.io/enforce: privileged
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted
    owner: platform-team

Safe Rollout Pattern

  1. Inventory namespaces and workloads that need privileged, hostPath, hostNetwork, or root access.
  2. Set warn=restricted and audit=restricted first while keeping enforce=baseline.
  3. Fix workload specs and Helm values to become restricted-compatible.
  4. Move platform agents into dedicated exception namespaces.
  5. Switch app namespaces to enforce=restricted after warnings are clean.

Symptom To Cause

SymptomLikely CauseCheck First
Forbidden: violates PodSecurityPod spec violates namespace enforce level.Admission message and namespace labels.
Helm install fails only in prod namespaceProd namespace enforces stricter PSA labels.Rendered chart securityContext and namespace labels.
App cannot write temp filesreadOnlyRootFilesystem enabled without writable mount.Add emptyDir for required temp/cache path.
Container fails as non-rootImage expects root-owned paths or privileged port.Use non-root image, change port, fix file ownership.
DaemonSet blockedNeeds hostPath/hostNetwork/privileged access.Dedicated privileged platform namespace.

Production Patterns

  • 1Default app namespaces to restricted: make exceptions explicit instead of broad.
  • 2Use audit/warn during migration: prevent surprise outages.
  • 3Patch Helm values: most charts expose securityContext knobs.
  • 4Pair with policy tools: use Kyverno/Gatekeeper for custom rules not covered by PSA.
  • 5Document exceptions: privileged namespaces should have owner and purpose labels.