Files
qinglong/docs/adr/ADR-0266-externally-staged-cluster-prompt-output-active-key-rotation.md

9.0 KiB
Raw Permalink Blame History

ADR-0266Externally staged、resourceVersion-fenced 的 Cluster Prompt Output Active Key Rotation

  • 状态:Accepted
  • 日期:2026-08-03
  • 关联:QL-RFC-0001 D-207/D-244/D-245/D-247、ADR-0261、ADR-0262

上下文

ADR-0262 已让 Cluster 使用固定 Kubernetes Secret 作为 Prompt output keyring material authority 并以一次性 Job 完成 inactive key retirement;后续运行时同源投影也已证明同一 Pod/进程可观察 generation 更新,并继续解密历史 Artifact。尚未关闭的是首次 provision 与 active key rotation。

Kubernetes RBAC 的 create 不能由 resourceNames 限定。若 QingLong 自带 Job 创建目标 Secret 它将取得 namespace 内创建任意同类对象的权限,超出一次 rotation 的最小 authority。另一方面,若 Job 在内存中随机生成新 key 后再替换目标 Secret,更新响应丢失时无法重新构造完全相同的 material; 盲目重试会产生另一个 key,无法判断首次写入是否已成为 durable winner。

因此,本阶段需要把 material custody、目标 Secret CAS 与 PostgreSQL 审计拆成三个明确边界:外部 部署 Secret manager/KMS 负责首次 provision 和 staged materialcaller-driven Job 只消费一次不可变 投影并更新固定目标,PostgreSQL 只保存 content-free prepare/complete facts。

决策

1. 首次 provision 保持外部责任

QingLong 不内置对目标 Secret 的宽泛 create。首次创建 ql3-prompt-output-keyring、encryption-at-rest、备份、KMS wrapping/HSM custody 和 lost-key recovery 继续由部署者选择的 Secret manager/KMS ceremony 负责。首次对象必须满足 ADR-0262 的固定 identity、 canonical manifest、label/annotation 和唯一 data key 约束;rotation Job 只接受预先观察到的 UID, 不能把删除重建后的同名 Secret 当作原对象。

2. 新 material 由外部不可变 staging Secret 投影

每次 rotation 由外部 authority 创建固定名 ql3-prompt-output-key-rotation-material 的 immutable Secret,只含一个原始、恰好 32-byte 的 material.bin。kubelet 将该项以 0440、read-only、单文件 subPath 投影给 JobJob 没有对 staging Secret 的 get/list/watch/create/update/patch/delete API 权限,也不把 material 放进 command、ConfigMap、 环境变量、stdout、日志或 PostgreSQL。

命令只包含 expected target UID、active key ID、catalog digest、新 key ID 和低敏 operation identity。 material reader 使用 O_NOFOLLOW、regular-file、single-link、exact-size、mode 和 pre/post stat 围栏; 读取后复制到受控内存,并在完成或失败时清零。

3. 目标 Secret 使用三重预期与 resourceVersion CAS

rotation 必须同时匹配:

  • 预观察的 Secret UID
  • expected active key ID 与完整 expected catalog digest
  • 当前读取所得 resourceVersion

成功 successor 只追加唯一的新 key、把它设为 active、将 generation 加一,并保留全部旧 key 供历史 Artifact 解密。adapter 只能对固定 ql3-prompt-output-keyring 执行 get/update;不同 UID、active/catalog 漂移、key ID 复用、material 改写、manifest 非 canonical 或并发不同命令都失败关闭。

相同命令和相同 staged material 的重放必须得到完全相同 successor。Kubernetes update 响应丢失或连接 中断时,adapter 重新读取目标:只有 UID、generation、active key、catalog 和 material proof 都与预期 successor 一致时才返回 existing;不能以 HTTP 状态缺失直接宣告失败或盲写第二次。

4. PostgreSQL 采用 content-free prepare/complete protocol

在 target Secret mutation 前,专用 maintenance authority 必须追加 immutable preparation,绑定 rotation/request/mutation identity、expected UID/active/catalog、新 key ID和 staged material proof,但 不得保存 material、base64、Secret body、token、DSN 或文件路径。Secret CAS 成功后再追加 completion 绑定 successor generation/catalog 与 preparation digest。

崩溃恢复由 durable facts 和同一 staged material 裁决:

  1. 没有 preparation 时创建 preparation
  2. 有 preparation、无 completion 时重读/必要时 CAS 同一个 successor
  3. Secret 已是 exact successor 时只补 completion
  4. preparation 或 Secret 与命令/material proof 不一致时失败关闭;
  5. completion 已存在时返回 exact existing,不再更新 Secret。

PostgreSQL append-only protocol、最小权限 ACL、COMMIT-response-loss convergence、physical HA promotion survival 与真实 K3s Job 纵切面均已由下述证据关闭。CLI stdout 始终只能视为低敏操作结果, 不能冒充 durable audit。

