CoursesKubernetes administrationPod Security Standards

Pod Security Standards

The built-in baseline and restricted profiles.

Advanced10 min · lesson 50 of 65
In plain terms
Pod Security Standards are three house dress codes — anything-goes, sensible, and strict — posted on a room’s door. Turn on “warn” first so people see the sign before you start turning anyone away.

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.

ns.yaml
apiVersion: v1
kind: Namespace
metadata:
name: payments
labels:
# start rollout-safe: surface violations, block nothing 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
terminal
$ kubectl apply -f ns.yaml
$ kubectl get namespace payments --show-labels
namespace/payments configured
NAME STATUS AGE LABELS
payments 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.

terminal
$ kubectl apply -f payments-deploy.yaml # container runs as root, no securityContext
terminal
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.

terminal
$ kubectl label --dry-run=server --overwrite namespace payments \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=v1.31
terminal
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, seccompProfile
namespace/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.

terminal
$ 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
terminal
namespace/payments labeled
deployment.apps/payments-api restarted
LAST SEEN TYPE REASON OBJECT MESSAGE
9s 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
New namespace: which profile?
New namespace, pick a profile
set pod-security.kubernetes.io/enforce
network/storage plugins, node agents
privileged
needs host access; keep it to system namespaces only
general apps, mixed images
baseline
blocks privileged mode, host namespaces, hostPath, host ports and more; broadly compatible
your own hardened services
restricted
non-root, drop ALL caps, seccomp RuntimeDefault; the target for app workloads
Whichever you choose, add warn and audit at the same level first, fix the violators the dry run lists, then add enforce.
Enforce gates pods, not the Deployment
Flip enforce=restricted blind on a live namespace and kubectl apply of the Deployment returns a cheerful success while zero pods come up. Enforce refuses the pods the ReplicaSet tries to create, and that only shows up in ReplicaSet FailedCreate events or a Deployment stuck at 0 available, never as an error on the apply that caused it. Always warn and audit first, run the server-side dry run to list violators, remediate, then enforce. And know PSS's ceiling: it only judges the pod security context, so rules like 'images only from our registry' or 'every pod needs an owner label' need a policy engine such as Kyverno or OPA Gatekeeper (Open Policy Agent) running as an admission webhook beside it.

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.

probe.yaml
apiVersion: v1
kind: Pod
metadata:
name: probe
spec:
containers:
- name: probe
image: busybox:1.36
command: ["sleep", "300"]
securityContext:
privileged: true
terminal
$ kubectl create namespace pss-lab
$ kubectl label namespace pss-lab pod-security.kubernetes.io/warn=baseline
$ kubectl -n pss-lab apply -f probe.yaml
terminal
namespace/pss-lab created
namespace/pss-lab labeled
Warning: 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.

terminal
$ 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
terminal
pod "probe" deleted
namespace/pss-lab labeled
Error 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.

Quick check
01You flip pod-security.kubernetes.io/enforce=restricted on a namespace whose Deployment still runs as root. kubectl apply of that Deployment returns success, yet no pods are running. Why?
Incorrect — Enforce never evicts running pods; it only gates admission of new ones. Existing pods keep running until their next recreate.
Correct — The Deployment object is admitted because it isn't a Pod; its ReplicaSet then fails to create the violating pods, visible via kubectl get events, not the apply.
Incorrect — The label is valid and active; an invalid label would be rejected or simply do nothing, and here it's actively blocking the pods.
Incorrect — Pod Security Admission is built into the API server since v1.25. There's no webhook to install for PSS.
02A namespace is labeled pod-security.kubernetes.io/enforce=baseline. Someone applies a Pod whose container runs as root (uid 0) but sets nothing else that baseline blocks: no privileged mode, no host namespaces, no hostPath. Is the Pod admitted?
Incorrect — requiring non-root belongs to the restricted profile, not baseline.
Incorrect — baseline doesn't demand a securityContext, it only blocks a specific set of dangerous settings.
Incorrect — enforce does reject non-compliant Pods; this one is admitted because it breaks no baseline rule.
Correct — baseline stops a container climbing out of its box but tolerates root. Its block list is longer than these examples, and requiring non-root is what restricted layers on top.
03Before you set enforce=restricted on a live namespace, you want the exact list of already-running Pods that would be refused, without persisting anything. What gives you that?
Correct — a server-side dry run re-checks running Pods through the actual admission logic and prints the ones that would fail, changing nothing.
Incorrect — a client dry run never reaches the API server's admission code, so it can't tell you which Pods would be refused.
Incorrect — that warns on the Deployment's template, not on the set of Pods already running that enforce would re-check.
Incorrect — manual inspection is error-prone and never runs the admission logic that decides the real verdict.

Related