☰
Harvester 块卷加密解密:基于 Longhorn 的虚拟机镜像与数据卷加密实战
2026/10/13 9:20:16 网站建设 项目流程
  • 云原生
  • 虚拟化
  • 存储
  • 后端
  • 高可用

【免费下载链接】harvester

Open source hyperconverged infrastructure (HCI) software

项目地址:https://gitcode.com/gh_mirrors/ha/harvester
点击查看免费下载

本文基于 Harvester 官方增强提案 20240805-encrypted-block-volume.md,系统讲解块卷加密/解密的完整技术方案:如何为虚拟机启动盘(boot volume,即虚拟机镜像)与数据卷(data volume)开启透明加解密、涉及的 Secret 与 StorageClass 配置、VirtualMachineImageAPI 变更,并结合仓库源码(admission webhook 的 validator/mutator、BackingImage 控制器)剖析参数如何一路同步到 Longhorn 底层,以及如何在宿主机上验证卷确实已被 LUKS 加密。

1. 背景与目标

Harvester 作为开源超融合基础设施(HCI)软件,其虚拟机的磁盘分为两类:

  • Boot Volume(启动卷):由虚拟机镜像(VirtualMachineImage)提供,最终落盘为 Longhorn BackingImage;
  • Data Volume(数据卷):通过 CSI 动态供应的 PVC 块卷,可热插拔(hot plug)。

本增强(对应提案文档 "Support Block Volume Encryption/Decryption")的核心目标是:

  • 为 Boot Volume 和 Data Volume 提供加密能力(数据卷支持热插拔);
  • 为用户提供清晰、可操作的加密/解密启用流程。

同时文档明确了非目标(Non-goals):

  • 不涵盖 GUI 相关的改动(GUI 侧无需感知加密,加密对虚拟机透明);
  • 该增强仅面向 admin 用户——独立部署的 Harvester 只有 admin 用户,没有用户管理体系。

2. 总体设计:为什么沿用 Longhorn 的加密方式

设计上的关键区别在于两类卷走的路径不同:

  • Boot Volume:Longhorn 通过clone 机制实现镜像加密/解密,Harvester 沿用该机制。这意味着加密后会存在两个虚拟机镜像对象——一个加密镜像、一个源镜像。加密完成后,源镜像可以被删除,加密镜像完全独立、可复用;
  • Data Volume:Longhorn 本身就支持块卷加解密,Harvester 无需额外拷贝,直接通过加密 StorageClass 创建数据卷即可。

无论哪种卷,Longhorn 都在卷 attach 到虚拟机时自动解密,因此虚拟机内部读写启动卷和数据卷都像普通明文卷一样透明。

2.1 为什么不直接走 CSI 加密接口

提案 Notes 一节记录了当时的设计权衡:

一种替代方案是遵循 CSI 加密接口——Harvester 只要求 CSI driver 提供一个加密卷,attach 它触发 CSI driver 解密,再把镜像数据拷贝进该卷。但这种方式存在性能问题:每次启动虚拟机都要把镜像数据拷贝一次到加密卷,而底层 BackingImage 并不需要;虽然可以用 snapshot 规避,但virtualmachineimages.harvesterhci.io与 Longhorn 耦合较紧,会增加用户创建加密镜像的步骤。因此当前决定沿用 Longhorn 自身的加密/解密方法,直到找到更优解。

3. API 变更:VirtualMachineImage 的加密参数

本增强对VirtualMachineImageAPI 的变更(提案中的 diff 原文):

