9.0 KiB
ADR-0266:Externally 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 material,caller-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 投影给 Job;Job 没有对 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 裁决:
- 没有 preparation 时创建 preparation;
- 有 preparation、无 completion 时重读/必要时 CAS 同一个 successor;
- Secret 已是 exact successor 时只补 completion;
- preparation 或 Secret 与命令/material proof 不一致时失败关闭;
- 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 token;tokenless init 先证明 exact API allow 与 deny-canary
不可达。RBAC 仅允许对固定目标 Secret get/update 和创建 SelfSubjectAccessReview;staging Secret 不在
API authority 中。base NetworkPolicy 只允许 DNS,Kubernetes 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/aimanifest subpath、@qinglong/cluster-adminone-shot process 和@qinglong/cluster-postgresmaintenance adapter,不新增 workspace package; - workspace 保持 19 个 QL3 package,低配设备不会因 Cluster rotation 文件数增加 importer/SBOM 闭包;
- 每次 ceremony 最多读取一个 32-byte staged material 和一个有界 keyring Secret,无 list/watch/cache。
当前实现与接受门
已完成:
- shared manifest rotation:exact 32-byte、新 identity、generation+1、保留历史 key、exact replay;
- Kubernetes Secret adapter:UID/resourceVersion CAS、并发单赢家、状态缺失的 update-response-loss 重读;
- command-file-only CLI 与 hardened staged-material reader;
- 独立 Job/RBAC/ServiceAccount/NetworkPolicy、命令/material/token 三种分离挂载及 deployment mutation audit;
pg-9016append-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 条件 skip,dependency/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=true;ql3-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-free;staged 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 后仍保留的独立发布门:
- 生产 KMS/HSM/backup/lost-key ceremony 继续作为独立发布门,不由本 ADR 的 Kubernetes Secret 证据替代。
被拒绝方案
- Job 创建目标 Secret:Kubernetes
create无法按resourceNames收紧,会扩大 namespace authority。 - Job 内随机生成 key:响应丢失后无法 exact replay,可能产生两个不同 successor。
- 把 material 放入 command/ConfigMap/PostgreSQL:扩大日志、etcd、备份和审计泄漏面。
- 给 Job staging Secret API read:kubelet 投影已足够,额外 API authority 允许枚举或旁路读取。
- rotation 删除旧 key:会破坏历史 Artifact decrypt;删除只属于独立、已证明零引用的 retirement。
- 自动重试 Job/controller:不确定结果必须由 durable facts 裁决,后台重试会掩盖冲突并扩大常驻面。
- 为 rotation 新拆 package:没有独立制品或依赖反转价值,只会重新引入 D-207 的碎片化。