| 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.
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:
pod-security.kubernetes.io/enforce
pod-security.kubernetes.io/audit
pod-security.kubernetes.io/warnEach 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:
export KUBECONFIG=/etc/kubernetes/admin.conf
kubectl create namespace pss-lab --dry-run=client -o yaml | kubectl apply -f -namespace/pss-lab createdApply the mode labels without enforce:
kubectl label namespace pss-lab \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted \
--overwritenamespace/pss-lab labeledConfirm the labels:
kubectl get namespace pss-lab --show-labelsNAME STATUS AGE LABELS
pss-lab Active 0s kubernetes.io/metadata.name=pss-lab,pod-security.kubernetes.io/audit=restricted,pod-security.kubernetes.io/warn=restrictedNo 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:
pod-security.kubernetes.io/enforce-version
pod-security.kubernetes.io/audit-version
pod-security.kubernetes.io/warn-versionSet 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:
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 \
--overwriteSubmit a Pod that violates the pinned Restricted profile:
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
EOFA violating Pod surfaces the pinned version in the error:
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:
kubectl delete namespace pss-version-test --ignore-not-foundBuild a noncompliant workload
Use one readable Deployment that stacks several Restricted violations. Save it as pss-violation.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: DirectoryThis template violates Restricted on purpose:
hostNetwork: true(host namespace)privileged: trueandrunAsUser: 0- Added
NET_ADMINwithout dropping capabilities hostPathvolume (restricted volume type)- Missing
runAsNonRoot,allowPrivilegeEscalation: false, andseccompProfile
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:
kubectl apply -f pss-violation.yamlWarning: 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 createdThe warning names every failed control—use it as a remediation checklist. You can also dry-run before apply:
kubectl apply -f pss-violation.yaml --dry-run=server 2>&1 | head -3Audit 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):
kubectl get pods -n pss-lab -l app=pss-violationNAME READY STATUS RESTARTS AGE
pss-violation-fffcbcc8d-bkpb6 0/1 ContainerCreating 0 1sUnder 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:
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, andhostPath; useemptyDirinstead - Set
runAsNonRoot: truewith UID/GID65534(nobody) - Set
allowPrivilegeEscalation: falseandcapabilities.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:
kubectl delete deployment pss-violation \
-n pss-lab \
--cascade=foreground \
--wait=true \
--ignore-not-foundkubectl apply -f pss-compliant.yamldeployment.apps/pss-compliant createdNo 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:
kubectl get deployment pss-compliant -n pss-labNAME READY UP-TO-DATE AVAILABLE AGE
pss-compliant 0/1 1 0 6sPod 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:
- Inventory Deployments, StatefulSets, DaemonSets, Jobs, and CronJobs in the namespace
- Run
kubectl apply --dry-run=serveron each manifest and read PSA warnings - Validate Jobs and CronJobs—short-lived Pods still pass admission
- Validate init and ephemeral containers as well; PSA evaluates their applicable security-context fields independently
- Remediate generated Pod templates until dry-runs are silent
- Label
pod-security.kubernetes.io/enforce=restricted - Roll a compliant workload to prove enforce admits new Pods
- 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:
kubectl label namespace pss-lab \
pod-security.kubernetes.io/enforce=restricted \
--overwrite \
--dry-run=serverThen apply the label:
kubectl label namespace pss-lab \
pod-security.kubernetes.io/enforce=restricted \
--overwritenamespace/pss-lab labeledAdding 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:
kubectl rollout restart deployment/pss-compliant -n pss-labkubectl rollout status deployment/pss-compliant \
-n pss-lab \
--timeout=120sdeployment "pss-compliant" successfully rolled outA 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:
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}}]}}'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-systemoften 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:
- Inventory existing PSP objects and which ServiceAccounts used them (on a cluster that still serves the PSP API)
- Map each PSP rule set to Baseline or Restricted, noting gaps PSS does not cover (custom SELinux, AppArmor, or sysctl rules need another mechanism)
- Label namespaces with
warnandauditat your target profile - Remediate workloads using stderr warnings as the checklist
- Enable
enforcenamespace by namespace - 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:
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 -1What's Next
- Kubernetes Secrets with Examples
- Kubernetes Multi-Tenancy and Workload Isolation
- Run Sandboxed Containers with RuntimeClass and gVisor
References
- Pod Security Standards — profile definitions
- Enforce Pod Security Standards with Namespace Labels — PSA task guide
- Pod Security Admission — modes, versions, and exemptions
- Migrate from PodSecurityPolicy to the Built-In PodSecurity Admission Controller — official migration path
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.

