If there’s one topic in Kubernetes security that trips people up through pure naming confusion, it’s this one. PodSecurityPolicy (PSP) was removed from Kubernetes entirely as of v1.25. Anyone setting up cluster security today needs to know both the history and the replacement, because a lot of tutorials and Stack Overflow answers still reference the old API and will simply fail on a modern cluster. This article covers what PSPs were, why they’re gone, and exactly what to use instead.
A Short History: What PodSecurityPolicy Was
PodSecurityPolicy was a cluster-level resource that constrained what a Pod spec was allowed to request — things like running as root, using host networking, mounting host paths, or gaining privileged access. It was admission-controlled: the PSP admission controller checked incoming Pod specs against the policies bound to the requesting user/ServiceAccount via RBAC, and rejected non-compliant Pods.
A PSP looked like this (for historical reference only — this API no longer exists on current clusters):
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: restricted
spec:
privileged: false
allowPrivilegeEscalation: false
runAsUser:
rule: MustRunAsNonRoot
seLinux:
rule: RunAsAny
volumes:
- configMap
- secret
- emptyDir
- persistentVolumeClaim
It was deprecated in Kubernetes 1.21 and fully removed in 1.25. The reasons were well documented by the project: the binding model (via RBAC, indirectly, based on which policy a user could “use”) was confusing, hard to reason about, and made it easy to accidentally grant broader access than intended.
What Replaced It: Pod Security Admission (PSA)
Kubernetes 1.25+ ships Pod Security Admission, a built-in admission controller that enforces the Pod Security Standards — three predefined levels: privileged, baseline, and restricted. Unlike PSP, PSA is configured per-namespace via labels, not via a separate custom resource bound through RBAC.
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted
This single command does what previously required a PSP object, an RBAC Role, and a RoleBinding.
The Three Pod Security Standards
- Privileged: unrestricted, for system components (e.g. CNI plugins) that genuinely need host access.
- Baseline: blocks known privilege escalations, but is otherwise permissive.
- Restricted: heavily locked down — non-root, no privilege escalation, seccomp required, dropped Linux capabilities.
For most application namespaces, restricted is the correct target.
Applying Restricted Standards in Practice
A Pod that will actually pass restricted enforcement needs a securityContext designed for it:
apiVersion: v1
kind: Pod
metadata:
name: secure-app
namespace: production
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.example.com/myapp:1.0.0
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
ports:
- containerPort: 8080
Test it against the enforced standard:
kubectl apply -f secure-app.yaml -n production
If a Pod violates the standard, the API server rejects it outright with a message like:
Error from server (Forbidden): error when creating "secure-app.yaml":
pods "secure-app" is forbidden: violates PodSecurity "restricted:latest":
allowPrivilegeEscalation != false (container "app" must set
securityContext.allowPrivilegeEscalation=false)
That immediate, specific feedback is a real improvement over PSP’s often-cryptic RBAC-denial errors.
Namespace Rollout Strategy
Because warn and audit modes don’t block anything, they’re the safe way to test a policy before enforcing it:
kubectl label namespace staging \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted
Deploy your normal workloads, check kubectl get events and API server audit logs for violations, fix the Pod specs, and only then flip enforce=restricted.
Deployment Example Meeting Restricted Standards
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: myapp
image: registry.example.com/myapp:1.0.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
Third-Party Alternatives: OPA Gatekeeper and Kyverno
For teams needing more granular or custom policy logic than the three built-in PSA levels offer, two CNCF-ecosystem projects fill the gap:
Kyverno (policy-as-YAML, easier learning curve):
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-non-root
spec:
validationFailureAction: Enforce
rules:
- name: check-runAsNonRoot
match:
resources:
kinds: ["Pod"]
validate:
message: "runAsNonRoot must be true"
pattern:
spec:
securityContext:
runAsNonRoot: true
OPA Gatekeeper (Rego-based, more powerful, steeper curve) is the other common choice, typically picked when policy logic needs to reach beyond what PSA’s fixed levels express — for example, requiring specific labels across all namespaces, or enforcing organization-specific naming conventions.
RBAC Still Matters
PSA replaces PSP’s enforcement model, but RBAC still governs who can create, edit, or delete resources at all. The two are complementary, not substitutes for each other:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production
name: deployer
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "create", "update", "patch"]
Monitoring and Auditing
kubectl get events -n production --field-selector reason=FailedCreate
kubectl get pods -n production -o jsonpath='{.items[*].spec.securityContext}'
Cluster audit logs (configured at the API server level) will also record PSA deny and warn decisions if audit policy is set to capture them — worth wiring into your existing log aggregation.
Migrating an Existing Cluster from PSP to PSA
Teams running clusters built before 1.25 often still have PSP objects and RBAC bindings lingering from the old model, and migrating cleanly takes a deliberate sequence rather than a single flag flip:
- Inventory existing PSPs and note which workloads each one was actually permitting.
- For each namespace, determine the closest matching Pod Security Standard level (
privileged,baseline, orrestricted). - Apply that level in
warn/auditmode first, across every namespace. - Review audit logs and
kubectl get eventsfor violations over at least one full deployment cycle. - Fix any workloads that would be rejected, then flip to
enforce. - Only after
enforceis live everywhere and stable, remove the old PSP objects and their RBAC bindings.
# Step 3, applied cluster-wide via a script over all namespaces
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
kubectl label namespace "$ns" \
pod-security.kubernetes.io/warn=baseline \
pod-security.kubernetes.io/audit=baseline --overwrite
done
Skipping straight to enforce across an entire cluster in one step is the single most common cause of an unplanned outage during this specific migration — a warn/audit trial period is genuinely worth the extra day or two it takes.
Namespace Exemptions for System Workloads
Not every namespace should target restricted — CNI DaemonSets, storage drivers, and other system components often need privileged access to do their job, and PSA supports exempting specific namespaces, users, or RuntimeClasses entirely at the cluster level rather than fighting the standard on a per-Pod basis:
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1
kind: PodSecurityConfiguration
defaults:
enforce: baseline
enforce-version: latest
exemptions:
namespaces:
- kube-system
- calico-system
This is applied via the API server’s admission configuration file, not a namespace label — a genuinely different mechanism worth distinguishing from the per-namespace labeling shown earlier, since it operates cluster-wide rather than per-namespace.
Verifying Enforcement Actually Works
Before trusting a restricted label in production, it’s worth deliberately testing that it rejects what it should:
kubectl run privileged-test --image=nginx --privileged=true -n production
Error from server (Forbidden): pods "privileged-test" is forbidden:
violates PodSecurity "restricted:latest": privileged (container
"privileged-test" must not set securityContext.privileged=true)
Seeing this exact rejection is the confirmation that the namespace label is actually being enforced by the API server — an easy, low-risk smoke test worth running immediately after labeling any namespace.
What Pod Security Admission Deliberately Doesn’t Cover
It’s worth being clear about PSA’s actual scope, since it’s narrower than what PSP theoretically allowed teams to enforce. PSA only evaluates the three fixed standards against a Pod’s own security-relevant fields — it has no concept of organization-specific rules like “every image must come from our approved registry,” “every Deployment must have a cost-center label,” or “no Service may be of type LoadBalancer outside these three namespaces.” Those genuinely custom policies are exactly the gap Kyverno and OPA Gatekeeper fill, and most production clusters running anything beyond a small team end up using PSA for the baseline security posture and one of these two tools for organization-specific governance layered on top, rather than treating PSA as a complete policy solution on its own.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-image-registries
spec:
validationFailureAction: Enforce
rules:
- name: validate-registries
match:
resources:
kinds: ["Pod"]
validate:
message: "Images must come from the approved registry"
pattern:
spec:
containers:
- image: "registry.example.com/*"
This kind of registry allowlisting is a genuinely common production requirement — preventing a compromised or careless deploy from pulling an unverified public image — and it sits entirely outside what PSA is designed to check, regardless of which of the three built-in levels a namespace enforces.
Choosing Between Kyverno and OPA Gatekeeper
For teams evaluating which of the two extension tools to adopt: Kyverno’s policies are plain YAML with a pattern-matching syntax that’s approachable for anyone already comfortable writing Kubernetes manifests, while OPA Gatekeeper’s policies are written in Rego, a genuinely separate query language with a real learning curve but considerably more expressive power for complex, cross-resource logic. Teams whose policy needs are mostly “require this field,” “restrict this value,” or “block this resource type” tend to find Kyverno faster to adopt and maintain; teams with more complex governance requirements spanning multiple resource types and external data sources often find Rego’s expressiveness worth the steeper initial investment.
Common Mistakes
- Copy-pasting old PSP YAML into a 1.25+ cluster and being confused when
kubectl applyerrors with “no matches for kind PodSecurityPolicy.” - Enforcing
restrictedcluster-wide on day one without awarn/audittrial period, breaking every workload with a root-requiring base image. - Forgetting that system namespaces (like
kube-system) often genuinely needprivilegedand shouldn’t be force-fit intorestricted. - Assuming PSA replaces the need for RBAC — it doesn’t; they answer different questions (“what can this Pod do” vs. “who can create this Pod”).
Summary
PodSecurityPolicy is gone, and Pod Security Admission is the current standard: three built-in levels, applied via namespace labels, with immediate rejection feedback instead of indirect RBAC denials. For anything beyond the three fixed levels, Kyverno or OPA Gatekeeper extend policy enforcement with custom rules. Either way, the goal hasn’t changed since PSP’s introduction — Pods should run with the least privilege that lets them actually do their job.
References
- Kubernetes Pod Security Standards: https://kubernetes.io/docs/concepts/security/pod-security-standards/
- Pod Security Admission: https://kubernetes.io/docs/concepts/security/pod-security-admission/
- PSP deprecation history: https://kubernetes.io/docs/concepts/security/pod-security-policy/
- Kyverno documentation: https://kyverno.io/docs/
- OPA Gatekeeper: https://open-policy-agent.github.io/gatekeeper/website/docs/
