Skip to content

Commit 4b800a3

Browse files
committed
block: add volume_encryption flag for dm-crypt support
Adds a `volume_encryption` flag to the storage class configuration. When enabled, this flag ensures that the NVMe device discovered via NVMe-TCP is encrypted using the `dm-crypt` module on the client side. Users must also provide a volume-specific Kubernetes Secret containing the encryption passphrase. This secret must be referenced in the StorageClass using the following parameters: csi.storage.k8s.io/volume-secret-name: <secret-name> csi.storage.k8s.io/volume-secret-namespace: <namespace> Example: kubectl create secret generic volume-secret --from-literal=passphrase='your-passphrase'
1 parent 6b39734 commit 4b800a3

File tree

8 files changed

+130
-1
lines changed

8 files changed

+130
-1
lines changed

charts/vastblock/templates/storage-class.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
{{- $tenant_name := pluck "tenantName" $options $.Values.storageClassDefaults | first | quote -}}
2828
{{- $volume_group := pluck "volumeGroup" $options $.Values.storageClassDefaults | first | quote -}}
29+
{{- $volume_encryption := pluck "volumeEncryption" $options $.Values.storageClassDefaults | first | quote -}}
2930
{{- $mount_options := pluck "mountOptions" $options $.Values.storageClassDefaults | first -}}
3031
{{- $reclaim_policy := pluck "reclaimPolicy" $options $.Values.storageClassDefaults | first | quote -}}
3132
{{-
@@ -53,7 +54,7 @@ metadata:
5354
reclaimPolicy: {{ $reclaim_policy }}
5455
parameters:
5556
subsystem: {{ $subsystem }}
56-
{{- range $key, $value := dict "vip_pool_name" $vip_pool_name "vip_pool_fqdn" $vip_pool_fqdn "volume_group" $volume_group "transport_type" $transport_type "fsType" $fstype "tenant_name" $tenant_name }}
57+
{{- range $key, $value := dict "vip_pool_name" $vip_pool_name "vip_pool_fqdn" $vip_pool_fqdn "volume_group" $volume_group "volume_encryption" $volume_encryption "transport_type" $transport_type "fsType" $fstype "tenant_name" $tenant_name }}
5758
{{- if and $value (ne $value ( quote "" )) }}
5859
{{ $key }}: {{ if (kindIs "int" $value) }}{{ $value | quote }}{{ else }}{{ $value }}{{ end }}
5960
{{- end }}

charts/vastblock/values.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ storageClassDefaults:
5858
# can represent nested folder structures. For example:
5959
# - "folder1/folder2/block-{namespace}-{id}
6060
volumeGroup: ""
61+
# Enables encryption using LUKS on the device on the client side.
62+
# If set to true, the CSI driver will expect a volume-specific secret to be provided
63+
# with the encryption passphrase.
64+
# This secret must be referenced in the StorageClass with the keys:
65+
# csi.storage.k8s.io/volume-secret-name: <secret-name>
66+
# csi.storage.k8s.io/volume-secret-namespace: <namespace>
67+
68+
# Example Kubernetes Secret for volume encryption:
69+
# kubectl create secret generic volume-secret-namespace \
70+
# --from-literal=passphrase='my-secret-pass'
71+
72+
# Ensure the StorageClass has the following parameters set when volume_encryption is true:
73+
# csi.storage.k8s.io/volume-secret-name: volume-secret
74+
# csi.storage.k8s.io/volume-secret-namespace: default
75+
volume_encryption: true
6176
# Name of VAST VIP pool to use. Must specify either vipPool or vipPoolFQDN.
6277
vipPool: ""
6378
# The FQDN of the VIP pool to use. Must specify either vipPool or vipPoolFQDN.

examples/block/sc-vol-encryption.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
apiVersion: storage.k8s.io/v1
2+
kind: StorageClass
3+
metadata:
4+
name: vastdata-block
5+
parameters:
6+
csi.storage.k8s.io/controller-expand-secret-name: vast-mgmt
7+
csi.storage.k8s.io/controller-expand-secret-namespace: default
8+
csi.storage.k8s.io/controller-publish-secret-name: vast-mgmt
9+
csi.storage.k8s.io/controller-publish-secret-namespace: default
10+
csi.storage.k8s.io/provisioner-secret-name: vast-mgmt
11+
csi.storage.k8s.io/provisioner-secret-namespace: default
12+
subsystem: myblock
13+
vip_pool_name: vip1
14+
transport_type: TCP
15+
volume_encryption: "true"
16+
csi.storage.k8s.io/volume-secret-name: volume-secret
17+
csi.storage.k8s.io/volume-secret-namespace: default
18+
provisioner: block.csi.vastdata.com

vast_csi/block_utils.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,43 @@ def is_native_multipath_enabled():
3030
except Exception:
3131
return False
3232

33+
def is_luks_device(device_path: str) -> bool:
34+
"""
35+
Check if the given device is a LUKS-encrypted volume.
36+
37+
Args:
38+
device_path (str): The path to the block device.
39+
40+
Returns:
41+
bool: True if the device is LUKS, False otherwise.
42+
"""
43+
try:
44+
run(["cryptsetup", "isLuks", device_path])
45+
return True
46+
except ProcessExecutionError:
47+
return False
48+
49+
def is_crypto_luks(device_path):
50+
"""
51+
Determines whether the given device is a LUKS-encrypted block device.
52+
53+
Args:
54+
device_path (str): The path to the block device (e.g., /dev/nvme0n1).
55+
56+
Returns:
57+
bool: True if the device is using LUKS encryption (FSTYPE is crypto_LUKS), False otherwise.
58+
"""
59+
try:
60+
result = subprocess.run(
61+
['lsblk', '-no', 'FSTYPE', device_path],
62+
check=True,
63+
stdout=subprocess.PIPE,
64+
stderr=subprocess.DEVNULL,
65+
text=True
66+
)
67+
return result.stdout.strip() == 'crypto_LUKS'
68+
except subprocess.CalledProcessError:
69+
return False
3370

3471
def list_nvme_sessions():
3572
"""

vast_csi/builders/block.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class BlockProvisionBase(BaseVolumeBuilder):
3737
capacity_range: Optional[int] = None
3838
pvc_name: Optional[str] = None
3939
pvc_namespace: Optional[str] = None
40+
volume_encryption: Optional[str] = None
4041
volume_content_source: Optional[types.VolumeContentSource] = None # Either volume or snapshot
4142

4243
@classmethod
@@ -56,6 +57,7 @@ def from_parameters(
5657
vip_pool_fqdn = parameters.get("vip_pool_fqdn")
5758
vip_pool_name = parameters.get("vip_pool_name")
5859
volume_group = parameters.get("volume_group", "")
60+
volume_encryption = parameters.get("volume_encryption", "False")
5961
transport_type = parameters.get("transport_type", "TCP").upper()
6062
metadata = cls._parse_metadata_from_params(parameters)
6163
cls._validate_mount_src(vip_pool_name, vip_pool_fqdn, conf.use_local_ip_for_mount)
@@ -71,6 +73,7 @@ def from_parameters(
7173
tenant_name=tenant_name,
7274
transport_type=transport_type,
7375
volume_group=volume_group,
76+
volume_encryption=volume_encryption,
7477
vip_pool_name=vip_pool_name,
7578
vip_pool_fqdn=vip_pool_fqdn,
7679
cluster_name=cluster_name,
@@ -113,6 +116,8 @@ def volume_context(self) -> dict:
113116
context["vip_pool_name"] = self.vip_pool_name
114117
elif self.vip_pool_fqdn:
115118
context["vip_pool_fqdn"] = self.vip_pool_fqdn_with_prefix
119+
if self.volume_encryption:
120+
context["volume_encryption"] = self.volume_encryption
116121
return context
117122

118123

@@ -277,6 +282,7 @@ class StaticBlockVolumeBuilder(BaseVolumeBuilder):
277282
cluster_name: Optional[str] = None
278283
vip_pool_name: Optional[str] = None
279284
vip_pool_fqdn: Optional[str] = None
285+
volume_encryption: Optional[str] None
280286
transport_type: Optional[str] = "TCP"
281287

282288
@classmethod
@@ -294,6 +300,7 @@ def from_parameters(
294300
vip_pool_fqdn = parameters.get("vip_pool_fqdn")
295301
vip_pool_name = parameters.get("vip_pool_name")
296302
transport_type = parameters.get("transport_type", "TCP").upper()
303+
volume_encryption = parameters.get("volume_encryption")
297304
cls._validate_mount_src(vip_pool_name, vip_pool_fqdn, conf.use_local_ip_for_mount)
298305
cluster_name = parameters.get("cluster_name")
299306
return cls(
@@ -307,6 +314,7 @@ def from_parameters(
307314
vip_pool_fqdn=vip_pool_fqdn,
308315
transport_type=transport_type,
309316
cluster_name=cluster_name,
317+
volume_encryption=volume_encryption,
310318
)
311319

312320
@property
@@ -321,6 +329,8 @@ def volume_context(self) -> dict:
321329
context["vip_pool_name"] = self.vip_pool_name
322330
elif self.vip_pool_fqdn:
323331
context["vip_pool_fqdn"] = self.vip_pool_fqdn_with_prefix
332+
if self.volume_encryption:
333+
context["volume_encryption"] = self.volume_encryption
324334
return context
325335

326336
def build_volume(self) -> types.Volume:

vast_csi/builders/test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class TestVolumeBuilder(BaseVolumeBuilder):
2727
cluster_name: Optional[str] = None
2828
vip_pool_name: Optional[str] = None
2929
vip_pool_fqdn: Optional[str] = None
30+
volume_encryption: Optional[str] = None
3031
qos_policy: Optional[str] = None
3132
capacity_range: Optional[int] = None # Optional desired volume capacity
3233
pvc_name: Optional[str] = None

vast_csi/plugins/base.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def wrapper(self, request, context):
6868
params = {fld.name: value for fld, value in request.ListFields()}
6969
# secrets are not logged and not the part of function signature.
7070
secrets = params.pop("secrets", {})
71+
volume_secret = params.pop("volume_secret", {})
7172
missing_params = required_params - {"request", "context", "vms_session", "exit_stack"} - set(params)
7273

7374
# Get cluster_name from volume_id, snapshot_id or source_volume_id in case of id identifier with metadata
@@ -93,6 +94,9 @@ def wrapper(self, request, context):
9394
for line in stringify_dict(params):
9495
log(f"({method}) {line}")
9596

97+
if volume_secret:
98+
log(f"({method}) volume_secret: <redacted>")
99+
96100
if "vms_session" in required_params:
97101
# If secret exist and method signature requires `vms_session`
98102
# then `vms_session` with secret will be injected into function parameters
@@ -104,6 +108,12 @@ def wrapper(self, request, context):
104108
except LookupFieldError:
105109
params["vms_session"] = None
106110

111+
if "volume_secret" in required_params:
112+
params["volume_secret"] = volume_secret
113+
elif "volume_secret" in non_required_params:
114+
if volume_secret:
115+
params["volume_secret"] = volume_secret
116+
107117
exit_stack = ExitStack()
108118
if "exit_stack" in required_params:
109119
params["exit_stack"] = exit_stack

vast_csi/plugins/block.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
try_nvme_probes,
5454
change_io_policy,
5555
is_native_multipath_enabled,
56+
is_luks_device,
57+
is_luks_crypto,
5658
)
5759
from vast_csi.utils import (
5860
stringify_dict,
@@ -313,6 +315,7 @@ def ControllerPublishVolume(
313315
)
314316
vip_pool_name = volume_context.get("vip_pool_name")
315317
vip_pool_fqdn = volume_context.get("vip_pool_fqdn")
318+
volume_encryption = volume_context.get("volume_encryption")
316319
if vip_pool_fqdn:
317320
discovery_server = vip_pool_fqdn
318321
elif vip_pool_name:
@@ -329,6 +332,7 @@ def ControllerPublishVolume(
329332
host_nqn=blockhost.nqn,
330333
nguid=nguid,
331334
discovery_server=discovery_server,
335+
volume_encryption=volume_encryption
332336
)
333337
return types.CtrlPublishResp(publish_context=publish_context)
334338

@@ -426,6 +430,7 @@ def NodeStageVolume(
426430
vms_session=None,
427431
publish_context=None,
428432
volume_context=None,
433+
volume_secret=None,
429434
):
430435
exit_stack.enter_context(volume_locked(volume_id))
431436
volume_context = volume_context or dict()
@@ -450,6 +455,7 @@ def NodeStageVolume(
450455
host_nqn = publish_context["host_nqn"]
451456
nguid = publish_context["nguid"]
452457
discovery_server = publish_context["discovery_server"] # Either vip pool ip or fqdn
458+
volume_encryption = publish_context["volume_encryption"]
453459
need_resize = volume_context.get("need_resize", False)
454460

455461
if nvme_session := get_connected_session(host_nqn=host_nqn, sybsystem_nqn=subsystem_nqn):
@@ -479,6 +485,37 @@ def NodeStageVolume(
479485
device_path = device.DevicePath
480486
change_io_policy(device_name=device.Name, io_policy="round-robin")
481487

488+
if volume_encryption:
489+
luks_device_name = f"crypt-{volume_id}"
490+
luks_device_path = f"/dev/mapper/{luks_device_name}"
491+
492+
if volume_secret:
493+
passphrase = volume_secret.get("passphrase")
494+
if not passphrase:
495+
raise Abort(INVALID_ARGUMENT, "Missing passphrase for encrypted volume")
496+
else:
497+
raise Abort(INVALID_ARGUMENT, "Volume encryption detected, but 'volume_secret' not provided. Please provide a passphrase for the encrypted volume.")
498+
499+
if not os.path.exists(luks_device_path):
500+
is_luks = is_luks_device() and is_luks_crypto()
501+
502+
if not is_luks:
503+
logger.info(f"Formatting device {device_path} with LUKS")
504+
try:
505+
hostcmd.run(["cryptsetup", "luksFormat", "--batch-mode", device_path], input=passphrase.encode())
506+
except ProcessExecutionError as e:
507+
raise Abort(INTERNAL, f"LUKS format failed for {device_path}: {e.stderr.strip()}")
508+
509+
logger.info(f"Opening encrypted device {device_path} as {luks_device_name}")
510+
try:
511+
hostcmd.run(["cryptsetup", "open", device_path, luks_device_name], input=passphrase.encode())
512+
except ProcessExecutionError as e:
513+
raise Abort(INTERNAL, f"Failed to open LUKS device {device_path}: {e.stderr.strip()}")
514+
else:
515+
logger.info(f"LUKS device already opened at {luks_device_path}")
516+
517+
device_path = luks_device_path
518+
482519
if volume_capabilities.is_filesystem:
483520
fs_type = volume_capabilities.fs_type
484521
formatted = format_device(requested_fs=fs_type, device=device_path)

0 commit comments

Comments
 (0)