Skip to main content
Version: main 🚧

Enforce storage quotas per tenant cluster

vCluster Platform project quotas cap aggregate storage consumption across all tenant clusters in a project, but they don't distribute limits across namespaces or enforce per-PVC caps inside individual tenant clusters. Per-cluster enforcement fills that gap. A ResourceQuota or Kyverno policy applied inside the tenant cluster controls how much storage any single namespace can consume and how individual PVCs may be sized.

This guide covers two patterns for per-cluster enforcement. Both can be applied automatically at provisioning time through templates so that every new tenant cluster starts with the right guardrails in place.

Use ResourceQuota for namespace-level aggregate limits. Use Kyverno when you need per-PVC size limits or storage class allowlists. Use both when tenants need bounded total usage and per-object guardrails.

Pattern 1: ResourceQuota inside the tenant cluster​

A Kubernetes ResourceQuota object enforces aggregate limits on PVC count and total storage requests. It's namespace-scoped, so enforcing limits across all namespaces in the tenant cluster requires one ResourceQuota per namespace. Adding one to a tenant cluster template deploys it inside every tenant cluster created from that template.

Add a ResourceQuota to a tenant cluster template​

In the objects field of a VirtualClusterTemplate, include a ResourceQuota for each namespace you want to constrain. vCluster Platform applies the objects inside the tenant cluster at provisioning time.

apiVersion: management.loft.sh/v1
kind: VirtualClusterTemplate
metadata:
name: standard-tenant
spec:
template:
helmRelease:
chart:
version: "0.25.0"
objects: |-
apiVersion: v1
kind: ResourceQuota
metadata:
name: storage-quota
namespace: default
spec:
hard:
requests.storage: "100Gi"
count/persistentvolumeclaims: "20"

These two constraints are independent aggregate limits. requests.storage caps the combined total storage across all PVCs in the namespace, not the size of any individual PVC. In this example, the namespace cannot have PVCs that total more than 100Gi of space. count/persistentvolumeclaims caps the number of PVCs regardless of their size. In this example, the namespace cannot have more than 20 PVCs, even if their total combined storage is less than 100Gi.

With this example, a tenant could create 20 PVCs of 5Gi each (100Gi combined, both limits reached simultaneously), or 2 PVCs of 50Gi each (100Gi combined, only the storage limit reached). What is not possible is more than 20 PVCs or any number of PVCs totaling more than 100Gi.

Scope to a storage class​

To limit storage from a specific storage class, add a scoped resource name under the same spec.hard key:

spec:
hard:
ceph-rbd.storageclass.storage.k8s.io/requests.storage: "50Gi"

Apply across namespaces​

Repeat the ResourceQuota block for each namespace that requires enforcement.

Namespace templates

For per-namespace enforcement that applies when a namespace is created inside a tenant cluster, use a Space template with a ResourceQuota in the objects field instead. See Create a template.

Enforcement errors​

When a tenant or namespace admin creates a PVC that would push usage past the configured limit, the API server rejects the request with an error:

Error from server (Forbidden): persistentvolumeclaims "my-pvc" is forbidden:
exceeded quota: storage-quota, requested: requests.storage=20Gi,
used: requests.storage=90Gi, limited: requests.storage=100Gi

The count limit produces a similar error, replacing requests.storage with count/persistentvolumeclaims in the output.

Pattern 2: Kyverno policies​

ResourceQuota enforces aggregate limits but cannot cap individual PVC sizes or restrict which storage classes tenants may use. Kyverno fills those gaps.

Install Kyverno inside the tenant cluster as an App in the tenant cluster template. The examples below use kyverno.io/v1. Adjust the API version and validation syntax for the Kyverno version installed in your tenant clusters.

Restrict individual PVC size​

The following ClusterPolicy rejects any PVC whose storage request exceeds 50Gi:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-pvc-size
spec:
validationFailureAction: Enforce
background: false
rules:
- name: max-pvc-storage-request
match:
any:
- resources:
kinds:
- PersistentVolumeClaim
validate:
message: "PVC storage requests must not exceed 50Gi."
deny:
conditions:
any:
- key: "{{ request.object.spec.resources.requests.storage }}"
operator: GreaterThan
value: "50Gi"

Restrict allowed storage classes​

The following ClusterPolicy limits PVCs to an approved set of storage classes, preventing tenants from requesting storage from arbitrary provisioners such as Ceph RBD:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-storage-class
spec:
validationFailureAction: Enforce
background: false
rules:
- name: allowed-storage-classes
match:
any:
- resources:
kinds:
- PersistentVolumeClaim
validate:
message: "Only 'standard' and 'fast-ssd' storage classes are permitted."
pattern:
spec:
storageClassName: "standard | fast-ssd"

Embed Kyverno policies in a tenant cluster template​

Add ClusterPolicy manifests to the objects field of a VirtualClusterTemplate to deploy them inside every tenant cluster created from that template. Multiple manifests are separated by ---.

apiVersion: management.loft.sh/v1
kind: VirtualClusterTemplate
metadata:
name: standard-tenant
spec:
template:
helmRelease:
chart:
version: "0.25.0"
objects: |-
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-pvc-size
spec:
validationFailureAction: Enforce
background: false
rules:
- name: max-pvc-storage-request
match:
any:
- resources:
kinds:
- PersistentVolumeClaim
validate:
message: "PVC storage requests must not exceed 50Gi."
deny:
conditions:
any:
- key: "{{ request.object.spec.resources.requests.storage }}"
operator: GreaterThan
value: "50Gi"
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-storage-class
spec:
validationFailureAction: Enforce
background: false
rules:
- name: allowed-storage-classes
match:
any:
- resources:
kinds:
- PersistentVolumeClaim
validate:
message: "Only 'standard' and 'fast-ssd' storage classes are permitted."
pattern:
spec:
storageClassName: "standard | fast-ssd"

Template application order​

The platform applies objects before apps. ClusterPolicy manifests in the objects field are applied before the Kyverno App finishes installing its CRDs and admission webhook, so the apply will fail if Kyverno is not already present. To avoid this, either pre-install Kyverno in the tenant cluster before using this template, or package the ClusterPolicy manifests as a dedicated App listed after the Kyverno App rather than using the objects field.

PVC admission and quota enforcementTenant createsPVCTenant clusterResourceQuotaPattern 1KyvernoClusterPolicyPattern 2PVCcreatedRejectedRejectedProject quotaplatform levelpassespassesrejectsrejectsusagetracked
Storage quota enforcement flow for tenant PVC creation

Comparison​

CapabilityResourceQuotaKyverno
Aggregate storage limit per namespaceYesNo
PVC count limit per namespaceYesNo
Storage class-scoped aggregate limitYesNo
Per-PVC maximum sizeNoYes
Restrict allowed storage classesNoYes
Applied automatically using templatesYesYes (requires Kyverno App)

Use ResourceQuota for aggregate enforcement and Kyverno for per-object rules and storage class governance. The two approaches work together and can be included in the same template.

  • Manage Quotas: project-level quota configuration including aggregate requests.storage and count/persistentvolumeclaims limits
  • Create a template: add objects and Apps to a tenant cluster template