type VirtualMachineImageSpec struct { - // +kubebuilder:validation:Enum=download;upload;export-from-volume + // +kubebuilder:validation:Enum=download;upload;export-from-volume;clone SourceType VirtualMachineImageSourceType `json:"sourceType"` + // +optional + SecurityParameters *VirtualMachineImageSecurityParameters `json:"securityParameters,omitempty"` } +type VirtualMachineImageSecurityParameters struct { + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=encrypt;decrypt + CryptoOperation VirtualMachineImageCryptoOperationType `json:"cryptoOperation"` + + // +kubebuilder:validation:Required + SourceImageName string `json:"sourceImageName"` + + // +kubebuilder:validation:Required + SourceImageNamespace string `json:"sourceImageNamespace"` +}

对照当前仓库源码 image.go,该设计已完整落地:

  • VirtualMachineImageSecurityParameters结构体包含三个必填字段:cryptoOperation(枚举encrypt;decrypt)、sourceImageName、sourceImageNamespace;
  • SourceType枚举当前为download;upload;export-from-volume;restore;clone(源码中后续还新增了restore类型),clone即本增强引入的克隆/加密来源类型;
  • VirtualMachineImageCryptoOperationTypeEncrypt = "encrypt"、VirtualMachineImageCryptoOperationTypeDecrypt = "decrypt"两个常量也已定义。

4. 实操一:加密虚拟机镜像(Boot Volume)

对应提案 Story 1 与 "Encrypt a Virtual Machine Image" 小节,完整流程共 4 步。

4.1 准备源镜像

先有一个未加密的源虚拟机镜像(可通过上传或 URL 下载获得),例如:

apiVersion: harvesterhci.io/v1beta1 kind: VirtualMachineImage metadata: annotations: harvesterhci.io/storageClassName: harvester-longhorn name: alpine-extended namespace: default spec: displayName: alpine-extended-3.20.2-x86_64.iso retry: 3 sourceType: download url: https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-extended-3.20.2-x86_64.iso

4.2 创建加密 Secret

apiVersion: v1 kind: Secret metadata: name: longhorn-crypto namespace: longhorn-system # you could create secret in any namespace stringData: CRYPTO_KEY_VALUE: "Some secret value here" CRYPTO_KEY_PROVIDER: "secret" CRYPTO_KEY_CIPHER: "aes-xts-plain64" CRYPTO_KEY_HASH: "sha256" CRYPTO_KEY_SIZE: "256" CRYPTO_PBKDF: "argon2i"

各字段的合法取值在源码中有明确校验。storageclass webhook validator 定义了白名单:

Secret 字段合法取值
CRYPTO_KEY_CIPHERaes-xts-plain、aes-xts-plain64、aes-cbc-plain、aes-cbc-plain64、aes-cbc-essiv:sha256
CRYPTO_KEY_HASHsha256、sha384、sha512
CRYPTO_KEY_SIZE256、384、512
CRYPTO_PBKDFargon2i、argon2id、pbkdf2
CRYPTO_KEY_PROVIDER仅secret
CRYPTO_KEY_VALUE必须非空(即真实口令)

validateEncryptionSecret(validator.go#L248-L286)会在创建加密 StorageClass 时逐字段检查,缺失字段报missing field,取值非法报invalid field。

4.3 创建加密 StorageClass

kind: StorageClass apiVersion: storage.k8s.io/v1 metadata: name: harvester-longhorn-encryption provisioner: driver.longhorn.io allowVolumeExpansion: true parameters: numberOfReplicas: "2" staleReplicaTimeout: "2880" # 48 hours in minutes fromBackup: "" migratable: "true" encrypted: "true" csi.storage.k8s.io/provisioner-secret-name: "longhorn-crypto" csi.storage.k8s.io/provisioner-secret-namespace: "longhorn-system" csi.storage.k8s.io/node-publish-secret-name: "longhorn-crypto" csi.storage.k8s.io/node-publish-secret-namespace: "longhorn-system" csi.storage.k8s.io/node-stage-secret-name: "longhorn-crypto" csi.storage.k8s.io/node-stage-secret-namespace: "longhorn-system"

关键点:encrypted: "true"触发加密校验;6 个csi.storage.k8s.io/*-secret-*参数必须指向同一个Secret(组名+命名空间完全一致),否则 webhook 会拒绝创建——这一点由validateEncryptionParams(validator.go#L288-L331)保证,它遍历util.CSIEncryptionSecretKeyPairs中列出的四组 key 对(provisioner/node-stage/node-publish/node-expand,其中 node-expand 为可选)并强制一致性。

4.4 用 clone 方式创建加密镜像

apiVersion: harvesterhci.io/v1beta1 kind: VirtualMachineImage metadata: annotations: harvesterhci.io/storageClassName: harvester-longhorn-encryption name: encrypted-alpine-extended namespace: default spec: displayName: encrypted-alpine-extended-3.20.2-x86_64.iso retry: 3 sourceType: clone securityParameters: cryptoOperation: "encrypt" sourceImageName: "alpine-extended" # it's VirtualMachineImage and not display name sourceImageNamespace: "default"

也可以在 GUI 中通过 "Edit as YAML" 按钮提交同样的内容。sourceImageName必须填源 VirtualMachineImage 的 name(而非 displayName)。加密完成后,加密镜像就是一个普通镜像,可以直接在 GUI 创建虚拟机时选择。

从源码看,clone 场景的securityParameters是必填的:CheckSecurityParameters 在sourceType: clone时依次校验:

  1. cryptoOperation、sourceImageName、sourceImageNamespace非空;
  2. 请求用户有权 GET 源镜像(SubjectAccessReview);
  3. 源镜像存在,且ImageImported条件为 True(源镜像必须 ready);
  4. 不能对已加密镜像再次加密(报can not re-encrypt),不能对未加密镜像执行解密(报can not re-decrypt);
  5. 加密操作时,annotation 中的harvesterhci.io/storageClassName必须存在,对应 StorageClass 必须存在、其encrypted参数必须为"true",且不能是绑定到特定 BackingImage 的 StorageClass(backingImage参数非空时报错)。

此外securityParameters一经创建不可修改(SecurityParameterConsistency,validator.go#L339-L344)。

5. 实操二:解密虚拟机镜像

解密不需要新的加密 StorageClass——因为密钥信息可以从源加密镜像反查回来(源码中IsDecryptOperation时直接使用源镜像的spec.storageClassParameters,见 backingimage.go#L161-L169)。流程:

  1. 准备一个已加密的虚拟机镜像(如 4.4 节产物);
  2. 创建解密目标镜像,注意 annotation 使用非加密StorageClass(如harvester-longhorn),cryptoOperation设为decrypt:
apiVersion: harvesterhci.io/v1beta1 kind: VirtualMachineImage metadata: annotations: harvesterhci.io/storageClassName: harvester-longhorn name: decrypt-alpine-extended namespace: default spec: displayName: decrypt-alpine-extended-3.20.2-x86_64.iso retry: 3 sourceType: clone securityParameters: cryptoOperation: "decrypt" sourceImageName: "encrypted-alpine-extended" # not display name sourceImageNamespace: "default"
  1. 完成后同时拥有加密与未加密两个镜像,可在 GUI 中任选其一启动虚拟机。

对 Story 1 中"删除源镜像"场景的说明:加密镜像与源镜像相互独立,加密完成后源镜像可安全删除,加密镜像可继续复用。

6. 实操三:加密数据卷(Data Volume)

数据卷路径更简单,无需克隆:

  1. 创建与 4.2 节相同的 Secret(longhorn-crypto);
  2. 创建与 4.3 节相同的加密 StorageClass(harvester-longhorn-encryption);
  3. 创建数据卷时选择该 StorageClass 即可。

此时卷内数据在宿主机上即为密文;在虚拟机内可正常读写。此外,该加密卷可作为热插拔卷(hot plug volume)attach 到任意虚拟机,attach 时由 CSI driver 自动解密。在 Rancher Integration 模式下,非 admin 用户也可使用该 StorageClass 创建加密卷(权限边界见第 9 节 FAQ)。

7. 源码级实现剖析:参数如何同步到 Longhorn

7.1 Admission Webhook Mutator:注入 storageClassParameters

提案 "Implementation Overview" 指出:Harvester 使用与 Longhorn 相同的参数,只需将这些参数同步给 Longhorn 的 BackingImage 即可激活加解密。

在对象被控制器处理前,admission webhook 的 mutator 会把 annotation 指向的 StorageClass 参数合并进spec.storageClassParameters。实现见 mutator.go:

  • mergeStorageClassParams构建允许透传的参数白名单:nodeSelector、diskSelector、numberOfReplicas、staleReplicaTimeout、dataLocality、encrypted,再加上util.CSIEncryptionSecretKeyPairs(四组 CSI secret name/namespace 键);
  • PatchImageSCParams以 JSON Patch 写入/spec/storageClassParameters(已存在则为replace)。

这与提案中示例的"注入后对象"一致:storageClassParameters中出现了csi.storage.k8s.io/provisioner-secret-name等 6 个 secret 参数与encrypted: "true"。

7.2 BackingImage 控制器:clone + 加密参数下发

backingimage.go 中,当sourceType == clone时,控制器创建 LonghornBackingImage并填充spec.sourceParameters:

case harvesterv1.VirtualMachineImageSourceTypeClone: bi.Spec.SourceParameters[lhv1beta2.DataSourceTypeCloneParameterEncryption] = vmio.GetSecurityCryptoOption(vmi) ... bi.Spec.SourceParameters[lhv1beta2.DataSourceTypeCloneParameterBackingImage] = sourceBiName targetImage := vmi if vmio.IsDecryptOperation(vmi) { // if try to decrypt image, we should get the storage class of source virtual machine image. targetImage = sourceImage } bi.Spec.SourceParameters[lhv1beta2.DataSourceTypeCloneParameterSecret] = vmio.GetSCParameters(targetImage)[util.CSINodePublishSecretNameKey] bi.Spec.SourceParameters[lhv1beta2.DataSourceTypeCloneParameterSecretNamespace] = vmio.GetSCParameters(targetImage)[util.CSINodePublishSecretNamespaceKey]

即提案中展示的 Longhorn 对象:

apiVersion: longhorn.io/v1beta2 kind: BackingImage metadata: name: vmi-d93d3210-e547-463f-8f3a-30d3ce8e87a9 namespace: longhorn-system spec: minNumberOfCopies: 3 sourceParameters: backing-image: "{this is the backing image name, will get it via VirtualMachineImage}" encryption: "encrypt" secret: "longhorn-crypto" secret-namespace: "longhorn-system" sourceType: clone

两个值得注意的实现细节:

  • sourceParameters中的backing-image不是源镜像 name,而是通过源镜像反查到的源 Longhorn BackingImage 名称(util.GetBackingImageName(bib.biCache, sourceImage)),这就是"clone 机制"的落点;
  • 密钥取自哪里由操作类型决定:加密时取新镜像自身的storageClassParameters;解密时取源加密镜像的storageClassParameters——这解释了为什么解密不需要 annotation 再指定加密 StorageClass。

随后createStorageClass(backingimage.go#L176-L203)为这个 BackingImage 创建一个专属 StorageClass,其parameters.backingImage指向新 BackingImage,并携带全部 CSI secret 参数与encrypted: "true"(对应提案中longhorn-encrypted-alpine-extended示例):

allowVolumeExpansion: true apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: longhorn-encrypted-alpine-extended parameters: backingImage: vmi-d93d3210-e547-463f-8f3a-30d3ce8e87a9 backingImageDataSourceType: clone csi.storage.k8s.io/node-publish-secret-name: encryption csi.storage.k8s.io/node-publish-secret-namespace: default csi.storage.k8s.io/node-stage-secret-name: encryption csi.storage.k8s.io/node-stage-secret-namespace: default csi.storage.k8s.io/provisioner-secret-name: encryption csi.storage.k8s.io/provisioner-secret-namespace: default encrypted: "true" migratable: "true" numberOfReplicas: "2" staleReplicaTimeout: "2880" provisioner: driver.longhorn.io reclaimPolicy: Delete volumeBindingMode: Immediate

此后虚拟机以该专属 StorageClass 供应 PVC,Longhorn 在 attach 时自动完成 LUKS 解密,虚拟机内呈现明文卷。

7.3 Validator 全景:六项校验的源码对应

提案列出的 6 条校验要求在当前代码中的落点:

#提案要求源码位置
1创建加密 StorageClass 时,三组(当前源码扩展为四组,node-expand 可选)csi.storage.k8s.io/*-secret-name/namespace必须一致validateEncryptionParams
2创建加密 StorageClass 时 Secret 必须存在,且字段合法validateEncryption / validateEncryptionSecret
3被 StorageClass 引用的 Secret 不可删除secret webhook validator,借助索引器 StorageClassBySecret 反查引用它的加密 StorageClass,删除时报Secret is used by encrypted storage classes
4创建加密虚拟机镜像时,annotation 指向的 StorageClass 必须存在CheckSecurityParameters
5创建加密/解密镜像时,加解密相关参数必须存在CheckSecurityParameters(cryptoOperation/sourceImageName/sourceImageNamespace)
6加密镜像不能再加密,未加密镜像不能解密CheckSecurityParameters(can not re-encrypt/can not re-decrypt)

8. 验证方法:如何确认卷真的被加密了

提案 Test Plan 中给出的宿主机级验证手法非常实用(对应截图 06.png、07.png):

  1. 启动虚拟机后,在宿主机检查/dev/longhorn:
    • 若卷已加密,cryptsetup luksDump {pvc_name}会显示LUKS 头信息;
    • 若未加密,则提示 invalid LUKS device。
  2. 用错误口令执行luksOpen:口令错误显示No key available with this passphrase;口令正确则显示which is in use (already mapped or mounted)——因为 CSI driver 已经把它挂载了,挂载路径可在同一宿主机的/dev/mapper下看到。
  3. 验证镜像加密:建议先启动一台不带额外磁盘的虚拟机,启动后/dev/longhorn下只有一个盘,用上述方法验证。

提案 Test Plan 的完整验证清单:

  • 卷可按预期加密与解密;
  • 加密的 boot volume 与 data volume 可正常 attach 到虚拟机;
  • 加密 data volume 作为热插拔卷 attach 正常工作;
  • 使用加密卷时,虚拟机快照恢复、备份、迁移(migration)行为符合预期;
  • 无正确密钥时卷不可访问(按上述 LUKS 检查法验证);
  • 虚拟机镜像本身已加密。

9. 限制条件(Limitations)

提案明确列出当前版本不支持的场景,使用时需注意:

  • 不支持从加密的 boot volume / data volume导出为镜像(export-from-volume);
  • 不支持下载加密镜像再上传以复用(密钥绑定在集群内 Secret 中,镜像文件本身无法脱离该环境使用);
  • 不支持数据卷在加密与未加密 StorageClass 之间跨类恢复(encrypted→unencrypted 或 unencrypted→encrypted)。

升级策略(Upgrade strategy):无(None)——该功能为增量能力,不涉及存量数据迁移。

10. FAQ

Q1:用 K8s Secret 加密卷,如何防止不同用户读取 Secret?

取决于部署形态。独立部署的 Harvester 只有一个 admin 用户,无法规避;Rancher Integration 模式下,可以为不同用户划分不同的 Project/Namespace,从而阻止用户读取他人命名空间中的 Secret(参见 Harvester 官方文档中 Multi-Tenancy with Rancher Integration 的示例说明)。

Q2:能否阻止普通用户创建 StorageClass?

可以。按 Q1 的方式在 Rancher Integration 中划分 Project 与 Namespace:StorageClass 是集群级资源,普通用户可以读,但不能创建;唯一途径是请求 admin 代建。

Q3:我们如何"真正"确认一个卷是加密的?

对最终用户(虚拟机内部)而言永远不可见——CSI driver 在卷 attach 时自动解密。但能登录宿主机的 admin 可以在虚拟机启动后执行cryptsetup luksDump {pvc_name}检查/dev/longhorn/pvc-xxxx,验证方式见第 8 节的测试计划截图说明。

11. 小结

本增强通过"Secret + 加密 StorageClass +sourceType: clone的 VirtualMachineImage"三件套,把 Longhorn 的块级加密能力暴露给 Harvester 用户:boot volume 走 BackingImage clone 加密(加密后镜像独立于源镜像、可删除源镜像),data volume 走 StorageClass 直接加密并支持热插拔,attach 时自动解密对用户完全透明。仓库中的 admission webhook(storageclass validator、secret validator、VMI validator / mutator 与 BackingImage 控制器 共同保证了参数一致性校验、密钥引用防误删与向 Longhorn 的参数同步,使整套加密方案既可在 YAML 中完整复现,又有源码级的安全兜底。

  • 云原生
  • 虚拟化
  • 存储
  • 后端
  • 高可用

【免费下载链接】harvester

Open source hyperconverged infrastructure (HCI) software

项目地址:https://gitcode.com/gh_mirrors/ha/harvester
点击查看免费下载
上一篇:OpenAPI-Specification示例项目解析:学习实战API设计
下一篇:Rundeck任务调度大师课:Job选项与变量传递的终极配置指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询