- 云原生
- 虚拟化
- 存储
- 后端
- 高可用
【免费下载链接】harvester
Open source hyperconverged infrastructure (HCI) software
本文基于 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.iso4.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_CIPHER | aes-xts-plain、aes-xts-plain64、aes-cbc-plain、aes-cbc-plain64、aes-cbc-essiv:sha256 |
CRYPTO_KEY_HASH | sha256、sha384、sha512 |
CRYPTO_KEY_SIZE | 256、384、512 |
CRYPTO_PBKDF | argon2i、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时依次校验:
cryptoOperation、sourceImageName、sourceImageNamespace非空;- 请求用户有权 GET 源镜像(SubjectAccessReview);
- 源镜像存在,且
ImageImported条件为 True(源镜像必须 ready); - 不能对已加密镜像再次加密(报
can not re-encrypt),不能对未加密镜像执行解密(报can not re-decrypt); - 加密操作时,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)。流程:
- 准备一个已加密的虚拟机镜像(如 4.4 节产物);
- 创建解密目标镜像,注意 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"- 完成后同时拥有加密与未加密两个镜像,可在 GUI 中任选其一启动虚拟机。
对 Story 1 中"删除源镜像"场景的说明:加密镜像与源镜像相互独立,加密完成后源镜像可安全删除,加密镜像可继续复用。
6. 实操三:加密数据卷(Data Volume)
数据卷路径更简单,无需克隆:
- 创建与 4.2 节相同的 Secret(
longhorn-crypto); - 创建与 4.3 节相同的加密 StorageClass(
harvester-longhorn-encryption); - 创建数据卷时选择该 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):
- 启动虚拟机后,在宿主机检查
/dev/longhorn:- 若卷已加密,
cryptsetup luksDump {pvc_name}会显示LUKS 头信息; - 若未加密,则提示 invalid LUKS device。
- 若卷已加密,
- 用错误口令执行
luksOpen:口令错误显示No key available with this passphrase;口令正确则显示which is in use (already mapped or mounted)——因为 CSI driver 已经把它挂载了,挂载路径可在同一宿主机的/dev/mapper下看到。 - 验证镜像加密:建议先启动一台不带额外磁盘的虚拟机,启动后
/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
相关推荐
Longhorn v2 卷加密:基于 dm_crypt 的 CSI 加密卷设计与实战指南
Longhorn v2 卷加密:基于 dm_crypt 的 CSI 加密卷设计与实战指南 导读 本文围绕 Longhorn 官方增强提案 20250107 v2
云原生存储高可用容器编排nerdctl 镜像加密与解密实战:基于 OCIcrypt 的镜像层加密方案
nerdctl 镜像加密与解密实战:基于 OCIcrypt 的镜像层加密方案 本文围绕 nerdctl 对 OCIcrypt(即 containerd 生态中的
CLI云原生如何在MonoGame中高效生成字体图集:从基础到高级优化
如何在MonoGame中高效生成字体图集:从基础到高级优化 在跨平台游戏开发中,字体渲染性能直接影响游戏体验。MonoGame作为强大的游戏开发框架,提供了完整
游戏开发图形学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考