PodSecurityPolicy (Deprecated)

PodSecurityPolicy (PSP) was a cluster-scoped admission resource that gated which Pod security configurations were allowed to be created — and, uniquely, could both validate and mutate Pods (applying defaults) (Kubernetes — Pod Security Policies). It was the original answer to “how do I stop developers from running privileged containers.” PSP was deprecated in Kubernetes 1.21 and removed entirely in 1.25 (PSP Deprecation blog) — not because the idea was wrong, but because its authorization model was unfixably confusing. This note is historical context: PSP no longer exists in any supported cluster, but understanding it explains why its successors — Pod Security Admission for the common case, Kyverno / OPA Gatekeeper for fine-grained policy — are shaped the way they are, and is essential when migrating an old cluster or reading pre-2022 documentation.

Mental Model

PSP’s fatal design was that a policy was not assigned to a Pod — instead, a Pod was admitted if the requesting identity (the user, or more usually the Pod’s ServiceAccount) had been granted the RBAC use verb on some PSP that happened to permit that Pod’s spec. The set of “policies in effect” was therefore an emergent property of the RBAC graph, not anything visible on the Pod or the namespace.

flowchart TD
    REQ["CREATE Pod (by user U,<br/>Pod runs as ServiceAccount SA)"]
    GATHER["Gather every PSP that U *or* SA<br/>has RBAC 'use' verb on"]
    PSPSET["Candidate PSP set<br/>{psp-a, psp-b, psp-c, ...}"]
    PICK["Find a PSP in the set that<br/>ALLOWS this Pod — preferring<br/>one needing no mutation"]
    MUTATE["Apply that PSP's defaulting<br/>mutations to the Pod"]
    ADMIT["Pod admitted"]
    REJECT["No PSP in set allows it → REJECT"]

    REQ --> GATHER --> PSPSET --> PICK
    PICK -->|"found"| MUTATE --> ADMIT
    PICK -->|"none"| REJECT

The diagram shows PSP’s evaluation. The insight to extract — and the reason PSP was removed: there is no way, looking at this picture, to answer “which PSP applies to my Pod?” before the Pod is created. The answer depends on the full RBAC graph of two identities, on which PSPs exist, and on a non-obvious selection rule among them. The thing that secures a Pod is invisible at the point you write the Pod.

Mechanical Walk-through

What a PSP could control

A PodSecurityPolicy object (policy/v1beta1 API group) carried fields covering essentially the entire Pod security surface: privileged, allowPrivilegeEscalation, defaultAllowPrivilegeEscalation, requiredDropCapabilities / allowedCapabilities, volumes (allowed volume types), hostNetwork / hostPID / hostIPC, hostPorts, runAsUser / runAsGroup / fsGroup / supplementalGroups (each with a strategyMustRunAs, MustRunAsNonRoot, RunAsAny), seLinux, readOnlyRootFilesystem, allowedHostPaths, seccomp/apparmor (via annotations), and more. In raw expressiveness it exceeded today’s Pod Security Standards.

Validation and mutation

PSP did two jobs. It validated — rejected a Pod whose spec the chosen PSP forbade. It also mutated — a PSP with, say, defaultAllowPrivilegeEscalation: false or a runAsUser MustRunAs strategy would fill in those fields on a Pod that omitted them. This defaulting was genuinely useful (you could harden Pods their authors had not bothered to harden), and its absence is the single capability Pod Security Admission consciously gave up.

The broken authorization model

The removal blog and the historical-context blog are explicit about the core defect. A Pod was authorized against PSP if either the creating user or the Pod’s ServiceAccount held the RBAC use verb on a PSP permitting the Pod. This produced several unfixable problems:

  • “Which PSP applies?” was unanswerable. With multiple PSPs in the candidate set, PSP used a non-obvious selection rule (prefer a PSP that requires no mutation; otherwise pick one alphabetically-ish). You could not predict, before creation, which policy — and therefore which mutations — your Pod would get.
  • Accidental broad grants. Granting use on a permissive PSP to a widely-bound ServiceAccount, or to a group like system:authenticated, silently let everything run privileged. The grant looked like an innocuous RBAC line; its effect was a cluster-wide hole.
  • Authorizing the wrong subject. Because the ServiceAccount could carry the use grant, the human creating the Pod and the policy that governed it were decoupled — a workload’s effective policy depended on a ServiceAccount the developer might not even know about.
  • Fail-open on enablement. PSP was not on by default; enabling the admission plugin without first creating PSPs and granting use instantly broke the cluster (no PSP → no Pod admitted). This made PSP famously dangerous to turn on, so many clusters simply never did.
  • No dry-run, no audit/warn mode. PSP rejected or admitted; there was no way to evaluate “would this break things” without actually breaking things. The gradual-rollout story that Pod Security Admission’s warn/audit modes provide did not exist.

The deprecation timeline

PSP was deprecated in Kubernetes 1.21 (April 2021) and removed in 1.25 (August 2022). Per Kubernetes’ deprecation policy this gave roughly a year of overlap. The 1.21 deprecation blog announced the plan and promised a built-in replacement; that replacement, Pod Security Admission, reached stable in 1.25 — the same release that deleted PSP. A cluster on 1.25 or later has no PodSecurityPolicy resource at all; manifests referencing it fail.

Configuration / API Surface

PSP cannot be created on any current cluster. This example is historical — shown so the structure is recognizable in old manifests:

