确定性密钥派生协议
确定性密钥派生协议的技术细节
确定性密钥派生协议
面向需要理解 etcd-encryption-manager deterministic 模式内部约束、CRD 字段、与 etcd-sync 之间契约的实现方与 Agent。
设计原则
| 原则 | 含义 |
|---|---|
| 不同步密钥本身 | etcd-sync 只同步派生输入(SeedBundle),密钥由 Active 与 Standby 各自派生 |
| 输入完全一致 → 输出完全一致 | HKDF 是确定性算法;只要 root secret + SeedBundle + 字段集合一致,输出 key 一定一致 |
| 角色由 marker 自动判定 | runtime 是 Active 还是 Standby 由 kube-public/etcd-sync ConfigMap 决定,不由人工或 chart 配置 |
| 角色翻转触发重启 | 进程启动时拍下角色,运行中检测到翻转持续 60 秒就主动退出,由控制器重启重新初始化 |
派生算法
输入
| 输入 | 长度 / 类型 | 来源 |
|---|---|---|
rootSecret | 32 字节原始 bytes | kube-system/<rootSecretRef.name> 中 root-key 数据键 |
seed | 32 字节,hex 编码字符串 | EtcdKeySeedBundle.spec.seed |
replicationGroupID | 字符串 | EtcdEncryptionConfig.spec.deterministic.replicationGroupID,与 SeedBundle 中字段一致 |
dimension | 字符串,默认 default | 同上 |
revision | uint64 | EtcdKeySeedBundle.spec.revision |
derivationAlgorithm | hkdf-sha512-v1(默认)或 hkdf-sha256-v1 | 同 SeedBundle |
算法
- 选择 hash:默认 SHA-512;当
derivationAlgorithm=hkdf-sha256-v1时改为 SHA-256 - 拼接 salt 输入:
encodeCtx("etcd-seed-salt/v1", replicationGroupID, dimension, revision, seed),hash 之得到 salt - 拼接 info 输入:
encodeCtx("etcd-encryption-key/v1", replicationGroupID, dimension, revision) - 调用
HKDF(hash, rootSecret, salt, info, 32)派生 32 字节密钥 - 输出 base64 编码字符串
encodeCtx 把每个字段编码为 [uint16(len)][bytes],再按顺序拼接,确保字段边界不可被歧义解析。
派生流水线(输入 / 中间态 / 输出):
输出
| 项目 | 取值 |
|---|---|
| 长度 | 32 字节(256 bit AES-GCM key) |
| 编码 | base64 标准编码字符串 |
| 落地位置 | kube-system/etcd-encryption-key-<revision>,数据键 key |
CRD 与作用域
| CRD | shortName | 作用 | 是否参与 DR 同步 |
|---|---|---|---|
EtcdKeySeedBundle | eksb | 共享种子 + 派生算法 + revision,spec 不可变 | 是,Active 写入,Standby 只读消费 |
EtcdKeySeedBundleState | — | 单集群派生执行状态、错误信息 | 否,每集群本地 |
EtcdEncryptionConfig | eec | 加密类型、rotation、key 策略、deterministic 子配置 | 否,由各自 chart 渲染、保持值一致 |
EtcdKeySeedBundle.spec 设置了 CEL 校验 self == oldSelf,禁止任何字段修改,避免同步过程中出现"先派生再改 spec"的不一致。
Runtime 角色判定
判定规则:
| 信号 | 角色 |
|---|---|
kube-public/etcd-sync ConfigMap 存在 | Standby |
kube-public/etcd-sync ConfigMap 不存在 | Active |
| 启动时拍下角色,运行中翻转持续 60 秒 | 主动 os.Exit,要求被外部重启 |
注意:runtime 监控不参与 leader election;每个副本都自己监控自己的角色,目的是确保没有任何老进程带着过期的角色上下文继续工作。
监控副本的检查时序:
这个 marker 与 active-standby-controller-coordination/controller-integration-handbook.md 中 drsyncgate.IsRunning 看的是同一个 ConfigMap。
etcd-sync 规则边界
| 类别 | 路径 | 规则 data key | 备注 |
|---|---|---|---|
| 同步 | /registry/etcdkeyseedbundles.cluster.alauda.io/ | sync-prefix-file.txt | 共享 SeedBundle |
| 忽略 | /registry/etcdkeyseedbundlestates.cluster.alauda.io/ | ignore-prefix-file.txt | 本地执行状态 |
| 忽略 | /registry/secrets/kube-system/etcd-encryption-key- | ignore-prefix-file.txt | 本地派生出的 key Secret |
| 忽略 | /registry/etcdencryptionconfigs.cluster.alauda.io/default | ignore-equal.txt | 本地配置 |
| 忽略 | /registry/secrets/kube-system/<rootSecretRef.name> | ignore-equal.txt | 由两侧各自分发 |
| 忽略 | /registry/configmaps/kube-public/etcd-sync | ignore-equal.txt | runtime marker,由本地 etcd-sync 维护 |
每条规则对应的 ConfigMap 字段语义见 etcd-sync-dynamic-configmap-migration/provider-spec.md 的"支持的数据键"与"排序模型"。
approvalDelay 时序约束
activationPolicy=timed-approval 时,Active 创建本地 EtcdKeySeedBundleState 后会等待 approvalDelay 再自动批准。批准之后 Active 立刻把本地 EncryptionConfig.keys 从 [old, new] 重排为 [new, old],开始用 key_N 加密;新密文随后通过 etcd-sync 进入 Standby。
安全要求:approvalDelay 必须不小于 Standby 端到端预热时间 T_prep,否则会出现新密文先于新密钥到达 Standby 的解密失败窗口。
T_prep 由四个串联分量组成:
| 分量 | 含义 | 典型量级 | 主要影响因素 |
|---|---|---|---|
T_sync | SeedBundle 通过 etcd-sync 复制到 Standby etcd | 秒级 | etcd-sync 复制策略、跨地域 RTT、目标 etcd 写入延迟 |
T_reconcile | Standby controller 触发、创建本地 BundleState、读 Root Secret、HKDF 派生、写回 EncryptionConfig | 秒级 | controller workqueue、API server 写入延迟 |
T_deploy | 把新 encryption-provider.conf 下发到所有控制平面节点 | 数十秒到数分钟 | 控制平面节点数、节点串/并行下发策略、节点可达性 |
T_reload | kube-apiserver 检测 encryption-provider.conf 变化并加载新 provider | 秒到分钟 | 是否启用 --encryption-provider-config-automatic-reload(K8s 1.22+);未启用时依赖 apiserver Pod 重启 |
只有四步都完成,Standby 才进入 Prepared,本地 EncryptionConfig.keys=[old, new],具备解密 revision=N 密文的能力。
时间线:
失败模式:若 approvalDelay < T_prep,Active 在 Standby 仍处于 Preparing(甚至更早阶段)时已经切主。期间被加密同步过去的 Secret/ConfigMap 在 Standby 上会出现 no matching prefix found 类错误,直到 Standby 的预热链路追平为止。
取值建议:
- 默认
30m面向常见规模(个位数控制平面节点 + 同城跨集群 etcd-sync + 启用 automatic-reload)有较大安全余量 - 控制平面节点较多、跨地域复制、或目标 K8s 版本未启用
automatic-reload时,应按测量到的T_prep上限再加安全余量调大 - 调小到分钟级前必须实测:etcd-sync 抖动峰值、
T_deploy全节点完成的 P99 时间、apiserver reload 收敛时间 - 任何无法以时间窗口稳定覆盖的场景(Standby 节点周期性失联、维护窗口期)应切换为
manual,由运维显式 gating
approvalDelay 不保证的事:它只控制 Active 端的"等待时间",不读取 Standby 的实际预热结果。即使到期后 Active 自动批准,Standby 也可能因为同步阻塞或节点故障而仍未 Prepared。这是 v1 的已知 best-effort 边界。
失败语义
| 失败类型 | 表现 |
|---|---|
| Root Secret 长度不是 32 字节 | 派生函数立即返回错误,加密配置无法 reconcile,EtcdEncryptionConfig.status.deployStatus 进入 UpdateFailed |
| Seed 解码后不是 32 字节 | 同上 |
derivationAlgorithm 不在支持列表 | 同上,错误信息 unsupported derivation algorithm |
| Root Secret 在 Active / Standby 内容不同 | 派生不报错,但两边 key 字节级不同;密文同步过去后 Standby 解密失败 |
replicationGroupID 在两侧不同 | 同上 |
| SeedBundle 没被 etcd-sync 同步过去 | Standby 缺少派生输入,etcd-encryption-key-<revision> 不会生成 |
前三类是部署期硬错误,能在配置阶段暴露;后三类只在密文流转后才暴露,必须在交付前用本主题验证清单中"两边 key 一致"那一条覆盖到。