“生成新值、更新 Secret、重启服务”看似完成了轮换,实际可能同时击穿数据库连接池、让旧实例无法认证,并使仍在流通的令牌突然失效。凭据的生产者、消费者、验证端和缓存不会在同一时刻切换;把一个值原地覆盖,只是把分布式迁移伪装成配置修改。

无停机轮换需要一个可回退的协议:新旧版本短暂共存,新写入只使用新版本,读取或验证在有界窗口内兼容旧版本,确认旧版本不再使用后才撤销。

先建立清单和版本身份

每个 secret 至少要记录用途、负责人、发行方、消费者、环境、权限范围、创建与到期时间、轮换方式和紧急联系人。

消费者应引用逻辑名称和明确版本,例如 payments/db-password 的 v42,而不是可被静默覆盖的“current”。日志只记录版本 ID 和结果,绝不记录 secret、连接串或可逆摘要。

使用 expand、migrate、contract

计划轮换可以拆成四个阶段:

  1. 准备:生成最小权限的新版本,验证目标身份、格式和回滚路径,但尚不用于生产流量。
  2. 扩展:认证端或验证端同时接受新旧版本;所有消费者获得读取新版本的权限。
  3. 迁移:生产者只用新版本签名、加密或建立新连接;旧版本仅用于验证历史数据和排空存量连接。
  4. 收缩:观察跨过最长令牌、缓存、任务和连接生命周期,确认旧版本使用量为零,再撤销并验证拒绝行为。

每一步都要幂等并持久化状态,不能依赖上一次调用留在内存中的变量。失败时只回退活动版本。

数据库凭据要切连接池

直接修改同一数据库用户的密码,会让已经更新和尚未更新的实例互相竞争:旧连接可能暂时可用,但一旦重连就失败。更安全的做法是建立第二个用户,复制并核对最小权限,部署新凭据,让新连接进入新池,再排空旧池并撤销旧用户。

应用热加载时先完整解析和试连新配置,再原子替换活动客户端引用。旧客户端只为已开始的请求保留,并受排空 deadline 限制。若试连失败,继续使用旧版本并告警,不能把无效值扩散到所有实例。

签名与验证必须分开迁移

签名方只能用一个活动私钥产生新签名,验证方却需要在过渡期持有多个公钥。令牌或消息携带 key ID,验证方据此选择密钥;RFC 7517 的 kid 就用于在 JWK Set 中匹配轮换中的具体密钥。

顺序应是:先发布新公钥并确认所有验证方可见,再让签名方切到新私钥;等待旧令牌的最长期限、时钟偏差和队列延迟过去后,才移除旧公钥。接受多个 key ID 不等于接受任意算法,算法、发行方、受众和用途仍需固定校验。

加密密钥轮换不是只影响新数据

密文要保存密钥版本。轮换后,新写入使用活动 key,读取端暂时保留旧 key 集合,再通过后台任务重包裹数据密钥或重加密历史数据。任务必须可恢复、限速,并记录每个版本仍保护的数据量。

覆盖率为零且备份、归档已处理旧密文后,才能销毁旧解密材料。“不再用于加密”并不等于“立即不能解密”;envelope encryption 可避免重写全部业务数据。

配置分发不等于应用已切换

Kubernetes Secret 注入为环境变量后,运行中的容器不会看到更新,必须重启。挂载为 volume 的内容会以最终一致方式更新,但 subPath 挂载不会自动更新;应用仍需监听或定期重读,并在内存中原子切换完整配置。看到文件变化不等于新凭据已经可用,必须执行格式、权限和实际认证检查。

动态凭据可缩小泄漏窗口,但消费者要在 lease 到期前带抖动续租。刷新应 single-flight;失败时只能在剩余有效期内服务并告警。

撤销需要证据

看板按版本展示签发、读取、认证结果、最后使用时间和旧版本命中,不展示 secret。撤销前检查消费者、活动连接、令牌期限、积压任务和历史密文覆盖率;撤销后确认旧值被拒绝且新值仍可用。

计划轮换允许短暂重叠;疑似泄漏是事件响应,可能必须立即撤销旧值并接受部分请求失败。预案要能缩小权限、禁用单个版本、识别受影响调用方并补发安全凭据,而不是把紧急轮换当成更快的常规脚本。

测试应覆盖新版本无效、部分实例未刷新、连接池排空超时、验证方缓存旧 JWK Set、重复与乱序事件、Secret volume 延迟、动态 lease 到期、历史密文混用多个版本,以及轮换中途回滚。上线前还要演练旧版本撤销后的错误是否清晰且不会泄露敏感信息。

安全轮换的完成条件不是“新值已生成”,而是新版本已被所有目标路径使用,旧版本的残余依赖可被量化,并且撤销已经验证。把轮换当成有状态迁移,才能同时缩短凭据暴露期和避免发布事故。