Pod Security Standards (PSS)
“https://kubernetes.io/docs/concepts/security/pod-security-standards/”
Pod Security Standards (PSS) are three predefined security profiles for Pods — privileged, baseline, and restricted. They’re applied at the namespace level via labels, and any Pod that doesn’t meet the standard is rejected (or warned) at admission. PSS replaced the deprecated PodSecurityPolicy (PSP) in k8s 1.25+. It covers the workload hardening part of the L07 layer; the API access and network parts are in other notes.
Table of Contents
- The Three Profiles
- The Three Modes (Enforce / Audit / Warn)
- How PSS Is Enforced
- The Privileged Profile
- The Baseline Profile in Depth
- The Restricted Profile in Depth
- Namespace Label Syntax
- The Profile Versions
- The Migration Cookbook
- Per-Namespace Patterns
- The PSS vs PSP Differences
- PSS vs OPA / Kyverno
- Common Exceptions
- Operations and Debugging
- Gotchas and Common Mistakes
1. The Three Profiles
| Profile | Intended for | What it allows |
|---|---|---|
privileged | System / infrastructure workloads | Essentially unrestricted |
baseline | Default for most namespaces | Prevents known privilege escalations |
restricted | Hardened, security-sensitive namespaces | Strict — k8s best practices |
The profiles are cumulative: restricted is a superset of baseline, which is a superset of privileged. A Pod that meets restricted also meets baseline and privileged.
The decision:
privilegedforkube-system(system Pods need full access).baselinefor dev / test namespaces (some flexibility).restrictedfor production (the safe default).
2. The Three Modes (Enforce / Audit / Warn)
PSS has three modes for each profile:
enforce— reject violating Pods. The admission controller denies them.audit— allow but log violations. The audit log has the violation.warn— allow but show a warning to the user viakubectl.
The standard pattern:
# all three modes at the same profile
kubectl label ns production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/enforce-version=latestenforce— actual enforcement.audit— log violations (so you can see them in the audit log).warn— UX for users; they see a warning when they try to deploy a violating Pod.
The modes are independent. You can have enforce: baseline, audit: restricted (enforce baseline, but log restricted violations as well). This is for the migration phase.
3. How PSS Is Enforced
PSS is enforced by the Pod Security admission controller, which is built into the apiserver. It’s enabled by default in k8s 1.25+ (and was in alpha/beta before).
The flow:
- A Pod is created (or updated).
- The apiserver’s
PodSecurityadmission plugin runs. - For each namespace label (
enforce,audit,warn), the plugin checks the Pod against the corresponding profile. - If
enforceis violated, the Pod is rejected. - If
auditis violated, the violation is added to the audit log. - If
warnis violated, the admission response includes a warning.
The plugin is fast (in-process, no external call). It’s evaluated on every Pod creation / update.
3.1 The PodSecurity admission
The plugin is in --enable-admission-plugins=PodSecurity. It’s enabled by default.
The plugin reads the namespace’s labels:
pod-security.kubernetes.io/enforce—privileged/baseline/restrictedpod-security.kubernetes.io/enforce-version—latest/v1.30/ etc.pod-security.kubernetes.io/audit— samepod-security.kubernetes.io/audit-version— samepod-security.kubernetes.io/warn— samepod-security.kubernetes.io/warn-version— same
If the label is not set, the mode is disabled for that profile (no enforcement, no audit, no warn).
4. The Privileged Profile
privileged is the escape hatch. It blocks nothing. Any Pod is allowed.
kubectl label ns kube-system pod-security.kubernetes.io/enforce=privilegedUse for:
kube-system(system Pods).monitoring(Prometheus, Grafana, etc.).- Any namespace with system-level infrastructure.
If you set enforce: privileged, the namespace has no PSS enforcement. It’s not a “no security” setting per se — other policies (NetworkPolicy, RBAC, etc.) still apply. But PSS doesn’t add any checks.
5. The Baseline Profile in Depth
baseline is the default for most namespaces. It prevents known privilege escalations but allows common patterns (root, default hostPath for logs, etc.).
The baseline profile blocks (in detail):
privileged: true— privileged containers are rejected.hostNetwork: true— Pods sharing the host’s network are rejected.hostPID: true— Pods sharing the host’s PID namespace are rejected.hostIPC: true— Pods sharing the host’s IPC namespace are rejected.hostPathvolumes — almost allhostPathmounts are rejected. Exception: a few safe read-only paths (none by default; depends on the k8s version).- Specific capabilities —
SYS_ADMIN,NET_ADMIN,SYS_MODULE,SYS_RAWIO,SYS_PTRACE,SYS_BOOT, etc. (about 25 capabilities). - Specific procMount values —
procMount: Unmaskedis blocked. - Specific AppArmor profiles —
unconfinedis blocked (the default isruntime/default, which is allowed). - Specific SELinux options — custom user / role / type / level is blocked.
baseline allows:
runAsUser: 0(root).readOnlyRootFilesystem: false(the default).allowPrivilegeEscalation: true(the default).seccompProfile.type: Unconfined(the default).- Most other “less than ideal” settings.
baseline is for app namespaces that can’t meet restricted. Common reasons:
- The app needs to write to
/(legacy daemon). - The app needs root (no USER set in the image).
- The app uses
hostPath(e.g. for/devaccess).
6. The Restricted Profile in Depth
restricted is the safe default for new clusters. It enforces the k8s best practices.
The restricted profile blocks everything baseline blocks, plus:
runAsNonRoot: trueis required (must be set in the SecurityContext, OR the image’sUSERmust be non-root, ORrunAsUsermust be set to non-zero).seccompProfile.typemust beRuntimeDefaultorLocalhost(notUnconfined).allowPrivilegeEscalation: falseis required.capabilities.dropmust includeALL.capabilities.addis restricted to a small allow list:NET_BIND_SERVICE.
The restricted profile is strict. A Pod that meets restricted:
- Runs as non-root.
- Has all capabilities dropped (except the explicit allow list).
- Has a seccomp filter.
- Has no privilege escalation.
- Has no host namespaces.
- Has no privileged flag.
- Has a read-only root filesystem (recommended, not required).
A Pod that meets restricted is generally considered safe to run.
7. Namespace Label Syntax
The labels are well-known keys. The values are the profile name (or latest for the version):
# the most common setup
kubectl label ns production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/audit-version=latest \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/warn-version=latestThe *-version label controls the PSS version. latest means “use the version of the apiserver”. A specific version (e.g. v1.30) means “use the rules as defined in PSS v1.30”. Pin the version for production.
7.1 The label keys
| Key | Purpose | Values |
|---|---|---|
pod-security.kubernetes.io/enforce | Hard enforcement (reject) | privileged / baseline / restricted |
pod-security.kubernetes.io/enforce-version | The version for enforce | latest / v1.X |
pod-security.kubernetes.io/audit | Log violations | same as enforce |
pod-security.kubernetes.io/audit-version | Version for audit | same |
pod-security.kubernetes.io/warn | Warn the user | same |
pod-security.kubernetes.io/warn-version | Version for warn | same |
7.2 The version pinning
The *-version label pins the profile version. The default (no version) is latest, which is the version of the apiserver you’re using.
For production, pin the version. A k8s upgrade may tighten the profile (e.g. add a new check). If you’re on latest, the new check applies immediately. If you’re on v1.28, the old rules apply.
The version format:
latest— current apiserver version.v1.X— the version of PSS in k8s 1.X.
The official PSS versions match k8s releases. v1.30 is the PSS in k8s 1.30.
8. The Profile Versions
PSS has versions that match k8s releases:
v1.22— initial GA.v1.23— no changes.v1.24— no changes.v1.25—restrictedaddsrunAsNonRoot: trueenforcement, additionalcapabilities.addchecks.v1.26—unhealthyPodEvictionPolicy(separate feature).v1.27— no major changes.v1.28— additionalhostPathrestrictions inbaseline.v1.29— additionalseccompProfilechecks.v1.30— additional checks.
The versions are cumulative — v1.30 includes all v1.22 rules plus the additions.
9. The Migration Cookbook
The standard migration from a permissive cluster to restricted:
9.1 Phase 1: Audit (no enforcement)
# turn on audit for all production namespaces
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
kubectl label ns $ns \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/audit-version=latest \
--overwrite
done- No Pod is rejected.
- The audit log has violations.
- Use the log to see what’s broken.
9.2 Phase 2: Warn (UX)
# turn on warn for the same namespaces
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
kubectl label ns $ns \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/warn-version=latest \
--overwrite
done- Users see warnings when they deploy violating Pods.
- The CI can fail on warnings (with
kubectl apply --dry-run=server).
9.3 Phase 3: Fix
For each violation, fix the Pod:
runAsNonRoot: true— add to SecurityContext, or build a non-root image.seccompProfile— addseccompProfile: type: RuntimeDefault.allowPrivilegeEscalation: false— add to SecurityContext.capabilities.drop: [ALL]— add to SecurityContext.
Or, mark the namespace as a documented exception (baseline or privileged).
9.4 Phase 4: Enforce
# turn on enforce
kubectl label ns production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest \
--overwrite- Violating Pods are rejected.
- New violations are blocked immediately.
- The cluster is hardened.
The transition is per-namespace. Don’t enforce on a namespace with a non-compliant workload — fix the workload first.
10. Per-Namespace Patterns
The standard pattern for a cluster:
# kube-system
metadata: { labels: { pod-security.kubernetes.io/enforce: privileged } }
# production
metadata: { labels: { pod-security.kubernetes.io/enforce: restricted, pod-security.kubernetes.io/enforce-version: latest } }
# staging
metadata: { labels: { pod-security.kubernetes.io/enforce: baseline } }
# dev
metadata: { labels: { pod-security.kubernetes.io/enforce: baseline } }
# monitoring (Prometheus, etc.)
metadata: { labels: { pod-security.kubernetes.io/enforce: baseline } }kube-system is privileged because system Pods (CNI, storage, etc.) need access. production is restricted (the safe default). staging and dev are baseline (more flexibility for dev work).
For multi-tenant clusters:
- Each tenant’s namespace is
restrictedby default. - Documented exceptions get
baseline. - A privileged workload (e.g. CI runner) gets
privilegedand is locked down by NetworkPolicy and RBAC.
11. The PSS vs PSP Differences
PSP (PodSecurityPolicy) was the older, more flexible mechanism. PSS replaced it. The differences:
| PSP | PSS | |
|---|---|---|
| Status | Deprecated in 1.21, removed in 1.25 | Active, GA |
| Configuration | Custom per-policy | Three predefined profiles |
| Mechanism | RBAC (the user must be allowed to use a PSP) | Namespace labels |
| Flexibility | Highly configurable | Fixed rules |
| Use case | Custom policies | Standard hardening |
PSP allowed per-user policies (a user could use a specific PSP based on RBAC). PSS is per-namespace (the namespace determines the policy).
For most clusters, PSS is enough. For complex custom policies (e.g. “only the security team can deploy privileged containers”), use OPA / Kyverno alongside PSS.
12. PSS vs OPA / Kyverno
PSS is built into the apiserver. It only checks a fixed set of fields (the PSS rules). It’s fast (in-process) and free.
OPA / Kyverno / Gatekeeper check arbitrary constraints. They’re extensible but slower (separate process) and require configuration.
The standard pattern:
- PSS for the baseline (privileged containers, host namespaces, etc.). Built-in, free.
- OPA / Kyverno for organization-specific rules (e.g. “every image must come from our registry”, “every Pod must have these labels”). Extensible.
PSS + OPA / Kyverno is the standard “defense in depth” for admission.
13. Common Exceptions
Workloads that can’t meet restricted and need baseline or privileged:
- CNI plugins (Calico, Cilium, Weave) — need
hostNetwork,privileged. - GPU device plugins (NVIDIA) — need
privilegedfor GPU access. - Storage daemons (some CSI drivers) — need
hostPathfor device access. - Monitoring agents (Prometheus node-exporter, Datadog agent) — need
hostPathfor/proc,/sys. - Logging agents (Fluentd, Vector) — need
hostPathfor log dirs. - Init containers that do migrations — may need
hostPathfor backup/restore.
For these, document the exception and set the namespace to baseline or privileged. The exception should be scoped (only the system Pods, not all Pods in the namespace).
14. Operations and Debugging
14.1 Common commands
# list namespaces and their PSS labels
kubectl get ns -L pod-security.kubernetes.io/enforce,pod-security.kubernetes.io/warn,pod-security.kubernetes.io/audit
# describe a namespace
kubectl describe ns <ns>
# look at the labels
# try a Pod against a PSS namespace (dry-run)
kubectl apply -f pod.yaml --dry-run=server
# the response includes warnings if it violates
# check the audit log for violations
# (the audit log has the violation message)14.2 The “Pod rejected by PSS” case
A Pod creation fails with “violates PodSecurity “baseline:latest"" or similar.
# 1. Read the error message
kubectl describe pod <pod>
# the error says which field violates which profile
# 2. Fix the Pod
# - add runAsNonRoot: true
# - add seccompProfile.type: RuntimeDefault
# - drop capabilities
# etc.
# 3. Re-apply
kubectl apply -f pod.yaml14.3 The “PSS isn’t enforcing” case
PSS labels are set, but violations aren’t rejected.
# 1. Check the apiserver's enabled admission plugins
kubectl -n kube-system get pod kube-apiserver-<node> -o yaml | grep admission
# PodSecurity should be in the list
# 2. Check the namespace's labels
kubectl get ns <ns> -o jsonpath='{.metadata.labels}'
# the enforce label should be set
# 3. Check the apiserver's version
kubectl version
# PodSecurity is GA in 1.25+15. Gotchas and Common Mistakes
15.1 The 30+ common mistakes
-
PSS is admission-time only. Once a Pod is admitted, PSS doesn’t enforce anything at runtime. The kubelet doesn’t kill a Pod mid-flight for violating PSS.
-
The
warnandauditmodes are different.warnshows akubectl-side message;auditadds an entry to the cluster’s audit log. -
The
latestversion is not always what you want. Pin a specific version (e.g.v1.28) for production. -
PSS replaced PSP. PSP was deprecated in 1.21 and removed in 1.25. Don’t write new PSPs. PSS is the way.
-
PSS doesn’t replace NetworkPolicy. A
restrictedPod can still talk to anything. Pair with NetworkPolicy. -
The default namespace has no PSS labels. It inherits the cluster’s default, which is “allow everything”. Don’t deploy to
default. Make a namespace, label itrestricted, deploy there. -
"restricted"is a moving target. Each k8s release may tighten the rules. Pin a version, or expect warnings on k8s upgrades. -
Some legit workloads can’t meet
restricted. A workload that genuinely needs to write to/(legacy daemon) or run as root — these needbaselineorprivileged, and a documented exception. -
baselineallows root,restricteddoesn’t. A common migration: switch frombaselinetorestrictedand discover that every Pod runs as root. Fix by addingrunAsUser: 1000to the Pod spec. -
capabilities.drop: [ALL]is required forrestricted. A common migration step. -
seccompProfile.type: RuntimeDefaultis required forrestricted. A common migration step. -
allowPrivilegeEscalation: falseis required forrestricted. A common migration step. -
readOnlyRootFilesystemis recommended but not required forrestricted. However, it’s a hardening best practice. -
runAsNonRoot: truerequires the image to have a non-root USER. If the image’s USER is 0 (root), the Pod is rejected. SetrunAsUser: 1000explicitly. -
The
hostPathrestrictions inbaselineare strict. Almost allhostPathmounts are blocked. UseemptyDirorpersistentVolumeClaiminstead. -
The
baselineprofile allowsseccompProfile.type: Unconfined.restricteddoesn’t. -
The
baselineprofile allowsprocMount: Default.restrictedrequiresprocMount: Default(but doesn’t blockUnconfinedinbaseline). -
The
baselineprofile blocks ~25 specific capabilities.restrictedallows onlyNET_BIND_SERVICEas an add. -
The
baselineprofile allowsprivileged: false(the default). It only blocksprivileged: true. -
PSS doesn’t check container images for vulnerabilities. Use Trivy / Snyk for that.
-
PSS doesn’t enforce resource requests / limits. That’s ResourceQuota / LimitRange.
-
PSS doesn’t enforce image registry restrictions. Use OPA / Kyverno for that.
-
PSS doesn’t enforce NetworkPolicy. Use NetworkPolicy.
-
The PodSecurity admission controller is per-Pod, not per-Container. Container-level violations are caught if the Pod-level SecurityContext doesn’t override.
-
The PodSecurity admission controller doesn’t check init containers for
restricted. Wait — it does, since k8s 1.25. The init containers must meet the profile too. -
The
latestprofile version may differ across apiservers. In HA, the apiservers may have different versions (during upgrades). Pin the version. -
The
auditandwarnmodes can be on different profiles. E.g.enforce: baseline, audit: restricted— enforce baseline, but log restricted violations. -
The PodSecurity admission controller’s check is fast. It runs on every Pod creation, in-process. No external call.
-
A Pod that violates
restrictedbut meetsbaselineis admitted in abaselinenamespace. The Pod’s spec doesn’t change; only the namespace’s policy. -
PSS doesn’t validate that an admission policy (Kyverno, OPA) is also in place. Use PSS for the standard checks, OPA / Kyverno for the custom ones.
See also
- SecurityContext — the per-Pod / per-Container settings
- Admission Controllers — the broader admission layer
- Gatekeeper — for custom rules
- Kyverno — for custom rules (YAML)
- Compliance Frameworks — the CIS / NIST view