# HISTORICAL — policy/v1beta1 PodSecurityPolicy. Removed in Kubernetes 1.25.
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: restricted-psp
spec:
  privileged: false                  # forbid privileged containers
  allowPrivilegeEscalation: false     # forbid privilege escalation...
  defaultAllowPrivilegeEscalation: false  # ...and DEFAULT it to false (a MUTATION — PSA cannot do this)
  requiredDropCapabilities: ["ALL"]   # force every container to drop all capabilities
  volumes: ["configMap", "emptyDir", "projected", "secret", "persistentVolumeClaim"]
  hostNetwork: false
  hostIPC: false
  hostPID: false
  runAsUser:
    rule: MustRunAsNonRoot            # reject UID 0
  fsGroup:
    rule: MustRunAs
    ranges: [{ min: 1, max: 65535 }]
  seLinux:
    rule: RunAsAny
  readOnlyRootFilesystem: true
---
# The PSP did NOTHING until an identity was granted the 'use' verb on it via RBAC.
# This indirection — policy-via-RBAC-grant — is exactly what got PSP removed.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: use-restricted-psp
rules:
- apiGroups: ["policy"]
  resources: ["podsecuritypolicies"]
  verbs: ["use"]                      # the 'use' verb: "may have this PSP applied"
  resourceNames: ["restricted-psp"]

Line-by-line. The PSP spec resembles a Pod Security Standards Restricted profile — but note defaultAllowPrivilegeEscalation: false, a mutation: PSP would fill this field in on Pods that omitted it, something PSA structurally cannot do. The second object is the load-bearing, fatal part: the PSP is inert until a ClusterRole granting the use verb on it is bound to some identity. The Pod’s effective policy is thus a function of the RBAC graph — invisible from the Pod, invisible from the namespace, and the root cause of every confusion above.

Failure Modes

These are the historical failure modes — relevant when operating or migrating a legacy cluster:

Enabling the PSP admission plugin with no PSPs defined. The cluster instantly stops admitting Pods — no PSP in any candidate set means no Pod is authorized. The classic “I turned on PSP and the cluster died” incident.

Permissive PSP use-granted too broadly. A privileged: true PSP with use granted to system:authenticated or a widely-bound ServiceAccount silently re-permits privileged Pods cluster-wide. The grant is one inconspicuous RBAC line.

Unpredictable mutation. Two PSPs in the candidate set with different defaulting; the selection rule picks one non-deterministically from the operator’s point of view, so a Pod’s final spec varies. Debugging “why did my Pod get this runAsUser” was genuinely hard.

Migration gap on 1.25 upgrade. Upgrading a PSP-dependent cluster to 1.25 removes PSP — if Pod Security Admission (or a policy engine) is not in place first, the cluster goes from enforced to unenforced. Migration must precede the upgrade.

Alternatives and When to Choose Them

PSP has no current use — it is removed. Its responsibilities split among its successors:

  • Pod Security Admission — the built-in replacement for the common case: enforce one of the three fixed Pod Security Standards profiles per namespace. It deliberately drops PSP’s two problem features — the RBAC-use authorization model (replaced by simple namespace labels) and mutation (PSA is validate-only). Less powerful, vastly clearer.
  • Kyverno — a Kubernetes-native policy engine that does restore PSP’s missing powers: it can mutate (default securityContext fields, the thing PSA cannot do), express custom rules beyond the three PSS profiles, and generate resources. The closest “PSP replacement with mutation.”
  • OPA Gatekeeper — Open Policy Agent as an admission controller; Rego-based, ConstraintTemplate/Constraint model; fine-grained validation (and limited mutation). Chosen where an organization already standardizes on OPA/Rego.

The official guidance: use PSA for standard Pod hardening, and add Kyverno or Gatekeeper for fine-grained or mutating policy PSA cannot express. The combination covers everything PSP did, without PSP’s authorization model.

Production Notes

  • No current cluster runs PSP. If you see kind: PodSecurityPolicy in a manifest or chart, it targets a cluster older than 1.25 — treat it as a migration item, not a working config.
  • Migrating off PSP: use the official PSP→PSS mapping. Kubernetes publishes a mapping table translating PSP field combinations to the nearest Pod Security Standards profile. A tight PSP usually maps to Restricted; a lax one to Baseline. Where the PSP relied on mutation, that part of the policy must move to Kyverno, not PSA.
  • Migrate before the 1.25 upgrade. Stand up Pod Security Admission (in warn/audit mode) and any Kyverno/Gatekeeper policies, verify, then upgrade — otherwise the upgrade silently removes enforcement.
  • The lesson PSP teaches: security policy must be visible and predictable at the point of use. PSA’s namespace labels and Kyverno’s explicit policy objects are both reactions to PSP’s invisible, RBAC-graph-emergent model. When designing any policy system, “can a developer see, before acting, what governs them?” is the test PSP failed.

See Also

  • Pod Security Admission — the built-in replacement for PSP’s common case; validate-only, namespace-labelled
  • Pod Security Standards — the profile vocabulary PSA enforces; the conceptual model PSP’s ad-hoc fields became
  • Kyverno — restores PSP’s mutation and custom-rule capabilities that PSA dropped
  • OPA Gatekeeper — Rego-based fine-grained admission policy; the other PSP successor for complex policy
  • SecurityContext — the Pod fields PSP validated and defaulted
  • Kubernetes RBAC — the use verb on PSPs was how PSP’s (broken) authorization worked
  • Admission Controllers — PSP was one; the category its successors also belong to
  • Kubernetes MOC — parent MOC (§12 Security)