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 modes —enforce,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 ofenforce,audit,warn.<PROFILE>is one of the three Pod Security Standards profiles:privileged,baseline,restricted.- The optional
-versionsuffix pins the profile to a Kubernetes minor version (e.g.v1.31) orlatest. 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; theCREATE/UPDATErequest 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 messagekubectlprints 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-systemand 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:
- Label
warn: <target>andaudit: <target>— operators see violations, nothing breaks. - Fix the workloads the warnings/audit records surface.
- 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.31Line-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 workloadsLine-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
securityContextfields), 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/auditat the target profile, let the warnings drive workload fixes, then addenforce. Skipping straight toenforce: restrictedon a live namespace breaks workloads. - Set a cluster-wide
defaultsfloor. Out of the box, an unlabelled namespace isprivileged. AnAdmissionConfigurationdefaultsofenforce: baselineensures new namespaces are protected without anyone remembering to label them — a critical hygiene step on multi-team clusters. - Pin
-versionon 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-systemand infra namespaces almost always need exemption (or aprivilegedlabel). The node agents and add-ons there legitimately need host access — exempt the namespace, and keep the exemption list short and reviewed.
See Also
- Pod Security Standards — the three profiles PSA enforces; PSA is the enforcer, PSS is the spec
- SecurityContext — the per-Pod/per-container fields PSA evaluates
- PodSecurityPolicy (Deprecated) — the removed predecessor; PSA is its built-in replacement
- Kyverno / OPA Gatekeeper — fine-grained / mutating policy beyond PSA’s three fixed profiles
- ValidatingAdmissionPolicy / MutatingAdmissionPolicy — general-purpose in-process CEL policy
- Admission Controllers — the API-server stage PSA runs in
- Namespace — the unit PSA is configured on, via labels
- Kubernetes Audit Logging — where PSA’s
audit-mode annotations land - RuntimeClass — exemptable dimension; sandboxed runtimes may bypass PSA
- Kubernetes MOC — parent MOC (§12 Security)