PersistentVolumeClaims with no storageClassName set
What does ZopNight detect here?
ZopNight flags a Kubernetes PersistentVolumeClaim on EKS, GKE or AKS whose `spec.storageClassName` is empty or missing. Such a claim leaves its storage to whichever default StorageClass the cluster has, or to a hand-made PersistentVolume, so it can sit unbound, or land on a different disk type from the one the team expected.
Signal and threshold
| Field | Value |
|---|---|
| Rule IDs | RC-1769 · RC-1869 · RC-1969 |
| Category | governance |
| Severity | low |
| Metric | spec.storageClassName |
| Threshold | empty or not set |
| Source | ZopNight |
| Permissions used | list persistentvolumeclaims · list storageclasses.storage.k8s.io |
Where it applies
What an unnamed storage class leaves to chance
The StorageClass named on a claim decides which provisioner creates the disk, which disk type and which reclaim policy the volume gets. When a claim names none, the outcome depends on cluster settings the application team usually does not control. The PersistentVolumes documentation spells out the two cases:
- A claim with
storageClassName: ""can bind only to PersistentVolumes that also have no class. If nobody created such a volume, the claim waits forever. - A claim with the field missing is given the cluster’s default StorageClass, when the DefaultStorageClass admission plugin is on and a default exists. If none exists, the field stays unset until one appears; retroactive assignment of a later default is stable since Kubernetes v1.28.
Either way, the disk a team gets can change when a platform team changes the default, and moving a workload to another cluster can leave its claims pending.
Listing claims with no class
kubectl get pvc -A -o json | jq -r ' .items[] | select((.spec.storageClassName // "") == "") | "\(.metadata.namespace)/\(.metadata.name) phase=\(.status.phase)"'To see which StorageClass, if any, is the cluster default, run kubectl get storageclass; the
default is marked (default) in the name column.
The single condition behind the finding
ZopNight reads the claim’s storage class name from its collected spec and fires when the value is
empty or absent. It treats "" and a missing field the same way. There is no age gate or
metric; the check repeats on each evaluation and clears once the claim names a class. Because the
admission plugin normally fills the field in at creation, a claim that still shows no class was
usually created with "" on purpose or before a default existed.
Limits of the check
The rule does not look at the claim’s phase, so a Bound claim with no class is flagged alongside
a pending one. Claims stuck in Pending for other reasons are covered by
Unbound PVC. The rule also cannot see
whether a classless claim was designed for static provisioning, which is the common false positive.
A governance finding, with no savings figure
This recommendation carries no dollar amount. Its value is predictability: every claim states the storage it needs rather than inheriting it.
Naming the class explicitly
- Run
kubectl get storageclassand choose the class the workload should use. - Set
storageClassNamein the claim template of the Deployment, StatefulSet or Helm chart that creates the claim. - For claims already bound, note that the field cannot simply be edited on a live claim; the new value applies to newly created claims.
- For static provisioning, keep
""and document it, or give the pre-created volumes and claims a named class so the intent is explicit. - Make sure exactly one StorageClass carries the
storageclass.kubernetes.io/is-default-classannotation set totrue.