Problem
Mayastor supports pool-level (lvolstore) encryption via SPDK's crypto vbdev. All PVCs on a given DiskPool share one key, which does not serve:
- Distinct DEK per PVC for compliance controls that require per-data-owner key identifiers — PCI-DSS §3.5, HIPAA §164.312(a)(2)(iv), FedRAMP equivalents. Audit trails today distinguish pools, not tenants-within-pool.
- Cryptographic erase per PVC. Right-to-erasure, dataset expiration, and offboarding workflows require O(1) provable data destruction by destroying one key. Pool-level encryption offers this only at pool granularity.
Threat model in scope: encryption at rest — disk theft, backup exfiltration, decommissioned-media leakage, and the compliance controls above.
Proposed Solution
Add per-volume encryption by stacking the existing SPDK crypto vbdev one level higher: above the lvol, below the NVMe-oF target. Each PVC carries its own DEK, sourced from a K8s Secret referenced by its StorageClass. External key stores (Vault, cloud KMS, HSM) are reached via the standard External Secrets Operator (ESO) bridge — Mayastor's interface is a K8s Secret reference, and the ecosystem handles the rest.
I intend to implement this across openebs/mayastor and openebs/mayastor-control-plane.
openebs/mayastor — data plane
- Extend
ReplicaArgs with an optional encryption key, mirroring PoolArgs::with_encryption.
- Wrap the lvol via
create_crypto_vbdev_on_base_bdev and publish the wrapping bdev over NVMe-oF.
- Persist a key-name reference (never key material) as an lvol xattr. On pool import, hold encrypted lvols in a keyed-out state until the control plane replays the key via a new
SetReplicaKey RPC.
- Advertise a
MayastorFeatures.per_replica_encryption capability bit and gate the code behind ENABLE_REPLICA_ENCRYPTION, mirroring the existing diskpool_encryption pattern.
openebs/mayastor-control-plane — control plane
- Add a
VolumeEncryption { cipher, secret_ref } type to VolumeSpec (shape — additive vs. collapsed — is the open question below). Promote CreateReplica.encrypted from a bookkeeping-only field into a plumbed request property.
- Add a
SecretReader in the core agent that resolves K8s Secret refs to plaintext DEKs held only in process memory.
- Thread the resolved key through every replica-creating path: primary create, rebuild, move, clone, snapshot restore.
- Extend the scheduler with an additive
node.per_replica_encryption capability filter. The current pool-affinity filter and encryption_preference_soft() knob stay for pool-level users.
- Add a nexus child-alignment reconciliation pass so mixed encrypted / plaintext children in the same nexus do not fail alignment during rollout.
openebs/mayastor-extensions — CSI
- Template a per-PVC K8s Secret via StorageClass parameters (
csi.storage.k8s.io/provisioner-secret-name and namespace).
The CSI node plugin and consumer-side nvme connect are unaffected. The crypto boundary sits below the target.
Non-goals
- Not tenant isolation from the Mayastor operator. The io-engine holds the plaintext DEK in memory while serving reads. True operator isolation requires consumer-side encryption or confidential computing — a separate feature.
- Not in-place key rotation. SPDK's crypto vbdev has no rekey primitive. v1 rotation is create-new + copy + swap.
- Ciphertext at the lvol layer, which precludes future lvol-layer compression or cross-volume dedup for encrypted volumes.
Rollout
Phased, feature-flagged at every layer. Opt-in for at least one release before default-on. Perf benchmark (AES-NI on/off, replica count sweep) lands with the data-plane PRs. Correctness tests through the existing lvs-eval harness; hardware-dependent code paths gated in CI.
Alternatives
- Pool-level encryption alone (status quo). Does not provide per-PVC key scope or per-PVC cryptographic erase.
- LUKS on the consumer (wrapping CSI or init-container,
secrets-store-csi-driver pattern). Stronger — the platform never sees plaintext DEKs. Complementary, not competing; addresses an operator-isolation threat model this proposal explicitly does not.
- NVMe-oF TLS. Orthogonal; solves wire encryption only.
- Confidential-computing io-engine (SEV-SNP, TDX). Deferred; requires SPDK-side work and hardware availability.
Longhorn and Ceph have shipped similar per-volume approaches.
Additional context
Open question — VolumeSpec shape
Two options for expressing per-volume encryption on VolumeSpec:
-
Additive: keep the existing encrypted: bool and add encryption: Option<VolumeEncryption> alongside. Backward-compatible; no etcd migration; introduces two fields with overlapping semantics.
-
Collapsed: replace both with a single enum:
EncryptionMode { None | PoolAffinity | PerVolume { cipher, secret_ref } }
Single source of truth; requires an etcd schema migration and breaks any client that reads the boolean.
Which shape do you prefer to maintain?
Problem
Mayastor supports pool-level (lvolstore) encryption via SPDK's crypto vbdev. All PVCs on a given DiskPool share one key, which does not serve:
Threat model in scope: encryption at rest — disk theft, backup exfiltration, decommissioned-media leakage, and the compliance controls above.
Proposed Solution
Add per-volume encryption by stacking the existing SPDK crypto vbdev one level higher: above the lvol, below the NVMe-oF target. Each PVC carries its own DEK, sourced from a K8s Secret referenced by its StorageClass. External key stores (Vault, cloud KMS, HSM) are reached via the standard External Secrets Operator (ESO) bridge — Mayastor's interface is a K8s Secret reference, and the ecosystem handles the rest.
I intend to implement this across
openebs/mayastorandopenebs/mayastor-control-plane.openebs/mayastor— data planeReplicaArgswith an optional encryption key, mirroringPoolArgs::with_encryption.create_crypto_vbdev_on_base_bdevand publish the wrapping bdev over NVMe-oF.SetReplicaKeyRPC.MayastorFeatures.per_replica_encryptioncapability bit and gate the code behindENABLE_REPLICA_ENCRYPTION, mirroring the existingdiskpool_encryptionpattern.openebs/mayastor-control-plane— control planeVolumeEncryption { cipher, secret_ref }type toVolumeSpec(shape — additive vs. collapsed — is the open question below). PromoteCreateReplica.encryptedfrom a bookkeeping-only field into a plumbed request property.SecretReaderin the core agent that resolves K8s Secret refs to plaintext DEKs held only in process memory.node.per_replica_encryptioncapability filter. The current pool-affinity filter andencryption_preference_soft()knob stay for pool-level users.openebs/mayastor-extensions— CSIcsi.storage.k8s.io/provisioner-secret-nameand namespace).The CSI node plugin and consumer-side
nvme connectare unaffected. The crypto boundary sits below the target.Non-goals
Rollout
Phased, feature-flagged at every layer. Opt-in for at least one release before default-on. Perf benchmark (AES-NI on/off, replica count sweep) lands with the data-plane PRs. Correctness tests through the existing
lvs-evalharness; hardware-dependent code paths gated in CI.Alternatives
secrets-store-csi-driverpattern). Stronger — the platform never sees plaintext DEKs. Complementary, not competing; addresses an operator-isolation threat model this proposal explicitly does not.Longhorn and Ceph have shipped similar per-volume approaches.
Additional context
Open question —
VolumeSpecshapeTwo options for expressing per-volume encryption on
VolumeSpec:Additive: keep the existing
encrypted: booland addencryption: Option<VolumeEncryption>alongside. Backward-compatible; no etcd migration; introduces two fields with overlapping semantics.Collapsed: replace both with a single enum:
Single source of truth; requires an etcd schema migration and breaks any client that reads the boolean.
Which shape do you prefer to maintain?