5. 一次性权限与网络边界保持显式

operation 使用独立 tokenless ServiceAccount、backoffLimit: 0、短 deadline、只读 root、non-root、 drop ALL。只有主容器取得 600 秒 projected tokentokenless init 先证明 exact API allow 与 deny-canary 不可达。RBAC 仅允许对固定目标 Secret get/update 和创建 SelfSubjectAccessReviewstaging Secret 不在 API authority 中。base NetworkPolicy 只允许 DNSKubernetes API 必须由私有 overlay 以 exact /32 + TCP port 放行;接入 PostgreSQL 后,CloudNativePG overlay 也只能放行数据库 Pods 的 5432。

低配与 package 影响

  • Edge/Standalone 不装配 Kubernetes client、staging Secret、Job、listener、Pool、timer 或 watcher
  • runtime Cluster Pod 仍只读同源投影,没有 ServiceAccount token、Secret API 或 mutation authority
  • 实现留在既有 @qinglong/ai manifest subpath、@qinglong/cluster-admin one-shot process 和 @qinglong/cluster-postgres maintenance adapter,不新增 workspace package
  • workspace 保持 19 个 QL3 package,低配设备不会因 Cluster rotation 文件数增加 importer/SBOM 闭包;
  • 每次 ceremony 最多读取一个 32-byte staged material 和一个有界 keyring Secret,无 list/watch/cache。

当前实现与接受门

已完成:

  • shared manifest rotationexact 32-byte、新 identity、generation+1、保留历史 key、exact replay
  • Kubernetes Secret adapterUID/resourceVersion CAS、并发单赢家、状态缺失的 update-response-loss 重读;
  • command-file-only CLI 与 hardened staged-material reader
  • 独立 Job/RBAC/ServiceAccount/NetworkPolicy、命令/material/token 三种分离挂载及 deployment mutation audit
  • pg-9016 append-only preparation/completion、source CAS fence、content-free digest、专用 maintenance SELECT/INSERT-only ACL、readiness 与 COMMIT-response-loss durable re-read
  • one-shot process 的 Kubernetes authority/readiness → PostgreSQL prepare → Secret CAS → PostgreSQL complete 顺序,以及 prepare 后恢复、Secret update 响应丢失和 completion 响应丢失 exact convergence
  • AI 183 项中 180 pass/3 条件 skip、Cluster Admin 252 项中 250 pass/2 条件 skipdependency/deployment audit 43/43、两套 Kustomize render 与构建全绿;
  • 2026-08-03 PostgreSQL 18.4 arm64 physical HA 门包含本 ADR 的 rotation facts 后重跑全绿: remote_apply、timeline 1→2、一次 Secret durable write、材料 authority 两次调用、response-loss convergence、historical Artifact decrypt、content-free facts、promotion survival、旧主 pg_rewind 只读同步重入与总 gates.passed=trueql3-ha-* 零残留,受保护 evidence control-plane 未被操作。
  • 2026-08-03 真实三节点 K3s/Flannel + CloudNativePG 1.30.0/PostgreSQL 18.4 arm64 纵切面通过: 3/3 database Ready、52/16 migration、两次 one-shot Job 形成 completed/existing、Secret generation 1→2 且 resourceVersion 只改变一次、preparation/completion 各一条并保持 content-freestaged material 为 immutable Secret 的 0440 单文件投影,Job 对它无 API read authority;同一 tokenless runtime Pod 观察新 active key 并用保留的旧 key 解密历史 Artifact。exact RBAC/API/PG/DNS egress、同 Pod deny barrier、600 秒 token 与全部 gate 为 true;随机 K3s 资源和临时镜像零残留,受保护 control-plane 未触碰。

Accepted 后仍保留的独立发布门:

  1. 生产 KMS/HSM/backup/lost-key ceremony 继续作为独立发布门,不由本 ADR 的 Kubernetes Secret 证据替代。

被拒绝方案

  1. Job 创建目标 SecretKubernetes create 无法按 resourceNames 收紧,会扩大 namespace authority。
  2. Job 内随机生成 key:响应丢失后无法 exact replay,可能产生两个不同 successor。
  3. 把 material 放入 command/ConfigMap/PostgreSQL:扩大日志、etcd、备份和审计泄漏面。
  4. 给 Job staging Secret API read:kubelet 投影已足够,额外 API authority 允许枚举或旁路读取。
  5. rotation 删除旧 key:会破坏历史 Artifact decrypt;删除只属于独立、已证明零引用的 retirement。
  6. 自动重试 Job/controller:不确定结果必须由 durable facts 裁决,后台重试会掩盖冲突并扩大常驻面。
  7. 为 rotation 新拆 package:没有独立制品或依赖反转价值,只会重新引入 D-207 的碎片化。