Kubernetes Pod Security Standards and Admission

Configure Kubernetes Pod Security Admission with namespace labels for enforce, audit, and warn modes, remediate noncompliant workloads, pin policy versions, and migrate from removed PodSecurityPolicy.

Published

Updated

Read time 13 min read

Reviewed byDeepak Prasad

Kubernetes Pod Security Admission enforcing Baseline and Restricted profiles through namespace labels on a multi-node cluster
Tested on Rocky Linux 10.2 (Red Quartz) workstation
Package kubectl 1.36.3
Applies to Ubuntu, Debian, Kali Linux, Linux Mint, Pop!_OS, Raspberry Pi OS, elementary OS, Zorin OS, Parrot OS, MX Linux, RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream, Fedora
Kubernetes cluster
Cert prep CKS
Lab environment Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm
Privilege cluster-admin kubectl to label namespaces and create test workloads
Scope Privileged, Baseline, and Restricted Pod Security Standards; Pod Security Admission namespace labels; enforce, audit, and warn modes; version pinning; workload remediation; exemptions overview; migration from removed PodSecurityPolicy. Does not cover Kyverno, Gatekeeper, ValidatingAdmissionPolicy CEL, full SecurityContext field reference, image policy, or Linux node hardening.
Related guides Authentication and admission control
Kubernetes security architecture

Pod Security Standards (PSS) describe how tightly a Pod may interact with the host. Pod Security Admission (PSA) enforces those profiles through labels on namespaces. This walkthrough starts with warn and audit on a deliberately unsafe Deployment, reads the violation list, remediates the template, and only then enables enforce on a kubeadm lab running Kubernetes 1.36.

IMPORTANT
This guide covers built-in Pod Security Admission and the three standard profiles. It does not teach SecurityContext field-by-field, custom admission with Kyverno or Gatekeeper, or ValidatingAdmissionPolicy CEL. Use those tools when Baseline or Restricted do not match a custom control from legacy PodSecurityPolicy.

PSS, PSA, and legacy PodSecurityPolicy

Three names often appear together in security discussions:

Term What it is
Pod Security Standards Built-in profile definitions: Privileged, Baseline, Restricted
Pod Security Admission In-tree admission plugin that evaluates Pods against a profile
PodSecurityPolicy (removed) Deprecated v1beta1 API removed in Kubernetes v1.25

PSS answers what a hardened Pod should look like. PSA answers how the API server rejects or warns on noncompliant Pods. PodSecurityPolicy used cluster-scoped PodSecurityPolicy objects plus RBAC (use verb) so controllers could bind a policy to a ServiceAccount. PSA replaces that with namespace labels—no per-policy CRD and no PSP RBAC dance.

On Kubernetes 1.25 and later, PSA is enabled by default in the API server. You configure it by labeling namespaces, not by editing kube-apiserver flags for each profile.


Compare the profiles

Profile Intended posture
Privileged Unrestricted; for trusted system workloads only
Baseline Blocks common privilege-escalation paths while allowing many legacy apps
Restricted Strongest hardening; non-root, dropped capabilities, restricted volumes, seccomp

Privileged allows privileged: true, host namespaces, and hostPath volumes. Baseline still blocks the worst escalation paths but permits more defaults than Restricted. Restricted expects runAsNonRoot, allowPrivilegeEscalation: false, capabilities dropped to ALL, seccompProfile.type of RuntimeDefault or Localhost, and only approved volume types.

The upstream doc set lists every control ID per profile version. This article focuses on admission workflow and remediation—not duplicating the full field matrix.


Configure namespace modes

PSA reads three independent mode labels on each namespace:

text
pod-security.kubernetes.io/enforce
pod-security.kubernetes.io/audit
pod-security.kubernetes.io/warn

Each label value is privileged, baseline, or restricted:

  • enforce — rejects Pods that violate the profile at admission (Forbidden)
  • warn — prints a user-facing warning to the submitting client’s stderr but still admits the Pod or workload template
  • audit — records violations in the Kubernetes audit event annotation when audit logging is enabled; it does not print a warning to the client

You can mix profiles per mode. For example, warn=restricted while enforce is unset surfaces every Restricted violation during testing without blocking traffic. Each mode can pin a different policy version with its own *-version label.

