Pod Security Standards
The built-in baseline and restricted profiles.
One teammate ships a Deployment that runs as root, turns on privilege escalation, and mounts a slice of the host's filesystem. Nothing stops it. Last lesson you set a tight securityContext on your own pods (a pod is the smallest thing Kubernetes runs, one or more containers together), but that hardening only helps on the pods where somebody remembered to write it. A securityContext is a good habit. It is not a rule. Pod Security Standards turn the habit into a rule the cluster checks at the door, before a single container starts.
Think of three dress codes posted on a room's door: anything goes, sensible, and strict. Pod Security Standards (PSS) are those three dress codes for pods, and they ship as three named profiles. Privileged is unrestricted, meant for trusted infrastructure. Baseline blocks the settings that let a container climb out of its box. The three you meet most are privileged mode, host namespaces, and hostPath mounts (which hand a container part of the node's own disk), and the profile also refuses host ports, any Linux capability added beyond NET_BIND_SERVICE, seccomp turned off with Unconfined, unsafe sysctls, and custom AppArmor or SELinux settings. Restricted is the hardened profile, and it demands exactly what you set by hand last lesson: run as a non-root user, no privilege escalation, drop every Linux capability, seccomp (the kernel's built-in system-call filter) set to RuntimeDefault. Restricted is additive. It keeps everything Baseline blocks and layers the securityContext rules on top. The enforcer is a built-in piece of the API server (the front door every change to the cluster passes through) called Pod Security Admission. It's on by default and went stable in Kubernetes v1.25, the same release that deleted the old PodSecurityPolicy. Nothing to install, no webhook to keep alive.
Labels, modes, and where the check runs
You set profiles per namespace with labels shaped like pod-security.kubernetes.io/<mode>: <level>. Each profile runs in one of three modes, and the modes work independently. Enforce rejects a non-compliant pod outright. Audit lets it through but writes the violation into the API server's audit log. Warn lets it through and hands a warning back to whoever ran the command. Because the modes are independent, you can warn and audit at restricted while enforcing nothing, and that's the whole trick behind a safe rollout. Admission is the stage that runs right after Kubernetes has answered two questions about your request: who are you (authentication) and are you allowed (authorization, usually RBAC, Role-Based Access Control). Pod Security Admission runs there, inside the API server process itself. One label pins the ruleset by version, pod-security.kubernetes.io/enforce-version, so a future cluster upgrade can't quietly tighten what restricted means underneath a running workload. And if a namespace carries no PSS label at all, the cluster default applies, which is privileged unless an admin set a stricter default in the API server config. An unlabeled namespace is wide open. Labeling is opt-in.
apiVersion: v1kind: Namespacemetadata:name: paymentslabels:# start rollout-safe: surface violations, block nothing yetpod-security.kubernetes.io/warn: restrictedpod-security.kubernetes.io/warn-version: v1.31pod-security.kubernetes.io/audit: restrictedpod-security.kubernetes.io/audit-version: v1.31
$ kubectl apply -f ns.yaml$ kubectl get namespace payments --show-labelsnamespace/payments configuredNAME STATUS AGE LABELSpayments Active 6d kubernetes.io/metadata.name=payments,pod-security.kubernetes.io/audit-version=v1.31,pod-security.kubernetes.io/audit=restricted,pod-security.kubernetes.io/warn-version=v1.31,pod-security.kubernetes.io/warn=restricted
Roll it out one label at a time
The safe order never changes. Warn and audit first, watch what complains, fix the manifests, then add enforce. There's a subtlety that catches people. Warn and audit also inspect the pod template buried inside a controller like a Deployment, so applying a bad Deployment gets you a warning right at apply time. Enforce does not do that. Enforce only judges the actual Pod object. So warn is where the early, friendly feedback comes from, and it comes straight off the Deployment you applied.
$ kubectl apply -f payments-deploy.yaml # container runs as root, no securityContext
Warning: would violate PodSecurity "restricted:v1.31": allowPrivilegeEscalation != false (container "api" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "api" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "api" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "api" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")deployment.apps/payments-api created
Before you flip enforce on a live namespace, find out exactly who would break. Changing the enforce label makes Pod Security Admission re-check the pods already running in the namespace, and a server-side dry run shows you that verdict without persisting the label. The dry run runs the real admission code, so its answer matches what a live enforce would do. This is the pre-flight check that keeps enforce from turning into an incident.
$ kubectl label --dry-run=server --overwrite namespace payments \pod-security.kubernetes.io/enforce=restricted \pod-security.kubernetes.io/enforce-version=v1.31
Warning: existing pods in namespace "payments" violate the new PodSecurity enforce level "restricted:v1.31"Warning: payments-web-7d9f5b8c9-4xq2p, payments-api-6c9f8d7b4-jr9lk: allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true, seccompProfilenamespace/payments labeled (server dry run)
That last line says labeled, but the (server dry run) tag is the tell: nothing was persisted. You just got the list of pods that would be refused, for free, before committing. Fix those two workloads, then flip enforce for real and watch where the failure actually lands if you missed one. The Deployment still applies cleanly, because a Deployment is not a Pod. The ReplicaSet controller (the thing a Deployment uses to actually create pods) then tries to make pods from that template, and those get refused. The rejection surfaces as FailedCreate events on the ReplicaSet, not as an error on your apply.
$ kubectl label namespace payments \pod-security.kubernetes.io/enforce=restricted \pod-security.kubernetes.io/enforce-version=v1.31 --overwrite$ kubectl rollout restart deployment/payments-api -n payments$ kubectl get events -n payments --field-selector reason=FailedCreate
namespace/payments labeleddeployment.apps/payments-api restartedLAST SEEN TYPE REASON OBJECT MESSAGE9s Warning FailedCreate replicaset/payments-api-6c9f8d7b4 Error creating: pods "payments-api-6c9f8d7b4-" is forbidden: violates PodSecurity "restricted:v1.31": allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true, seccompProfile
That pairing is the normal production setup. Pod Security Admission covers the hardening baseline for free and in-process, and a policy engine handles every rule that isn't about the security context. One is built in and cheap, the other is flexible. Run both, and 'did the author remember' stops being a question you have to answer at 2am.
Start with warn/audit, then enforce. Jumping straight to restricted breaks DaemonSets and older charts.
Exemptions exist for system namespaces. Do not exempt everything to silence errors.
PSS is not NetworkPolicy. It hardens the pod shape, not who it can talk to, so a pod that passes restricted can still reach every other pod until a NetworkPolicy narrows it.
Try this
Do this on a scratch cluster you can throw away. Label a fresh namespace warn=baseline, apply a privileged pod and read the warning, then switch the same namespace to enforce=baseline and apply the identical file again. Same manifest, same profile, two different answers: that gap is the rollout pattern.
apiVersion: v1kind: Podmetadata:name: probespec:containers:- name: probeimage: busybox:1.36command: ["sleep", "300"]securityContext:privileged: true
$ kubectl create namespace pss-lab$ kubectl label namespace pss-lab pod-security.kubernetes.io/warn=baseline$ kubectl -n pss-lab apply -f probe.yaml
namespace/pss-lab creatednamespace/pss-lab labeledWarning: would violate PodSecurity "baseline:latest": privileged (container "probe" must not set securityContext.privileged=true)pod/probe created
Warn told you and let the pod run. Now delete the pod, switch the same namespace to enforce baseline, and apply the identical file again.
$ kubectl -n pss-lab delete pod probe$ kubectl label namespace pss-lab pod-security.kubernetes.io/enforce=baseline$ kubectl -n pss-lab apply -f probe.yaml$ kubectl delete namespace pss-lab
pod "probe" deletednamespace/pss-lab labeledError from server (Forbidden): error when creating "probe.yaml": pods "probe" is forbidden: violates PodSecurity "baseline:latest": privileged (container "probe" must not set securityContext.privileged=true)namespace "pss-lab" deleted
Takeaway
Pod Security Standards are built-in baselines: privileged, baseline, restricted. Namespace labels enforce them without an external engine.