Rook OSD 密钥加密密钥(KEK)轮换机制:设计与实现深度解析
【免费下载链接】rookStorage Orchestration for Kubernetes项目地址: https://gitcode.com/gh_mirrors/roo/rook
本指南以 Rook 设计文档 design/ceph/key-encryption-key-rotation.md 为核心骨架,结合仓库内 Operator 与 Daemon 层的完整源码实现,系统讲解 Rook 如何在无停机的前提下,通过 Kubernetes CronJob 周期性轮换加密 OSD 底盘的密钥加密密钥(Key Encryption Key,KEK),并同步更新 KMS 中的密钥版本。读完本文,你将掌握security.keyRotation配置的完整语义、五步安全轮换算法的工作原理,以及底层luksAddKey/luksChangeKey/luksKillSlot命令的封装细节与失败边界。
背景:为什么需要 KEK 轮换
Rook 使用 dm-crypt 并借助 cryptsetup 的 LUKS 扩展(cryptsetup(8))来加密承载 OSD 的 PVC。加密设备本身使用数据加密密钥(DEK),而 DEK 则由一把"密钥加密密钥"(KEK)保护,KEK 通常存放在 Rook 支持的各类密钥管理服务(KMS)中,包括:
- Kubernetes Secrets(默认)
- HashiCorp Vault
- IBM Key Protect
- KMIP(Key Management Interoperability Protocol)
- Azure Key Vault(源码 pkg/daemon/ceph/osd/kms/kms.go 中同样支持,用于 OSD 加密场景)
从安全角度看,长期不更换 KEK 会扩大密钥泄露带来的影响面。Rook 需要能够周期性地轮换 KEK,并同时在两个位置原子化地更新:一是承载 OSD 的加密设备(LUKS 密钥槽),二是 KMS 中保存的密钥本身。该功能的目标版本为 release-1.11.1(见设计文档 front-matter 中的target-version)。
目标(Goals)
- Rook 能够按计划周期性轮换 KEK,同步更新承载 OSD 的加密设备与 KMS,全程无停机。
非目标(Non-Goals)
- 暂不支持按需(on-demand)触发KEK 轮换。当前实现仅基于 CronJob 定时调度执行。
整体设计概览
整个 KEK 轮换特性由以下四个部分组成,它们共同构成了一个从"声明式配置"到"底层设备操作"的完整闭环:
- CRD 配置入口:
CephCluster.spec.security.keyRotation字段(enabled+schedule),对应 API 类型KeyRotationSpec,定义于 pkg/apis/ceph.rook.io/v1/types.go。 - Operator 侧调度器:为每个加密的 PVC 后端 OSD 创建命名规律、带亲和性约束的 CronJob,实现位于 pkg/operator/ceph/cluster/osd/key_rotation.go。
- CLI 命令入口:CronJob 容器内执行
rook key-management rotate-key命令,注册于 cmd/rook/secret.go。 - Daemon 侧轮换逻辑:实际执行"取旧钥→写槽→生成新钥→更新 KMS→验证→清理旧槽"的算法,实现在 pkg/daemon/ceph/osd/key_rotation.go,底层 LUKS 命令封装在 pkg/daemon/ceph/osd/encryption.go。
下面逐一深入每个环节。
KEK 轮换 CronJob:调度器的设计与实现
设计文档明确了 CronJob 的三条关键约束,源码 pkg/operator/ceph/cluster/osd/key_rotation.go 对它们逐一落实:
1. 一个加密 OSD 对应一个 CronJob
命名规则为rook-ceph-osd-key-rotation-<osdID>(源码常量keyRotationCronJobAppNameFmt = "rook-ceph-osd-key-rotation-%d")。makeKeyRotationCronJob在 key_rotation.go#L204-L234 中构造 CronJob,其中:
- 调度表达式
schedule取自spec.Security.KeyRotation.Schedule; - 若未配置 schedule,默认使用
@weekly(源码注释说明默认值写在代码中而非 CRD 默认值,是为了避免 CRD 默认值机制带来的兼容性问题); ConcurrencyPolicy设置为ForbidConcurrent,保证同一 OSD 的轮换任务不会并发执行,避免密钥槽竞争。
2. 使用 OSD Pod 亲和性调度到同一节点
applyKeyRotationPlacement在 key_rotation.go#L48-L65 中清空TopologySpreadConstraints与PodAntiAffinity,并设置RequiredDuringSchedulingIgnoredDuringExecution级别的PodAffinity:以 OSD 的标签(app=rook-ceph-osd、osd-id=<ID>等)为LabelSelector,以主机名kubernetes.io/hostname为TopologyKey。这样 CronJob 的 Pod 会被强制调度到目标 OSD 所在的宿主机——因为轮换必须操作宿主机上的加密设备。对应的单元测试 pkg/operator/ceph/cluster/osd/key_rotation_test.go 验证了该亲和性在 Affinity 为 nil 或已有反亲和性时的覆盖行为。
3. 与 OSD 共享宿主机设备访问能力
CronJob Pod 挂载了三类卷(见getKeyRotationPodTemplateSpec,key_rotation.go#L112-L201):
devices:宿主机的/dev目录,用于访问加密块设备;udev:宿主机的/run/udev目录,用于与宿主机的 udev 守护进程交互——源码注释明确指出"cryptsetup synchronizes with udev on host through semaphore",因此同时设置了HostIPC: true;bridge:宿主机上的 bridge 目录(<dataDirHostPath>/<namespace>/<pvcName>/ceph-<osdID>),内部包含 Rook 为加密设备在/var/lib/ceph/osd/下建立的映射入口(<blockType>-tmp)。
轮换任务处理的设备列表依据 OSD 拓扑动态生成:block 设备(bluestore_block)必然包含;若配置了 metadata PVC 与 wal PVC,则追加bluestore_metadata与bluestore_wal设备(key_rotation.go#L139-L145)。此外,如果 KMS 为 Vault,还会额外挂载 Vault 的 TLS 证书卷。
CronJob 的容器以特权模式(privileged: true)、root 用户运行,使用 Rook Operator 镜像,并携带 KMS 配置环境变量(kms.ConfigToEnvVar)、ROOK_CEPH_VERSION以及集群配置环境变量;命令行参数为key-management rotate-key <pvcClaimName> <device1> [device2] [device3]。
协调(Reconcile)流程
reconcileKeyRotationCronJob(key_rotation.go#L237-L303)是调度器的心脏:
- 未启用:通过标签选择器
app=rook-ceph-osd-key-rotation执行DeleteCollection,清理所有已存在的轮换 CronJob(忽略 NotFound 错误); - 已启用:列出带
app=rook-ceph-osd与osd-over-pvc标签的 OSD Deployment,逐个解析 OSD 信息与 PVC 属性,跳过未开启加密(!osdProps.encrypted)的 OSD;随后通过SetOwnerReference将 CronJob 归属于对应 OSD Deployment(保证 OSD 删除时 CronJob 被级联回收),最后以CreateOrUpdateCronJob幂等地创建或更新 CronJob。
KMS 侧更新能力:UpdateSecret
设计文档要求为每种 KMS 类型补充KMS.UpdateSecret(),用于将新的 KEK 写入 KMS。其统一接口实现在 pkg/daemon/ceph/osd/kms/kms.go#L233-L266:
- Kubernetes Secrets:直接更新同名 Secret 的值(
updateSecretInKubernetes); - HashiCorp Vault:按
GenerateOSDEncryptionSecretName规范化的名称,通过 Vault 的PutSecret覆盖写入(Vault 原生保留版本历史); - IBM Key Protect / KMIP / Azure Key Vault:当前返回错误
"update secret is not supported for the %q KMS"。也就是说,截至当前仓库实现,KEK 轮换的 KMS 更新能力仅对 Kubernetes Secrets 与 Vault 可用;IBM Key Protect 与 KMIP 在创建(PutSecret)和查询(GetSecret)上已有实现(kms.go#L82-L231),但轮换场景下的原地更新尚未支持。这是使用该功能前必须确认的约束。
KMS 提供方由KMS_PROVIDER连接参数决定(kms.go#L56-L80),为空时默认使用 Kubernetes Secrets。
五步安全轮换算法:KMS 与 LUKS 槽位的状态机
设计文档给出了 KEK 轮换的核心状态机,以 K1(KMS 中的当前 KEK)与 K2(待加入的新 KEK)为两个密钥版本,通过 LUKS 的槽位 0 与槽位 1 做缓冲,保证任何一步中断都不会让设备"打不开":
| Step | 操作 | LUKS Slot 0 | LUKS Slot 1 | KMS 中的 Key |
|---|---|---|---|---|
| 1 | 获取 K1 | K1 | K1 | |
| 2 | 将 K1 加入 slot 1 | K1 | K1 | K1 |
| 3 | 生成 K2 并加入 slot 0 | K2 | K1 | K1 |
| 4 | 将 K2 更新到 KMS | K2 | K1 | K2 |
| 5 | 从 slot 1 移除 K1 | K2 | K2 |
设计文档特别强调:上述步骤保证了即使操作在任意一步被打断,KMS 中的 KEK 也始终能够打开加密设备,所有中断引发的边界情况都被覆盖。
源码层面的对应实现:Daemon 侧入口RotateKeyEncryptionKey(pkg/daemon/ceph/osd/key_rotation.go#L31-L102)与状态机逐条对应:
- 获取 K1:
kms.GetSecret(secretName),若 Secret 为空直接报错拒绝轮换; - K1 写入 slot 1:对每个设备执行
addEncryptionKey(device, currentKey, currentKey, slot1)——用 K1 作为口令、K1 作为新口令写入槽位 1; - 生成 K2 并写入 slot 0:先调用
oposd.GenerateDmCryptKey()生成新密钥(pkg/operator/ceph/cluster/osd/config.go#L61-L68,基于随机字节的 base64 编码);随后对每个设备先removeEncryptionKeySlot(device, currentKey, slot0)清理可能残留的槽位 0,再用addEncryptionKey(device, currentKey, newKey, slot0)将 K2 写入槽位 0; - KMS 更新与校验:
kms.UpdateSecret(secretName, newKey)后将新值写回 KMS,随后kms.GetSecret回读并严格比对——不一致则整体报错("failed to verify the new key in the KMS"),在验证通过之前绝不进入清理旧密钥的阶段; - 移除旧钥:最后对每个设备执行
removeEncryptionKeySlot(device, newKey, slot1),用新钥 K2 作为口令清除槽位 1 中的旧钥 K1,完成轮换。
注意第 5 步使用 K2 作为luksKillSlot的认证口令——因为此时设备已被 K2 接管,只有持有 K2 才能合法清理旧槽位。
底层 LUKS 命令封装:幂等性与失败容忍
轮换逻辑依赖的三个 cryptsetup 命令封装位于 pkg/daemon/ceph/osd/encryption.go,它们体现了极强的幂等设计:
luksAddKey(addEncryptionKey,encryption.go#L234-L283)
- 通过临时口令文件传递
--key-file与--key-slot; - 若槽位为空则直接添加成功;
- 若报错
"Key slot <N> is full",则调用ensureEncryptionKey(内部使用luksChangeKey)探测目标槽位中的口令是否恰好等于待写入的新口令:- 相同 → 幂等返回,无需任何改动;
- 不同 → 先用旧口令
luksKillSlot清空该槽位,再递归调用luksAddKey写入新口令。
luksChangeKey(ensureEncryptionKey,encryption.go#L198-L227)
- 用于探测"目标槽位中是否已存在指定口令";若错误信息包含
"No key available with this passphrase",返回false表示不匹配,而不视为致命错误。这正是重试/续跑场景下的关键容错:CronJob 重跑时,如果槽位状态已经符合预期,不会重复报错。
luksKillSlot(removeEncryptionKeySlot,encryption.go#L170-L193)
- 删除指定槽位;如果错误信息包含
"Keyslot <N> is not active"(槽位本就不存在),同样忽略错误——保证轮换流程可安全重入。
这三层幂等封装与"每步均可中断恢复"的设计目标互为印证:即便某个步骤失败导致 CronJob 在下个周期重试,addEncryptionKey/removeEncryptionKeySlot也能基于当前真实槽位状态继续推进,而不会破坏设备可用性。对应测试见 pkg/daemon/ceph/osd/encryption_test.go。
CephCluster CR 配置:开启 KEK 轮换
设计文档给出了新增的security.keyRotation配置段,对应源码中的KeyRotationSpec(pkg/apis/ceph.rook.io/v1/types.go#L530-L539),包含两个字段:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enabled | bool | false(+kubebuilder:default=false) | 是否启用 KEK 轮换 |
schedule | string | @weekly(代码内默认,见 key_rotation.go#L210-L214) | 轮换调度的 cron 表达式 |
在CephCluster中与既有 KMS 配置组合使用的示例:
apiVersion: ceph.rook.io/v1 kind: CephCluster metadata: name: rook-ceph namespace: rook-ceph spec: security: kms: connectionDetails: KMS_PROVIDER: vault # 或 ibm-kp / kmip / 空(默认 Kubernetes Secrets) # ... 其他 KMS 连接参数,见 # Documentation/Storage-Configuration/Advanced/key-management-system.md tokenSecretName: vault-token keyRotation: enabled: "true" # 设计文档示例中的字符串形式,实际字段为布尔值 schedule: "@weekly"几点说明:
schedule使用标准 cron 格式,也支持 Kubernetes CronJob 的快捷别名(如@weekly、@daily);- 轮换只对加密且由 PVC 承载的 OSD 生效(
reconcileKeyRotationCronJob中通过osdProps.encrypted过滤);非 PVC 或未启用加密的 OSD 不会创建 CronJob; - KMS 的完整配置方式(Vault token、IBM Key Protect 凭据、KMIP 连接等)可参考 key-management-system.md 与 ceph-cluster-crd.md。
如何验证轮换是否生效
- 确认 CronJob 已创建:
kubectl -n rook-ceph get cronjob,应看到形如rook-ceph-osd-key-rotation-0、rook-ceph-osd-key-rotation-1的任务(每个加密 OSD 一个); - 确认调度节点正确:
kubectl -n rook-ceph get pod -l app=rook-ceph-osd-key-rotation -o wide,Pod 所在节点应与对应 OSD 一致; - 查看轮换日志:
kubectl -n rook-ceph logs job/<cronjob 触发的 job>,应依次出现fetching the current key、adding the current key to slot "1"、generating new key、updating the new key in the KMS、Successfully rotated the key等日志; - 关闭轮换:将
security.keyRotation.enabled置为false后,协调器会删除全部轮换 CronJob。
边界情况、限制与安全说明
基于设计文档与源码实现,使用该功能时需要明确以下几点:
- 中断安全性:五步状态机保证任意步骤中断后,KMS 中的 KEK 仍能打开设备(每一步设备内至少保留一个可用密钥);CronJob 的
ForbidConcurrent策略与底层命令的幂等设计共同防止并发与重复执行造成破坏。 - KMS 更新能力差异:当前
UpdateSecret仅对 Kubernetes Secrets 与 Vault 实现;IBM Key Protect、KMIP、Azure Key Vault 的轮换更新会直接报错。设计文档中的方案表述为"需要为每种 KMS 类型增加支持",即该能力仍在演进中,启用前务必确认你的 KMS 类型受支持。 - 不支持按需轮换:非目标明确排除了按需触发;临时需要轮换只能调整 schedule 或等待下个周期(CronJob 也可手动触发,但这不是受支持的正式接口)。
- 特权容器:轮换 Pod 以 privileged + root 运行并挂载宿主机
/dev,这是操作 dm-crypt 设备所必需的,安全边界与 OSD provision 容器一致。 - 默认关闭:
enabled默认false,不会对未显式开启的集群产生额外负载。
总结
KEK 轮换是 Rook 加密存储安全能力的重要一环:通过security.keyRotation声明式配置、按 OSD 粒度生成的 CronJob 调度、LUKS 双槽位缓冲的五步状态机,以及 KMS 的密钥版本更新,Rook 在无需重启 OSD、无停机的前提下实现了加密密钥的周期性更换。设计文档(key-encryption-key-rotation.md)奠定了方案骨架,而 Operator 与 Daemon 层的源码则把"任意步骤可中断、任意状态可重入"的工程细节落到了实处。若要进一步深入,可依次阅读 pkg/operator/ceph/cluster/osd/key_rotation.go、pkg/daemon/ceph/osd/key_rotation.go、pkg/daemon/ceph/osd/kms/kms.go 及 pkg/daemon/ceph/osd/encryption.go 四份核心文件。
【免费下载链接】rookStorage Orchestration for Kubernetes项目地址: https://gitcode.com/gh_mirrors/roo/rook
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考