Pod Security Standards and Admission: levels and modes are different axes
In one sentence
Pod Security Standards decide what is forbidden (privileged, baseline, restricted), and Pod Security Admission decides how a violation is handled (enforce, audit, warn), and since these are different axes, they can be combined freely in namespace labels.
Why it was needed
A Pod spec has several switches that open up the whole host. hostNetwork, hostPID, privileged: true, hostPath volumes, and adding arbitrary capabilities. Turning on even one effectively removes container isolation. But these fields are also what legitimate infrastructure workloads (CNI agents, log collectors, node exporters) actually use. Because it is a problem where you must decide "how far to allow," not "forbid," a simple blocklist could not solve it.
PodSecurityPolicy filled that role and was then removed in v1.25. PSP looked not at the user but at the permissions of the party creating the Pod (usually a controller's service account), and when several PSPs applied to a Pod, it was hard to predict which would take effect. What came in its place is Pod Security Admission. The project decided the rules and fixed them in three levels, and simplified the unit of application down to a single namespace label.
How it works
Axis one — the level (Pod Security Standards).
| Level | Character |
|---|---|
privileged |
No restrictions. For system and infrastructure workloads |
baseline |
Minimal restrictions that block known privilege escalations. A default Pod spec passes as it is |
restricted |
Strongly requires Pod hardening best practices |
What baseline blocks includes sharing host namespaces, privileged, capabilities outside the allowlist, hostPath volumes, host ports, and bypassing AppArmor, SELinux, or seccomp. restricted adds to this a limit on volume types, allowPrivilegeEscalation: false, runAsNonRoot: true, not setting runAsUser to 0, explicitly specifying a seccomp profile, and dropping all capabilities and allowing only NET_BIND_SERVICE. There is an important difference here. baseline passes "if you do not use anything odd," but restricted does not let even an ordinary manifest pass. This is because a container with nothing written has no seccomp profile and has not dropped capabilities.
Axis two — the mode (Pod Security Admission).
| Mode | On violation |
|---|---|
enforce |
Rejects the Pod |
audit |
Adds an annotation to the event in the audit log and lets it pass |
warn |
Shows a warning to the user and lets it pass |
The two axes meet in labels. Each mode has two labels.
apiVersion: v1
kind: Namespace
metadata:
name: my-baseline-namespace
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: v1.30
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
This combination is the adoption procedure itself. Right now enforce only baseline, and count restricted violations with warnings and audit. A single namespace can set all three modes and give each mode a different level, so "turning on the next goal in advance and observing" takes just two lines of labels.
If you leave out the -version suffix, deployments stop on upgrade day. The value of a version label is a valid Kubernetes minor version or latest, and if you do not write it, it behaves as latest. The contents of a level change from version to version — in fact, restricted changed in v1.25. If you leave it at latest, on the day you upgrade the cluster, a deployment that worked yesterday is blocked even though you changed no manifest. Conversely, if you pin the version, nothing happens that day even if the level gets stronger, and a person decides the schedule for raising the version. It is a device for separating policy changes from cluster upgrades.
Admission is a mechanism that looks at requests. It cannot block Pods that are already running. Even after you attach a label, violating Pods keep running, and they are rejected only when they are restarted or rescheduled. That is why you need a procedure that pulls out the violations in advance before switching. If you run the label command as a server dry-run, violations of the Pods already in that namespace come out as warnings.
kubectl label --dry-run=server --overwrite ns payments \
pod-security.kubernetes.io/enforce=restricted
There is one more easy-to-miss fact in the documentation. enforce is not applied to workload resources, only to the Pods created as a result. audit and warn also apply to workloads such as Deployments. So for a Deployment with a violating template, kubectl apply succeeds (a warning does appear), and the fact that no Pods are being created surfaces only a few seconds later in the ReplicaSet events. It is a common cause of the inquiry "it was deployed but there are no Pods."
An exemption is written not in a label but in the admission controller configuration file, and it has three axes — username, RuntimeClass name, and namespace. The documentation warns against exempting controller service accounts. If you exempt replicaset-controller, everyone who can create workloads is implicitly exempt.
What you see in the field
First, a team that turned on restricted straight to enforce. All ordinary manifests are blocked. For a team that has never written securityContext, that means "fix every Deployment," and usually the label comes down within a day. The order is to turn on restricted with warn and audit and keep enforce at baseline.
Second, a case where restricted was applied to a system namespace. The moment a CNI agent or node exporter restarts, it cannot come up. The cluster ends up in a state where it cannot repair itself, and you learn the cause is the label only by opening the Pod events.
Third, an outage on cluster upgrade day. Deployments are blocked only in the namespaces that were turned on without -version. Neither the manifests nor the policy changed, so finding the cause takes a long time. Pinning the version is not laziness but change management.
Fourth, how to look with metrics. kube-apiserver emits pod_security_evaluations_total, pod_security_errors_total, and pod_security_exemptions_total. If you put these values on a dashboard during the period you had audit mode on, you can answer "is it okay to raise to enforce?" with numbers.
References
- Pod Security Admission: https://kubernetes.io/docs/concepts/security/pod-security-admission/
- Pod Security Standards: https://kubernetes.io/docs/concepts/security/pod-security-standards/
- Enforce Pod Security Standards with namespace labels: https://kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/
- Migrate from PodSecurityPolicy: https://kubernetes.io/docs/tasks/configure-pod-container/migrate-from-psp/
What you will do in the next lab
You attach labels directly to namespaces in the kwok cluster and go around the switchover procedure once. You first bring up a violating Pod, test the label with --dry-run=server to confirm that the existing violations come out as warnings, see the same Pod pass with only warn and audit on, and then raise to enforce and confirm it is rejected. You place a namespace with -version pinned and a latest namespace side by side, and also build for yourself the scene where a Deployment with a violating template succeeds at apply while its Pods are not created.