Pod Security Admission

Pod Security Admission (PSA) is the built-in admission controller that enforces the Pod Security Standards — it is the component that actually rejects a Pod whose [[SecurityContext|securityContext]] does not meet a namespace’s required hardening profile (Kubernetes — Pod Security Admission). PSA is the successor to PodSecurityPolicy for the common case: it shipped enabled by default and reached stable in Kubernetes 1.25, the same release that removed PSP. Its defining design choices are deliberate reactions to PSP’s failures: PSA is configured per-namespace via labels (not via cluster-scoped policy objects bound through RBAC), it is purely validating (it never mutates a Pod), and it offers three modesenforce, audit, warn — so a profile can be rolled out gradually. PSA is intentionally coarse; for fine-grained or mutating policy you reach past it to Kyverno or OPA Gatekeeper.

Mental Model

PSA is a per-namespace label-driven gate sitting in the API server’s admission chain. Each namespace declares, via labels, which Pod Security Standards profile applies and how strictly. When a Pod is created or updated, PSA reads the target namespace’s labels, evaluates the Pod against the named profile, and acts according to the mode.

flowchart TD
    REQ["CREATE/UPDATE Pod<br/>(or Deployment, Job, ...)"]
    PSA["Pod Security Admission controller<br/>(in the API server admission chain)"]
    NS["Target Namespace labels<br/>pod-security.kubernetes.io/{enforce,audit,warn}: profile"]
    EVAL["Evaluate Pod against the named PSS profile"]
    ENF{"enforce mode<br/>violated?"}
    REJECT["REJECT — Pod creation fails"]
    ADMIT["Pod admitted"]
    AUD["audit mode violated → audit-log annotation"]
    WARN["warn mode violated → user-facing warning"]

    REQ --> PSA
    NS -->|"PSA reads labels"| PSA
    PSA --> EVAL
    EVAL --> ENF
    ENF -->|"yes"| REJECT
    ENF -->|"no"| ADMIT
    EVAL -.-> AUD
    EVAL -.-> WARN

The diagram shows PSA’s evaluation per request. The insight to extract: the three modes are evaluated independently and simultaneously — a namespace can enforce: baseline while it warn: restricted and audit: restricted, so violating Pods are still admitted (they only break Baseline if they break Baseline) but operators get a warning and an audit record of how far they are from Restricted. This is the mechanism that makes a non-disruptive rollout possible.

Mechanical Walk-through

Enabled by default, stable since 1.25

PSA is compiled into the API server and enabled by default. It reached GA / stable in Kubernetes 1.25 — not coincidentally the release in which PSP was removed. (It existed earlier as a beta feature, on by default from 1.23.) On any current cluster, PSA is present and active; what varies is whether namespaces carry the labels that give it something to enforce. A namespace with no PSA labels falls back to the cluster’s default — by default, privileged (i.e., no restriction), unless an AdmissionConfiguration file overrides the default.

Configuration is per-namespace, via labels

PSA is configured by labelling the Namespace. The label key follows the pattern:

pod-security.kubernetes.io/<MODE>: <PROFILE>
pod-security.kubernetes.io/<MODE>-version: <VERSION>
  • <MODE> is one of enforce, audit, warn.
  • <PROFILE> is one of the three Pod Security Standards profiles: privileged, baseline, restricted.
  • The optional -version suffix pins the profile to a Kubernetes minor version (e.g. v1.31) or latest. Pinning matters because PSS profiles tighten over time — an unpinned profile floats with cluster upgrades and could begin rejecting previously-valid Pods.

All three modes can be set at once, and to different profiles. The choice is per-namespace because the Namespace is the natural unit of team/workload ownership — and because per-namespace granularity, rather than PSP’s RBAC-grant model, is exactly the simplification PSA was designed to deliver.

The three modes

PSA evaluates the Pod against each configured mode’s profile independently:

  • enforce — a Pod that violates the profile is rejected; the CREATE/UPDATE request fails with an error. This is the only mode that blocks anything.
  • audit — a violating Pod is admitted, but PSA adds an annotation to the audit-log entry recording the violation. Invisible to the user; visible to anyone reading audit logs (Kubernetes Audit Logging).
  • warn — a violating Pod is admitted, but PSA returns a user-facing warning in the API response (the message kubectl prints in yellow). Immediate operator feedback without breaking the apply.

A critical subtlety: enforce applies only to the resulting Pod, not to the workload resource that creates it. Create a Deployment with a privileged Pod template into an enforce: restricted namespace and the Deployment is accepted — only when its controller tries to create the Pod does PSA reject it (the Pod fails, the Deployment shows 0/N ready). By contrast, warn and audit are also applied to workload resources (Deployments, Jobs, StatefulSets, …) — so a warn-labelled namespace surfaces the warning at kubectl apply time on the Deployment, which is far more useful feedback. This is a deliberate design: enforce stays Pod-precise to avoid false rejections; warn/audit reach up to the templates to give early signal.

Exemptions

PSA supports static exemptions that cause it to skip evaluation entirely. Exemptions are configured in the API server’s AdmissionConfiguration file (not via labels) along three dimensions:

  • Usernames — requests from listed authenticated/impersonated users bypass PSA.
  • RuntimeClassNames — Pods specifying a listed RuntimeClass bypass PSA (a sandboxed runtime may legitimately need otherwise-forbidden settings).
  • Namespaces — entire namespaces bypass PSA (typically kube-system and infrastructure namespaces).

An important caveat: a username exemption only exempts direct Pod creation by that user — it does not exempt Pods created on that user’s behalf by a controller. Consequently you should not exempt controller ServiceAccounts: doing so would exempt every Pod every controller creates. Namespace exemption is the right tool for “this whole namespace runs privileged infra.”

