Kubernetes removed PodSecurityPolicy in v1.25, yet plenty of clusters still run workloads that can escalate privileges, mount the host filesystem, or execute as root by default. Kubernetes Pod Security Standards define the built-in replacement: three profiles — privileged, baseline, and restricted — enforced natively through the Pod Security admission controller, no third-party webhook required. If you're still running PodSecurityPolicy, have no pod-level hardening at all, or aren't sure which namespaces are actually protected, this guide walks through implementing Kubernetes Pod Security Standards end to end. You'll audit your current posture, roll out enforcement safely using warn and audit modes, apply the restricted pod security policy profile where it matters most, and complete a clean k8s PSP migration guide path without breaking production deployments along the way.
Step 1: Understand the Kubernetes Pod Security Standards Profiles
Before touching a single namespace label, know what you're enforcing. Kubernetes Pod Security Standards define exactly three profiles, each stricter than the last:
- Privileged — unrestricted. No policy applied; equivalent to having no controls at all. Reserve this for system namespaces like
kube-systemor CNI/CSI daemonsets that genuinely need host access. - Baseline — blocks known privilege escalations while staying compatible with most common workloads. Disallows host namespaces, privileged containers, and most dangerous capabilities, but still allows things like running as root.
- Restricted — the hardened profile. Requires running as non-root, drops all Linux capabilities by default, enforces
seccompProfile: RuntimeDefault, disallows privilege escalation, and requires a read-only root filesystem posture for volumes where applicable.
Most teams target restricted for application namespaces and baseline for anything that needs a bit more flexibility (init containers doing setup work, sidecars with legacy assumptions). Very few workloads legitimately need privileged.
Step 2: Audit Your Cluster's Current Pod Security Posture
Don't guess — measure. Start by checking what's actually running that would violate restricted today. kubectl won't tell you this directly, so use kubectl-who-can style introspection or, more simply, dry-run the labels before you apply them:
# See which namespaces exist and whether they already carry PSS labels
kubectl get ns --show-labels | grep pod-security
# Dry-run "restricted" against a namespace without enforcing it yet
kubectl label --dry-run=server --overwrite ns my-app \
pod-security.kubernetes.io/enforce=restricted
The dry-run above triggers the admission controller's validation logic and returns warnings for every pod spec that would fail, without actually blocking anything. Run this against every application namespace and capture the output — it's your remediation backlog. Common findings at this stage include containers running as root without an explicit runAsNonRoot: true, missing seccompProfile entries, and containers requesting NET_ADMIN or other capabilities they don't need.
Step 3: Turn On the Pod Security Admission Controller in Audit/Warn Mode
The Pod Security admission controller has been built into the API server since Kubernetes 1.23 (stable since 1.25) — there's no separate component to install. It's driven entirely by namespace labels, which makes rollout low-risk if you sequence it correctly. Start with warn and audit, not enforce:
apiVersion: v1
kind: Namespace
metadata:
name: my-app
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/audit: restricted
This configuration enforces the safer baseline profile immediately (low blast radius) while surfacing every restricted violation as a client-side warning and an audit log entry. Apply it cluster-wide with a script rather than by hand:
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
kubectl label ns "$ns" \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted \
--overwrite
done
Give this a full deploy cycle (at least one release for every service) so warnings surface against real, current pod specs rather than stale ones.
Step 4: Enforce the Restricted Pod Security Policy Profile
Once warnings are clean, or your engineers have fixed the flagged pod specs, flip enforce to restricted namespace by namespace, starting with lower environments:
kubectl label ns staging-checkout \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest \
--overwrite
Pin enforce-version to latest (or a specific Kubernetes minor version) so behavior doesn't silently shift on cluster upgrades. For workloads that now fail, the fix is usually one of these additions to the pod or container securityContext:
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
capabilities:
drop: ["ALL"]
allowPrivilegeEscalation: false
Roll this out namespace-by-namespace rather than cluster-wide in one shot. A staged rollout with monitoring in between is what separates a smooth restricted pod security policy adoption from an incident.
Step 5: Complete Your k8s PSP Migration Guide Path
If you're coming from PodSecurityPolicy, don't just delete PSP objects and hope PSS labels catch everything — the two systems don't map one-to-one, and PSP admission controllers can leave orphaned ClusterRoleBindings referencing use verbs on policies that no longer matter. The Kubernetes project publishes a mapping between common PSP fields and their PSS equivalents; use it as your migration checklist:
- Inventory every
PodSecurityPolicyobject and whichClusterRole/ClusterRoleBindingpairs grantuseaccess to it. - For each PSP, identify the closest matching PSS profile (most locked-down custom PSPs map to
restricted; permissive ones often land closer tobaseline). - Apply the equivalent PSS labels in
warn/auditmode first (Step 3) before removing the PSP. - Once
enforceis live and clean, delete the PSP objects and their bindings:
kubectl delete psp <policy-name>
kubectl delete clusterrole <policy-name>-psp-user
kubectl delete clusterrolebinding <policy-name>-psp-binding
- Confirm the
PodSecurityPolicyadmission plugin is removed from the API server's--enable-admission-pluginsflag (it's gone by default since 1.25, but managed-cluster configs sometimes carry stale flags forward).
Skipping the overlap window is the single most common cause of migration outages — workloads that passed PSP validation can still fail PSS if a custom PSP allowed something baseline or restricted blocks by default.
Step 6: Handle Legitimate Exceptions Without Reopening the Cluster
Some workloads — CNI plugins, storage drivers, monitoring agents — legitimately need elevated permissions. Don't loosen the whole namespace to accommodate them; isolate them instead:
metadata:
labels:
pod-security.kubernetes.io/enforce: privileged
Put these workloads in a dedicated, tightly access-controlled namespace (e.g., kube-infra) rather than mixing them with application pods. Combine the namespace-level exception with RBAC restrictions on who can deploy into it, and alert on any new workload landing there.
Verification and Troubleshooting
After rollout, verify enforcement is actually active rather than assuming the labels took effect:
# Confirm labels are set as expected
kubectl get ns my-app -o jsonpath='{.metadata.labels}'
# Try to create a pod that violates "restricted" — it should be rejected
kubectl run test-priv --image=nginx --overrides='{"spec":{"containers":[{"name":"test-priv","image":"nginx","securityContext":{"privileged":true}}]}}' -n my-app
You should see an admission error referencing the violated PSS control (e.g., privileged or allowPrivilegeEscalation). If the pod is created instead of rejected, check for:
- Wrong label key — it's
pod-security.kubernetes.io/enforce, notsecurity.kubernetes.ioor a leftover PSP-era annotation. - Exempted namespace or user — check the API server's
--admission-control-config-filefor aPodSecurityconfiguration withexemptionsthat may cover your namespace, username, or runtime class. - Version skew —
enforce-versionpinned to an older Kubernetes minor version may not yet include a newer restricted-profile check. - Webhook ordering conflicts — if another mutating webhook rewrites
securityContextafter the Pod Security admission controller runs, validation results can look inconsistent between dry-run and actual apply. Check webhookadmissionReviewVersionsordering inkubectl get mutatingwebhookconfigurations -o yaml.
For ongoing verification, add a CI check that runs the dry-run label command from Step 2 against manifests before merge, so regressions are caught before they reach the cluster rather than at deploy time.
How Safeguard Helps
Rolling out Kubernetes Pod Security Standards correctly requires knowing not just what's enforced today, but what changed since yesterday's deploy — a single new sidecar or a Helm chart bump can quietly reintroduce a root-running container into an otherwise hardened namespace. Safeguard continuously scans your cluster and CI/CD pipelines for the exact conditions the restricted profile targets — privileged containers, missing seccomp profiles, writable root filesystems, and excess capabilities — and maps findings back to the specific commit, image, or manifest that introduced them.
Rather than waiting for an admission rejection in production, Safeguard flags violations at build and pull-request time, so engineers fix pod specs before they ever hit a namespace with enforce=restricted. For teams still mid-migration off PodSecurityPolicy, Safeguard's policy engine can run the same checks as your target PSS profile against pre-merge manifests, giving you a live view of migration readiness across every namespace instead of a one-time audit. Combined with Safeguard's software supply chain visibility — tracking what's actually running back to source and build provenance — teams get both the enforcement layer Kubernetes provides natively and the earlier-stage visibility needed to keep pod security posture from drifting between reviews.