Create a lab namespace and start with warn and audit only:

bash
export KUBECONFIG=/etc/kubernetes/admin.conf
kubectl create namespace pss-lab --dry-run=client -o yaml | kubectl apply -f -
output
namespace/pss-lab created

Apply the mode labels without enforce:

bash
kubectl label namespace pss-lab \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/audit=restricted \
  --overwrite
output
namespace/pss-lab labeled

Confirm the labels:

bash
kubectl get namespace pss-lab --show-labels
output
NAME      STATUS   AGE   LABELS
pss-lab   Active   0s    kubernetes.io/metadata.name=pss-lab,pod-security.kubernetes.io/audit=restricted,pod-security.kubernetes.io/warn=restricted

No enforce label means noncompliant Pods can still be created while you inventory violations.


Pin policy versions

By default PSA evaluates against latest for the running Kubernetes minor version—for example restricted:latest on 1.36. Pin an explicit minor version when you need predictable behavior across upgrades:

text
pod-security.kubernetes.io/enforce-version
pod-security.kubernetes.io/audit-version
pod-security.kubernetes.io/warn-version

Set enforce-version=v1.30 to lock enforcement to the Restricted rules as they existed in the 1.30 policy release, even after the cluster upgrades to 1.36. Use latest when you want new controls automatically as the cluster version advances. Version labels can pin each mode to a Kubernetes minor version independently—for example warn-version=v1.30 with enforce-version=latest.

Create a version-pinned namespace and test rejection:

bash
kubectl create namespace pss-version-test
kubectl label namespace pss-version-test \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=v1.30 \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/audit=restricted \
  --overwrite

Submit a Pod that violates the pinned Restricted profile:

bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: pss-version-violate
  namespace: pss-version-test
spec:
  restartPolicy: Never
  containers:
  - name: pss-version-violate
    image: busybox:1.36
    command: ["sleep", "60"]
    securityContext:
      privileged: true
EOF

A violating Pod surfaces the pinned version in the error:

