Safeguard
Container Security

How to implement Kubernetes Pod Security Standards

A step-by-step guide to implementing Kubernetes Pod Security Standards, from auditing pods to enforcing restricted mode and migrating off PSP.

Karan Patel
Cloud Security Engineer
8 min read

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-system or 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:

  1. Inventory every PodSecurityPolicy object and which ClusterRole/ClusterRoleBinding pairs grant use access to it.
  2. For each PSP, identify the closest matching PSS profile (most locked-down custom PSPs map to restricted; permissive ones often land closer to baseline).
  3. Apply the equivalent PSS labels in warn/audit mode first (Step 3) before removing the PSP.
  4. Once enforce is 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
  1. Confirm the PodSecurityPolicy admission plugin is removed from the API server's --enable-admission-plugins flag (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, not security.kubernetes.io or a leftover PSP-era annotation.
  • Exempted namespace or user — check the API server's --admission-control-config-file for a PodSecurity configuration with exemptions that may cover your namespace, username, or runtime class.
  • Version skewenforce-version pinned to an older Kubernetes minor version may not yet include a newer restricted-profile check.
  • Webhook ordering conflicts — if another mutating webhook rewrites securityContext after the Pod Security admission controller runs, validation results can look inconsistent between dry-run and actual apply. Check webhook admissionReviewVersions ordering in kubectl 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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.