Kubernetes: The Practical Guide to Migrating from PodSecurityPolicy to Pod Security Admission
The admission controller that replaced PodSecurityPolicy has been stable since Kubernetes 1.25, but configuring the privileged, baseline, and restricted profiles per namespace still breaks workloads that weren't audited before enforcement.
The admission controller that replaced PodSecurityPolicy has been stable since Kubernetes 1.25, but configuring the privileged, baseline, and restricted profiles per namespace still breaks workloads that weren't audited before enforcement.
PodSecurityPolicy (PSP) was removed from Kubernetes in version 1.25. In its place, Pod Security Admission (PSA) reached GA in the same release and is today the native mechanism for enforcing the Pod Security Standards at the namespace level, according to the official Kubernetes documentation. This isn't news from 1.31: it has been a stable feature since version 1.25, and the documentation page consulted today already covers v1.37. The reason the topic still causes pain for platform teams isn't the age of the feature, but that migrating from PSP to PSA changes the mental model from "one policy per policy binding" to "three standardized levels applied via namespace label," and that breaks automation that still assumes the old behavior.
What PSP Did Manually and PSA Standardized
With PSP, each cluster defined its own custom policies via PodSecurityPolicy objects, bound to service accounts or groups via RBAC. It was flexible, but also inconsistent across clusters and hard to audit: there was no common vocabulary to say "this namespace is restricted." PSA solves this with three fixed levels, defined by the Pod Security Standards: privileged (no restrictions), baseline (blocks known privilege escalations but minimizes friction with common workloads), and restricted (applies the most aggressive hardening practices, including runAsNonRoot, seccompProfile, and capabilities reduced to a minimum). There is no more custom policy: you choose one of the three levels per namespace, which makes auditing easier but removes the fine-grained granularity that teams used to PSP had.
The Three Modes: enforce, audit, and warn
Configuration happens via namespace labels, with no need for an additional controller (PSA has been built-in since 1.25). The format is:
pod-security.kubernetes.io/<mode>: <level>Where <mode> is enforce, audit, or warn, and <level> is one of the three levels. A namespace can combine all three with different levels, and this combination is what makes migration safe in practice:
kubectl label --overwrite ns pagamentos \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restrictedHere the cluster keeps accepting pods that only comply with baseline (nothing breaks in production), but any restricted-level violation already shows up as an annotation in the audit log and as a warning for whoever runs kubectl apply. This gives visibility without risk before tightening enforce to restricted. It's the equivalent of what any SRE team already does with feature flags: observe before blocking, and only promote enforce once the violation metric has been at zero for long enough.
The Detail That Breaks Deploys: Workload Resources vs. Pods
One point the documentation highlights, and one that often catches people migrating from PSP off guard: the enforce mode only acts on the final Pod object, never on the Deployment, StatefulSet, Job, or CronJob that created it. The audit and warn modes, on the other hand, are applied both on the workload resource and on the resulting pod. In practice this means a kubectl apply of a Deployment with a privileged container goes through without error (because enforce doesn't look at the Deployment), but the ReplicaSet controller will silently fail when trying to create the pods, because that's where enforce actually kicks in. If the platform team only monitors kubectl apply errors, this failure goes unnoticed until someone spots replicas stuck at zero. The right way to catch this early is to run audit/warn at restricted well before turning on enforce, and watch for the warnings that already appear when the Deployment is created.
Another behavior that differs from PSP: PSA does not re-evaluate pods that are already running when you tighten the namespace label. Only the creation (or update of sensitive fields) of a new pod goes through the check. This is good for not breaking production workloads at the moment of migration, but it also hides risk: a cluster can have the restricted label on enforce and still have privileged pods alive for months, created before the change.
Pinning the Policy Version with -version
The profiles themselves (baseline, restricted) evolve between Kubernetes minor releases, generally becoming stricter. To prevent a cluster upgrade from silently tightening the restricted level of an already-stable namespace, PSA accepts a second label:
pod-security.kubernetes.io/enforce-version: v1.31This locks the profile definition to the specified version (or use latest to always get the most recent one). For teams that upgrade clusters frequently, this is the equivalent of pinning a dependency: without the pin, a minor version upgrade can reject pods that passed without issue on the previous version, with no change whatsoever to the workload's manifest.
Exemptions: The Escape Valve That's Also a Risk
The documentation lists three exemption dimensions, statically configurable in the admission controller's configuration: username, RuntimeClassName, and namespace. The important caveat, quoted literally from the source, is that exempting a user only affects direct pod creation, "but not when creating a workload resource" (because the one creating the Deployment is normally the user, but the one actually creating the Pod is the controller's service account, such as system:serviceaccount:kube-system:replicaset-controller). The documentation's explicit recommendation is to never exempt these controller service accounts, because doing so implicitly frees any user able to create the corresponding workload resource, effectively nullifying the namespace's entire policy without anyone having asked for that.
Observability: Three Metrics to Avoid Migrating Blind
The kube-apiserver exposes three PSA-specific Prometheus metrics:
pod_security_evaluations_total: how many policy evaluations occurred (excludes exempt/ignored ones).pod_security_errors_total: errors that prevented normal evaluation, in which case the fallback is to apply the most recentrestrictedprofile, which can cause unexpected rejections.pod_security_exemptions_total: how many requests were exempted.
For any platform team running the migration, these three metrics are the minimum dashboard: evaluations_total segmented by namespace and outcome shows where latent violations still exist before tightening enforce; a spike in errors_total is a sign of broken configuration, not a bad workload; and a high and growing exemptions_total is a sign that someone is using the escape valve as a permanent solution instead of fixing the workload.
When It's Not Worth Forcing restricted
System namespaces (ingress controllers, CNI, some operators) frequently need capabilities that the restricted level prohibits by definition (access to host network interfaces, mounting hostPath volumes, or running as root because the software itself requires it). Forcing restricted in these cases isn't hardening, it's production breakage disguised as security. The path recommended by the documentation itself for clusters still using PodSecurityPolicy and planning to disable it is the dedicated migration guide (linked on the reference page), which advises mapping each existing PSP to the equivalent level before removing the old admission controller, rather than simply switching everything to restricted on the first deploy.
Translated from the Brazilian Portuguese original · Read the original
Kubernetes will prevent kubelet from starting on nodes with cgroup v1 starting in version 1.35
A post on the official Kubernetes blog, published on October 6, details the cgroup v1 deprecation timeline: starting with version 1.35, the kubelet simply won't come up on nodes still using the old model, unless someone forces a temporary override.