output
Error from server (Forbidden): pods "pss-version-violate" is forbidden: violates PodSecurity "restricted:v1.30": privileged (container "pss-version-violate" must not set securityContext.privileged=true), allowPrivilegeEscalation != false (container "pss-version-violate" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "pss-version-violate" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "pss-version-violate" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "pss-version-violate" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")

Delete test namespaces when you finish:

bash
kubectl delete namespace pss-version-test --ignore-not-found

Build a noncompliant workload

Use one readable Deployment that stacks several Restricted violations. Save it as pss-violation.yaml:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pss-violation
  namespace: pss-lab
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pss-violation
  template:
    metadata:
      labels:
        app: pss-violation
    spec:
      hostNetwork: true
      containers:
      - name: busybox
        image: busybox:1.36
        command: ["sleep", "3600"]
        securityContext:
          privileged: true
          runAsUser: 0
          capabilities:
            add: ["NET_ADMIN"]
        volumeMounts:
        - name: host-vol
          mountPath: /host
      volumes:
      - name: host-vol
        hostPath:
          path: /tmp
          type: Directory

This template violates Restricted on purpose:

  • hostNetwork: true (host namespace)
  • privileged: true and runAsUser: 0
  • Added NET_ADMIN without dropping capabilities
  • hostPath volume (restricted volume type)
  • Missing runAsNonRoot, allowPrivilegeEscalation: false, and seccompProfile

Start with warn and audit

Apply the Deployment while enforce is still unset. PSA prints the full violation list to stderr but admits the object:

bash
kubectl apply -f pss-violation.yaml
output
Warning: would violate PodSecurity "restricted:latest": host namespaces (hostNetwork=true), privileged (container "busybox" must not set securityContext.privileged=true), allowPrivilegeEscalation != false (container "busybox" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "busybox" must set securityContext.capabilities.drop=["ALL"]; container "busybox" must not include "NET_ADMIN" in securityContext.capabilities.add), restricted volume types (volume "host-vol" uses restricted volume type "hostPath"), runAsNonRoot != true (pod or container "busybox" must set securityContext.runAsNonRoot=true), runAsUser=0 (container "busybox" must not set runAsUser=0), seccompProfile (pod or container "busybox" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
deployment.apps/pss-violation created

The warning names every failed control—use it as a remediation checklist. You can also dry-run before apply:

bash
kubectl apply -f pss-violation.yaml --dry-run=server 2>&1 | head -3

Audit mode writes violations to the API audit log when cluster audit policy is configured; warn mode is what you see interactively in kubectl. Neither mode adds permanent pod-security annotations to Pod objects in this lab—the signal is the stderr warning and audit log entries.

Check that the controller still created a Pod (allowed under warn-only):

bash
kubectl get pods -n pss-lab -l app=pss-violation
output
NAME                            READY   STATUS              RESTARTS   AGE
pss-violation-fffcbcc8d-bkpb6   0/1     ContainerCreating   0          1s

Under warn and audit alone, Kubernetes does not block Pod creation—it only surfaces the policy gap.


Remediate the workload

Fix each item from the warning. Save a Restricted-compliant Deployment as pss-compliant.yaml:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pss-compliant
  namespace: pss-lab
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pss-compliant
  template:
    metadata:
      labels:
        app: pss-compliant
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534
        runAsGroup: 65534
        seccompProfile:
          type: RuntimeDefault
      containers:
      - name: busybox
        image: busybox:1.36
        command: ["sleep", "3600"]
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop: ["ALL"]
        volumeMounts:
        - name: data
          mountPath: /data
      volumes:
      - name: data
        emptyDir: {}

Changes mapped to the warning:

  • Removed hostNetwork, privileged, and hostPath; use emptyDir instead
  • Set runAsNonRoot: true with UID/GID 65534 (nobody)
  • Set allowPrivilegeEscalation: false and capabilities.drop: ["ALL"]
  • Added pod-level seccompProfile.type: RuntimeDefault

For field-level detail on each setting, see SecurityContext examples, Linux capabilities, and seccomp profiles.

Replace the violating Deployment. Use foreground deletion so terminating Pods do not linger and trigger the existing-Pod warning when you later add enforce:

bash
kubectl delete deployment pss-violation \
  -n pss-lab \
  --cascade=foreground \
  --wait=true \
  --ignore-not-found
bash
kubectl apply -f pss-compliant.yaml
output
deployment.apps/pss-compliant created

No Warning: would violate PodSecurity line on stderr means the template passes Restricted under warn mode—the admission check you care about before enabling enforce.

Confirm the Deployment object exists:

bash
kubectl get deployment pss-compliant -n pss-lab
output
NAME            READY   UP-TO-DATE   AVAILABLE   AGE
pss-compliant   0/1     1            0           6s

Pod readiness depends on image pull and runtime health on your nodes; PSA admission already accepted the template.


Enable enforcement

Turn on enforce only after warn output is clean for every workload in the namespace. A practical sequence:

  1. Inventory Deployments, StatefulSets, DaemonSets, Jobs, and CronJobs in the namespace
  2. Run kubectl apply --dry-run=server on each manifest and read PSA warnings
  3. Validate Jobs and CronJobs—short-lived Pods still pass admission
  4. Validate init and ephemeral containers as well; PSA evaluates their applicable security-context fields independently
  5. Remediate generated Pod templates until dry-runs are silent
  6. Label pod-security.kubernetes.io/enforce=restricted
  7. Roll a compliant workload to prove enforce admits new Pods
  8. Test one deliberate violation

Foreground deletion of pss-violation and its Pods (in the remediation step above) keeps the namespace clean before you add enforce. Dry-run the label change first:

bash
kubectl label namespace pss-lab \
  pod-security.kubernetes.io/enforce=restricted \
  --overwrite \
  --dry-run=server

Then apply the label:

bash
kubectl label namespace pss-lab \
  pod-security.kubernetes.io/enforce=restricted \
  --overwrite
output
namespace/pss-lab labeled

Adding or changing an enforce label evaluates existing Pods but does not evict them. Kubernetes warns when violating Pods are actually present—for example Warning: existing pods in namespace "pss-lab" violate the new PodSecurity enforce level "restricted:latest"—but that warning does not appear in this lab because the namespace is already clean.

The pss-compliant Deployment was created before enforce=restricted, so it does not by itself prove that enforcement permits new compliant Pods. PSA enforce checks resulting Pod creations, not the existing Deployment object. Recreate the Pod template and wait for the rollout:

bash
kubectl rollout restart deployment/pss-compliant -n pss-lab
bash
kubectl rollout status deployment/pss-compliant \
  -n pss-lab \
  --timeout=120s
output
deployment "pss-compliant" successfully rolled out

A successful rollout means the ReplicaSet created a new Restricted-compliant Pod and PSA admitted it under enforce. Then confirm enforce rejects a new noncompliant Pod:

bash
kubectl run pss-violate-enforce -n pss-lab --image=busybox:1.36 --restart=Never \
  --overrides='{"spec":{"hostNetwork":true,"containers":[{"name":"pss-violate-enforce","image":"busybox:1.36","command":["sleep","60"],"securityContext":{"privileged":true,"runAsUser":0}}]}}'
output
Error from server (Forbidden): pods "pss-violate-enforce" is forbidden: violates PodSecurity "restricted:latest": host namespaces (hostNetwork=true), privileged (container "pss-violate-enforce" must not set securityContext.privileged=true), allowPrivilegeEscalation != false (container "pss-violate-enforce" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "pss-violate-enforce" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "pss-violate-enforce" must set securityContext.runAsNonRoot=true), runAsUser=0 (container "pss-violate-enforce" must not set runAsUser=0), seccompProfile (pod or container "pss-violate-enforce" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")

Together, the rollout and the Forbidden response validate both outcomes: a newly created Restricted-compliant Pod is admitted, and a newly created noncompliant Pod is rejected. Re-applying the old violating Deployment after enforce is on still prints warnings on apply, but new Pods from that template fail admission when the controller tries to create them.


How PSA treats workload resources

PSA evaluates Pods at admission. Warn and audit evaluate workload templates such as Deployments during create and update; enforce applies only to resulting Pod objects.

Mode Deployment or workload apply Pod creation
warn Template admitted; warning returned to the submitting client Pod admitted; warning returned to a direct client
audit Template admitted; violation added to its audit event Pod admitted; violation added to its audit event
enforce Workload object is not rejected by PSA Noncompliant Pod rejected

A Pod created by a Deployment is submitted by a controller, so its warning is not necessarily shown in the administrator’s terminal.

A Deployment with a bad Pod template can sit in progressing with zero ready replicas once enforce is active—the controller is accepted, but every Pod create fails. Warn and audit give you earlier signal on the same template during kubectl apply --dry-run=server or the first apply stderr line.

PSA does not rewrite running Pods. Changing enforce from baseline to restricted does not mutate existing Pod specs—you must roll workloads to pick up remediated templates.


Configure exemptions

Cluster administrators can configure exemptions through the API server PodSecurity admission configuration. An exempt request skips enforce, audit, and warn checks. Exemption dimensions:

  • Usernames — service accounts or users that skip all PSA modes
  • Namespaces — system namespaces such as kube-system often stay exempt
  • RuntimeClasses — workloads using a named RuntimeClass skip all PSA modes

Exempting an end user affects Pods that user creates directly. Pods generated from Deployments are created by controller ServiceAccounts, so the user exemption does not automatically apply. Do not exempt controller ServiceAccounts broadly because that can effectively exempt every user allowed to create the corresponding workload resource.

Exemptions are broad bypasses. Document every entry and review them in change control; prefer namespace-scoped rollout with warn before relying on long-lived exemptions.


Migrate from PodSecurityPolicy

Perform PSP inventory, RBAC mapping, admission-plugin removal, and object deletion on the pre-upgrade Kubernetes 1.24-or-earlier cluster. On Kubernetes 1.25 and later, including this 1.36 lab, the PSP API is already removed; only stale YAML, RBAC, automation, and documentation may remain to clean up. PSP was removed in Kubernetes 1.25—Kubernetes 1.36 no longer serves the PodSecurityPolicy API, so it cannot inventory or delete PSP objects.

If you still have automation or docs referencing PodSecurityPolicy, migrate in this order:

  1. Inventory existing PSP objects and which ServiceAccounts used them (on a cluster that still serves the PSP API)
  2. Map each PSP rule set to Baseline or Restricted, noting gaps PSS does not cover (custom SELinux, AppArmor, or sysctl rules need another mechanism)
  3. Label namespaces with warn and audit at your target profile
  4. Remediate workloads using stderr warnings as the checklist
  5. Enable enforce namespace by namespace
  6. Remove PSP API objects, RBAC bindings, and admission plugin references from manifests and docs

Legacy annotation on Pods (kubernetes.io/psp) and PSP use RBAC have no equivalent in PSA—namespace labels replace the binding model entirely.


Troubleshooting

Symptom Likely cause Fix
No PSA warnings on apply Namespace missing the warn label, or the submitted template is already compliant Label namespace with warn; confirm Kubernetes ≥ 1.25
Forbidden: violates PodSecurity enforce label active and template noncompliant Remediate SecurityContext, volumes, and host namespaces per warning text
Deployment exists but no ready Pods Enforce blocks Pod creates from bad template Fix Pod template; check ReplicaSet events
Warn-only but cluster still risky enforce unset Expected during rollout—do not skip enforce in production
Different behavior after upgrade latest profile picked up new controls Pin *-version labels or re-test with dry-run
Legacy PSP docs still linked Old tutorials reference removed API Use namespace labels; retire PSP manifests

When audit logging is already enabled, correlate audit-mode violations from the log:

bash
jq 'select(
  .annotations["pod-security.kubernetes.io/audit-violations"] != null
) | {
  object: .objectRef,
  violation: .annotations["pod-security.kubernetes.io/audit-violations"]
}' /path/to/audit.log | tail -1

What's Next


References


Summary

You labeled a namespace with Pod Security Admission modes, deployed a deliberately weak Deployment, and read the would violate PodSecurity warning as a remediation checklist. After fixing host namespaces, privileged mode, capabilities, user IDs, volumes, and seccomp, the compliant template applied without warnings. Turning on pod-security.kubernetes.io/enforce=restricted let a rollout of pss-compliant complete successfully while rejecting a new violating Pod with Forbidden.

The distinction that saves production rollouts is template versus Pod: warn and audit surface problems on apply and dry-run, but enforce blocks at Pod admission. Controllers may look healthy while replicas never become ready if the Pod template still violates Restricted. Pin enforce-version when you need stable rules across cluster upgrades, and treat exemptions as temporary—not a substitute for fixing workload YAML.

For CKS and production hardening, pair PSA with SecurityContext discipline on every chart, then layer seccomp and capability drops where Restricted alone is not enough. Retire PodSecurityPolicy manifests and RBAC once every namespace runs on labels.


Frequently Asked Questions

1. What is the difference between Pod Security Standards and Pod Security Admission?

Pod Security Standards define three built-in profiles—Privileged, Baseline, and Restricted—with specific rules for volumes, capabilities, and security contexts. Pod Security Admission is the in-tree admission plugin that evaluates Pods against a chosen profile using namespace labels. Standards describe what to enforce; PSA is how Kubernetes enforces it.

2. Which namespace labels control Pod Security Admission?

Set pod-security.kubernetes.io/enforce, pod-security.kubernetes.io/audit, and pod-security.kubernetes.io/warn to privileged, baseline, or restricted. Optional pod-security.kubernetes.io/enforce-version, audit-version, and warn-version pin the policy minor version. Labels apply to the namespace; every Pod created in that namespace is evaluated.

3. Does Pod Security Admission block Deployments or only Pods?

Enforce mode rejects Pod creation at admission time. Warn and audit can evaluate workload templates such as Deployments during dry-run or create and surface violations before you turn on enforce. A Deployment may be accepted while its Pods fail under enforce if the template is noncompliant.

4. What happened to PodSecurityPolicy?

PodSecurityPolicy was removed in Kubernetes v1.25. It used cluster-scoped PSP objects and RBAC to authorize which policies a workload could use. Pod Security Admission replaces that model with namespace labels and built-in profiles, without per-policy API objects.

5. Can I use different profiles for enforce and warn on the same namespace?

Yes. A common rollout pattern sets warn and audit to restricted while enforce stays at baseline, or keeps enforce unset until workloads are remediated. Each mode can use a different profile and version label independently.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)