The gradual-rollout pattern

Because the modes are independent, the canonical adoption sequence for a namespace is:

  1. Label warn: <target> and audit: <target> — operators see violations, nothing breaks.
  2. Fix the workloads the warnings/audit records surface.
  3. Add enforce: <target> — now violations are rejected.

This warn → audit → enforce ratchet is the entire reason PSA has three modes; it is what makes adopting Restricted on a live namespace safe.

Configuration / API Surface

Per-namespace labels — the everyday PSA configuration:

apiVersion: v1
kind: Namespace
metadata:
  name: payments
  labels:
    # ENFORCE: reject Pods that violate the Baseline profile (the hard floor).
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/enforce-version: v1.31   # pin: don't drift on upgrade
    # WARN + AUDIT at the *stricter* Restricted profile — operators see how far
    # workloads are from Restricted, and it's recorded, but nothing is rejected yet.
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.31
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: v1.31

Line-by-line. enforce: baseline is the hard gate — Pods breaching Baseline are rejected outright. warn/audit: restricted evaluate the stricter profile but only surface (not block) violations: this namespace is firmly held at Baseline while being measured against Restricted, the exact mid-rollout state. Every mode carries an -version pin so a cluster upgrade tightening a profile cannot silently change behavior.

Cluster-wide defaults and exemptions go in an AdmissionConfiguration file passed to the API server:

apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
  configuration:
    apiVersion: pod-security.admission.config.k8s.io/v1
    kind: PodSecurityConfiguration
    defaults:                        # applied to namespaces with no PSA labels
      enforce: "baseline"            # cluster floor: no namespace is unprotected by default
      enforce-version: "latest"
      warn: "restricted"
      audit: "restricted"
    exemptions:
      usernames: []                  # avoid — does not cover controller-created Pods
      runtimeClasses: ["gvisor"]     # sandboxed runtime may need otherwise-forbidden settings
      namespaces: ["kube-system"]    # infra namespace runs privileged workloads

Line-by-line. defaults raises the cluster-wide floor so a freshly-created, unlabelled namespace is not privileged — a meaningful hardening of stock behavior. exemptions.namespaces lists kube-system, where node agents and control-plane add-ons legitimately need privileged settings. runtimeClasses exempts a sandboxed runtime. usernames is left empty deliberately — it would not exempt controller-created Pods anyway and is the wrong tool.

Failure Modes

Deployment accepted, Pods silently rejected. A privileged Pod template in an enforce-labelled namespace: the Deployment applies cleanly (PSA enforce does not check workload resources), but its ReplicaSet’s Pod creations are rejected. Symptom: Deployment stuck at 0/N, Pod-creation events show the PSA rejection. Always set warn too — warn does check the Deployment and surfaces the problem at apply time.

Namespace unlabelled and silently privileged. A namespace with no PSA labels and no cluster-default override enforces nothing. Operators assume PSA “is on” and protecting them. Set a cluster-wide defaults floor so unlabelled namespaces are at least Baseline.

Profile drift on upgrade. An unpinned enforce: restricted floats to the cluster version; an upgrade adding a new Restricted control begins rejecting Pods that were fine yesterday. Always pin -version.

Over-broad exemption. Exempting a controller ServiceAccount’s username to “make the operator work” exempts every Pod that controller creates — a gaping hole. Exempt the namespace, or fix the operator’s Pod templates instead.

Expecting PSA to default securityContext. PSA cannot mutate. It will reject a Pod missing seccompProfile: RuntimeDefault under Restricted, but it will not add it. Teams expecting PSP-style defaulting are surprised. Use Kyverno mutate rules or MutatingAdmissionPolicy for defaulting.

Alternatives and When to Choose Them

  • PSA vs Kyverno / OPA Gatekeeper. PSA is built-in, zero-dependency, and enforces the three fixed Pod Security Standards profiles at namespace granularity — and cannot mutate. Reach for Kyverno or Gatekeeper when you need: mutation (defaulting securityContext fields), a custom rule the PSS profiles do not express, finer-than-namespace granularity, or policy over non-Pod resources. Many clusters run both: PSA as the always-on baseline, a policy engine for the rest.
  • PSA vs PodSecurityPolicy. PSA is the built-in successor. It deliberately drops PSP’s two most problematic features — RBAC-use-verb-based authorization and mutation — accepting less power for a model that is actually comprehensible. See the PSP note for why those features had to go.
  • PSA vs ValidatingAdmissionPolicy. VAP is the general-purpose, in-process CEL-based validation mechanism (1.30+). You could re-implement PSS checks in VAP, but PSA already ships them, version-pinned and maintained. Use VAP for policy PSS does not cover; use PSA for PSS itself.

Production Notes

  • Roll out warn → audit → enforce, never flag-flip enforce. The three modes exist for exactly this. Label namespaces warn/audit at the target profile, let the warnings drive workload fixes, then add enforce. Skipping straight to enforce: restricted on a live namespace breaks workloads.
  • Set a cluster-wide defaults floor. Out of the box, an unlabelled namespace is privileged. An AdmissionConfiguration defaults of enforce: baseline ensures new namespaces are protected without anyone remembering to label them — a critical hygiene step on multi-team clusters.
  • Pin -version on every label. PSS profiles tighten across releases; pinning makes upgrades deterministic. Bump the pin as a deliberate, reviewed change.
  • PSA does not cover everything. It checks Pod security settings only — not image provenance, not network policy, not resource limits. It is one layer; pair it with image signing (Image Signing with Sigstore), NetworkPolicy, and runtime detection (Falco).
  • kube-system and infra namespaces almost always need exemption (or a privileged label). The node agents and add-ons there legitimately need host access — exempt the namespace, and keep the exemption list short and reviewed.

See Also