Kubernetes Pod Security Standards: Replacing PodSecurityPolicy
Pod Security Standards (PSS) replaced PodSecurityPolicy (PSP) in Kubernetes 1.25. Three predefined security profiles — Privileged, Baseline, and Restricted — are enforced via the built-in Pod Security Admission controller, namespace labels, and optionally Kyverno or OPA Gatekeeper for custom policies. This covers the migration from PSP, the production enforcement patterns, and the security contexts that satisfy the Restricted profile without breaking your workloads.

PodSecurityPolicy (PSP) was deprecated in Kubernetes 1.21 and removed in 1.25. Pod Security Standards (PSS) replaced it with three predefined profiles enforced by a built-in admission controller. The profiles are intentionally opinionated — they cover the security posture of most workloads without requiring custom policy authoring.
For policies beyond what PSS profiles express (requiring specific images, enforcing labels, blocking specific capabilities), pair PSS with Kyverno or OPA Gatekeeper.
The Three Profiles
| Profile | Use case | What it prevents |
|---|---|---|
| Privileged | System-level components (CNI plugins, storage drivers, monitoring agents) | Nothing — fully permissive |
| Baseline | Most workloads; blocks known dangerous configurations | hostNetwork, hostPID, hostIPC, hostPorts, privileged containers, NET_RAW and other non-default capabilities, unsafe sysctls, custom /proc mounts. hostPath volumes and allowPrivilegeEscalation are NOT restricted by Baseline — that is a Restricted-only requirement. Use the Restricted profile or Kyverno/OPA to enforce them. |
| Restricted | Security-sensitive workloads; highest security | Everything in Baseline + requires runAsNonRoot: true, requires allowPrivilegeEscalation: false, drops all capabilities, requires seccompProfile.type: RuntimeDefault or Localhost, restricts volumes to configMap, secret, projected, downwardAPI, emptyDir, csi, ephemeral, persistentVolumeClaim |
The Restricted profile is the target for production workloads that don't need node-level access.
Enforcing PSS via Namespace Labels
PSS is enforced by labeling namespaces. Three modes per profile:
- enforce — reject pods that violate the profile
- audit — allow pods but log violations to the audit log
- warn — allow pods but return a warning to the
kubectlcaller
1# Set Restricted enforcement on the payments namespace
2kubectl label namespace payments \
3 pod-security.kubernetes.io/enforce=restricted \
4 pod-security.kubernetes.io/enforce-version=latest \
5 pod-security.kubernetes.io/audit=restricted \
6 pod-security.kubernetes.io/audit-version=latest \
7 pod-security.kubernetes.io/warn=restricted \
8 pod-security.kubernetes.io/warn-version=latestPinning enforce-version=latest uses the current Kubernetes version's definition of Restricted. Use a specific version (e.g., v1.29) to avoid surprise enforcement changes across Kubernetes upgrades.
Security Context: Satisfying the Restricted Profile
A pod that satisfies the Restricted profile:
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: payments-api
5 namespace: payments
6spec:
7 template:
8 spec:
9 # Pod-level security context
10 securityContext:
11 runAsNonRoot: true # Required by Restricted
12 runAsUser: 1000 # UID for the process; must be non-zero
13 runAsGroup: 1000
14 fsGroup: 1000 # Files in mounted volumes owned by this GID
15 seccompProfile:
16 type: RuntimeDefault # Required by Restricted — RuntimeDefault or Localhost are both accepted
17
18 containers:
19 - name: payments-api
20 image: payments-api:1.2.0
21 # Container-level security context
22 securityContext:
23 allowPrivilegeEscalation: false # Required by Restricted
24 readOnlyRootFilesystem: true # Best practice; NOT required by PSS Restricted — enforce via Kyverno if needed
25 capabilities:
26 drop:
27 - ALL # Required by Restricted — drop all Linux capabilities
28 add: [] # Add back only what you need (NET_BIND_SERVICE for port < 1024)
29
30 # If the container needs to write files, use emptyDir volumes
31 volumeMounts:
32 - name: tmp
33 mountPath: /tmp
34 - name: cache
35 mountPath: /app/cache
36
37 volumes:
38 - name: tmp
39 emptyDir: {}
40 - name: cache
41 emptyDir: {}Common capabilities to add back when needed:
| Capability | Needed for |
|---|---|
NET_BIND_SERVICE | Binding to ports < 1024 (HTTP on port 80/443) |
NET_RAW | Raw socket access (ping, some network tools) |
CHOWN | Changing file ownership (avoid — run as correct UID instead) |
For most web services: drop ALL, add nothing. Run the application on port 8080 instead of 80.
Cluster-Wide Defaults
Set a cluster-wide default policy in the Pod Security Admission configuration. This applies to namespaces that don't have explicit PSS labels:
1# /etc/kubernetes/psa.yaml — passed to the API server via --admission-control-config-file
2apiVersion: apiserver.config.k8s.io/v1
3kind: AdmissionConfiguration
4plugins:
5 - name: PodSecurity
6 configuration:
7 apiVersion: pod-security.admission.config.k8s.io/v1
8 kind: PodSecurityConfiguration
9 defaults:
10 enforce: baseline # Default enforcement for unlabeled namespaces
11 enforce-version: latest
12 audit: restricted
13 audit-version: latest
14 warn: restricted
15 warn-version: latest
16 exemptions:
17 # Exempt system namespaces from enforcement
18 namespaces:
19 - kube-system
20 - kube-public
21 - monitoring
22 - cert-managerOn EKS, you can't pass custom API server flags. Use namespace labels directly, and rely on Kyverno or OPA Gatekeeper to enforce organization-wide baselines across all namespaces.
Audit Mode First: Finding Violations Before Enforcing
Before switching to enforce, run audit and warn to find violations:
1# Add audit and warn labels without enforcing — see what would fail
2kubectl label namespace payments \
3 pod-security.kubernetes.io/audit=restricted \
4 pod-security.kubernetes.io/warn=restricted
5
6# Look at audit logs for violations
7# On EKS (CloudWatch Logs):
8aws logs filter-log-events \
9 --log-group-name "/aws/eks/production/cluster" \
10 --filter-pattern '{ $.annotations["pod-security.kubernetes.io/audit-violations"] = "*" }'
11
12# Locally — kubectl apply will print warnings for non-compliant pods:
13# Warning: would violate PodSecurity "restricted:latest": ...Switch to enforce only after resolving all violations. Common violations to fix before enforcing Restricted:
- Running as root (UID 0) — fix: add
runAsNonRoot: true, set a non-zerorunAsUser - Missing
seccompProfile— fix: addseccompProfile.type: RuntimeDefault allowPrivilegeEscalationnot set to false — fix: addallowPrivilegeEscalation: false- Capabilities not dropped — fix: add
capabilities.drop: [ALL]
Helm Chart Compatibility
Many Helm charts ship with security contexts that don't satisfy Restricted. The kube-prometheus-stack, for example, uses hostNetwork: true for some components and requires the monitoring namespace to be Privileged or Baseline. Check the chart's documentation for the minimum PSS profile it supports.
For charts you control, add securityContext values to the values.yaml defaults:
1# values.yaml defaults that satisfy Restricted
2podSecurityContext:
3 runAsNonRoot: true
4 runAsUser: 1000
5 fsGroup: 1000
6 seccompProfile:
7 type: RuntimeDefault
8
9securityContext:
10 allowPrivilegeEscalation: false
11 readOnlyRootFilesystem: true
12 capabilities:
13 drop:
14 - ALLKyverno for Policies Beyond PSS Profiles
PSS profiles cover privilege escalation. For policies that PSS doesn't express — requiring specific image registries, enforcing resource limits on all pods, blocking latest tags — use Kyverno:
1# Block pods that don't set CPU/memory limits (PSS doesn't require this)
2apiVersion: kyverno.io/v1
3kind: ClusterPolicy
4metadata:
5 name: require-resource-limits
6spec:
7 validationFailureAction: Enforce
8 rules:
9 - name: check-cpu-memory-limits
10 match:
11 any:
12 - resources:
13 kinds: [Pod]
14 exclude:
15 any:
16 - resources:
17 kinds: [Pod]
18 namespaces: ["kube-system", "kube-public", "kube-node-lease"]
19 validate:
20 message: "CPU and memory limits are required on all containers"
21 pattern:
22 spec:
23 containers:
24 - resources:
25 limits:
26 cpu: "?*"
27 memory: "?*"PSS handles the security posture (privilege escalation, host access); Kyverno handles operational policies (resource limits, image registries, required labels). They complement each other.
Secure by Default: Kyverno for PSS Mutation
In 2026, rather than blocking non-compliant pods, platform teams use Kyverno to mutate them into compliance. This "secure by default" pattern automatically injects the required securityContext fields, allowing developers to focus on code rather than security YAML:
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: mutate-pss-restricted
5spec:
6 rules:
7 - name: set-restricted-context
8 match:
9 any:
10 - resources:
11 kinds: [Pod]
12 namespaces: ["payments", "orders"]
13 mutate:
14 patchStrategicMerge:
15 spec:
16 securityContext:
17 runAsNonRoot: true
18 seccompProfile:
19 type: RuntimeDefault
20 containers:
21 - name: "*"
22 securityContext:
23 allowPrivilegeEscalation: false
24 capabilities:
25 drop: [ALL]By mutating pods to meet the Restricted profile, you enforce a high security baseline across the cluster without requiring every team to update their Helm charts manually.
Frequently Asked Questions
What's the difference between seccompProfile.type: RuntimeDefault and Localhost?
RuntimeDefault uses the container runtime's built-in seccomp profile (the one Docker/containerd ships with). It blocks ~300 system calls not needed by typical applications. Localhost points to a custom seccomp profile file on the node. RuntimeDefault is the right starting point for most workloads — it satisfies the Restricted profile requirement and reduces syscall attack surface without requiring custom profiles.
Does PSS enforce security contexts on init containers?
Yes. The Pod Security admission controller checks init containers, ephemeral containers, and regular containers — all must satisfy the profile's requirements. A pod with a compliant main container but a non-compliant init container will be rejected.
How do I handle a third-party container that runs as root?
Three options: (1) Add runAsUser to override the UID at the pod/container level — works if the container doesn't require root privileges for its actual function, just was built with root as default. (2) Use the Baseline profile for that namespace instead of Restricted. (3) Run the container in a dedicated namespace with a more permissive profile while enforcing Restricted everywhere else. Option 1 is best when feasible — check that the application actually works as non-root.
For admission webhook policies that complement PSS (enforcing image registries, blocking privileged namespaces), see Kubernetes Admission Webhooks: OPA Gatekeeper and Kyverno. For RBAC policies that limit who can create privileged pods, see Kubernetes RBAC Advanced Patterns. For supply chain security and image signing that pairs with PSS enforcement, see Container Image Security: Supply Chain from Build to Production.
Migrating from PodSecurityPolicy to PSS or enforcing the Restricted profile across a multi-team cluster? Talk to us at Coding Protocols — we help platform teams implement security baselines that don't break running workloads.


