Originally published on kuryzhev.cloud
Someone on the platform team wrote a script that labeled every namespace with enforce: restricted in one PR, merged it on a Friday, and by Monday half the deploy pipelines were failing with cryptic admission errors. No pods were down — nothing was running yet — but nothing new could ship either. That's the story behind most botched Kubernetes Pod Security Standards rollouts: the policy itself is fine, the sequencing is the problem.
Pod Security Standards (PSS), enforced through the Pod Security Admission (PSA) controller, is one of those Kubernetes features that looks trivial in the docs and then bites you in production because nobody read past the "apply this label" example. Let's go through what it actually checks, where teams get the rollout wrong, and how to do it without a 2am incident.
What Pod Security Standards actually does
PSS defines three built-in profiles — privileged, baseline, and restricted — that describe increasingly strict constraints on pod specs. These aren't runtime rules. There's no sidecar watching your containers, no daemon inspecting syscalls. It's pure admission-time validation inside kube-apiserver, stable and built-in since Kubernetes 1.25, replacing the deprecated PodSecurityPolicy. Nothing to install, nothing to upgrade separately — it ships with the control plane.
Each namespace gets three independent mode labels: enforce, audit, and warn. Each can point to a different profile and even a different pinned version. This independence is the entire rollout lever most teams ignore — you can run warn: restricted for weeks without ever blocking a single deploy, just to see what would break.
What it does not do matters just as much. PSS has zero effect on RBAC, network policy, or image provenance. It doesn't scan running containers for drift. And critically — it only evaluates pods on create or update. A pod that was already running when you tightened the policy stays exactly as it was, non-compliant or not, until the next rollout touches it. That gap is where "we're compliant" turns into "we thought we were compliant."
How people use it wrong
The most common failure is skipping straight to enforcement. A bulk label sweep that sets enforce: restricted across every namespace, with no prior audit/warn signal, breaks CI/CD the moment any Helm chart sets privileged: true, runs as root, or simply omits seccompProfile. And a lot of upstream charts still do exactly that. You find out during a deploy, not during planning.
Second mistake: treating exemptions as permanent. exemptions.usernames and exemptions.runtimeClasses exist for legitimate edge cases — but teams add an exemption to unblock a deploy under pressure and never revisit it. Six months later nobody remembers why it's there, and the policy has quietly stopped protecting the thing it was meant to protect. Every exemption should come with a written reason and an expiry date, full stop.
Third: assuming a label change retroactively fixes existing pods. It doesn't. Watch out — if you flip a namespace to enforce: restricted and nothing in the audit log complains, that just means no new pod was created yet. Old ReplicaSets can sit there indefinitely, non-compliant, invisible to the policy until the next rollout or node reschedule triggers a re-check. Teams report "we're secure now" based on the label existing, not on actual pod state.
The correct approach
Start every namespace in observation mode: enforce at baseline (or unset), audit and warn at restricted. This lets policy violations surface in events and audit logs without blocking anything. Run it for at least one full release cycle before touching enforce.
# Safe rollout pattern: audit + warn first, enforce stays permissive
# until violations are reviewed. Apply per-namespace, not cluster-wide.
apiVersion: v1
kind: Namespace
metadata:
name: payments
labels:
# Pin the version during migration so cluster upgrades don't
# silently change what "restricted" means mid-rollout.
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: v1.31
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.31
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.31
---
# Minimal pod spec that actually passes "restricted" —
# use this as the checklist when fixing violating workloads.
apiVersion: v1
kind: Pod
metadata:
name: compliant-example
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.internal/app:stable
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
Pin enforce-version explicitly during migration instead of leaving it at latest. If you leave it floating and the cluster gets upgraded mid-rollout, the definition of "restricted" can shift under you — new checks get added between minor versions, and a workload that passed last week can suddenly fail after a control plane bump nobody on your team even noticed.
Fix workloads at the source rather than exempting them. Drop hostNetwork, add seccompProfile: RuntimeDefault, set a non-root runAsUser, drop all capabilities and re-add only what's needed (usually nothing, sometimes NET_BIND_SERVICE). This is more work than adding a namespace to an exemption list, but it's the difference between a policy that does something and one that's theater.
Advanced patterns
PSS gives you three fixed profiles. It doesn't know your org requires images from an internal registry, or that every deployment needs a team label, or that resource limits are mandatory. For that, layer a policy engine — Kyverno or OPA/Gatekeeper — on top. Keep the division clean: PSS handles the universal security baseline, the policy engine handles business rules. Don't reimplement "no privileged containers" in Kyverno when PSS already does it for free.
Some workloads are legitimately privileged — CNI plugins, CSI storage drivers, certain monitoring agents that need hostPath or host networking. Don't handle these with broad cluster-wide exemptions. Isolate them into a dedicated namespace labeled enforce: privileged, and lock down who can create pods there with RBAC scoped to that namespace only. The exemption becomes a wall, not a hole.
For GitOps-managed clusters, the real risk is new namespaces appearing without any PSS labels at all — someone self-serves a namespace through Argo CD or Flux and it defaults to whatever the cluster-wide baseline is, which might be nothing. Bake PSS labels into your namespace-provisioning template or enforce them via a mutating admission policy so the gap never reopens as teams create namespaces on their own.
# Rollout checklist — do this in order, per namespace, not all at once
1. Set enforce=baseline (or leave unset), audit=restricted, warn=restricted
2. Deploy normally for one full release cycle
3. Grep audit logs / kubectl get events for "violates PodSecurity"
4. Triage violations:
- Fixable (missing seccompProfile, root user) -> patch chart/manifest
- Genuinely privileged (CNI, storage driver) -> move to dedicated
namespace labeled enforce=privileged, lock down via RBAC
5. Only after violation count hits zero: flip enforce=restricted
6. Set an expiry date on any remaining exemptions.usernames entries
7. Re-run step 3 after next cluster minor upgrade (policy behavior
can shift even with enforce-version pinned, if you bump it)
Performance notes
PSS itself is nearly free — it's one admission controller check inside kube-apiserver, no external webhook round trip, negligible per-request latency. The cost shows up when you stack third-party admission webhooks on top of it. Kyverno and Gatekeeper both add real per-pod-create latency, and if failurePolicy: Fail is set and the webhook pod goes down, every deployment in the cluster stops. Test with failurePolicy: Ignore in staging before you ever flip it to Fail in prod.
Audit mode isn't free either. Turning on cluster-wide audit: restricted on day one in a large cluster generates a real spike in audit log and event volume — enough to move your log ingestion bill if you're shipping everything to a centralized store. Scope audit logging to the namespaces you're actively migrating first, not the whole cluster at once.
One more thing worth saying plainly: baseline is not a security posture, it's a floor. It blocks the obviously dangerous stuff — hostPID, hostNetwork, privileged containers — but it still permits capability sets and configurations that a security team would flag in a real audit. If your compliance checklist says "PSS enabled" and stops there at baseline, you have a checkbox, not a control. restricted — non-root, no privilege escalation, dropped capabilities, seccomp required — is the actual target for anything handling sensitive data.
PSS also won't save you from a bad network path or an over-permissioned service account. Pair any Kubernetes Pod Security Standards rollout with NetworkPolicy and least-privilege RBAC; on its own, PSS is one layer, not a complete posture. We've written more on locking down namespace-level traffic over on kuryzhev.cloud if you're doing both at once.
The short version: don't enforce before you audit, don't exempt without an expiry, and don't confuse a namespace label with a compliant fleet of running pods. Get the sequencing right and PSS becomes background noise — invisible until it catches something real.
Top comments (0)