Files
qinglong/docs/adr/ADR-0262-kubernetes-secret-backed-cluster-prompt-output-key-retirement.md
T

135 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-0262Kubernetes Secret-backed Cluster Prompt 输出 Key Retirement
- 状态:Accepted
- 日期:2026-08-02
- 接受日期:2026-08-03
- 关联:QL-RFC-0001 D-207/D-244/D-245、ADR-0233、ADR-0261
## 上下文
ADR-0261 已实现 Prompt output Artifact 的双方言加密、retention GC 和两阶段 key
retirement,但 Cluster 只留下可注入的 material authority。把本机 POSIX file-keyring
直接放到共享卷会失去多副本 CAS、对象 identity 和 Kubernetes 权限边界;反过来,立即把某个
云厂商 KMS SDK、Vault client 或 HSM driver 放进常驻 control plane,又会把凭据、重依赖和网络
故障域扩散到每个 Cluster Pod。
本阶段需要一个可部署、可审计且不冒充最终 KMS/HSM 的中间实现:使用部署者专有、可变的
Kubernetes `Opaque` Secret 保存 canonical keyring manifest,只让一次性 maintenance Job 对
该单个对象执行退役 CAS。Secret 的首次创建、active rotation、外部加密/备份和运行时装配仍是
部署产品责任,不能由 migration 或 control plane 隐式生成。
## 决策
### 1. Secret 是单对象 material authority,不是数据库副本
Cluster retirement adapter 固定 namespace、Secret name、预先观察到的 Secret UID 和唯一 data key。
它只接受:
- `type: Opaque``immutable: false`
- 恰好一个 data entry,内容是 ADR-0261 的 canonical bounded keyring manifest
- 固定 managed label,以及与 manifest 一致的 generation/catalog-digest annotations
- 非删除状态、相同 UID、合法 `resourceVersion`,且没有 `stringData` 或会复制密钥正文的
last-applied annotation。
adapter 只调用 `readNamespacedSecret``replaceNamespacedSecret`。退役在内存中删除 inactive key
material、保留 content-free retirement receipt,再用当前 `resourceVersion` 做 CAS replace。409、响应
丢失和并发相同命令必须重读:只有 exact retirement 已成为 durable winner 才返回成功;UID 重建、
active key、proof/catalog 漂移或不同命令竞争全部失败关闭。禁止 list/watch/create/delete/patch、缓存、
timer 和后台 controller。
### 2. 数据库事实与 Secret CAS 由一次性 CLI 编排
既有 Cluster retirement process 继续先使用 `ql3_ai_maintenance` 在 PostgreSQL 中提交 preparation
然后调用 material authority,最后追加 completion。新 `ql3-prompt-output-key-retire` 只接受绝对路径
command file,命令 exact 绑定 schema、operation、namespace、Secret name、expected UID、data key 和
key/retirement/request/mutation identity。stdout 只允许低敏状态和 digest,不返回 Secret、key、DSN、
token 或文件路径。
CLI 通过 in-cluster `KubeConfig` 创建官方 Kubernetes client,并在任何 material 读取前执行完整
`SelfSubjectAccessReview` 矩阵:必须允许对 exact Secret 的 get/update,同时明确拒绝 list/watch/
create/delete/patch、其他 Secret、ConfigMap 和 Pod。审查不一致时不打开 PostgreSQL retirement
authority。CLI 退出时清除 KubeConfig 中保留的 token/context 引用。
### 3. 部署保持 opt-in、短生命周期和默认断网
operation 使用独立 ServiceAccount、精确 `resourceNames` Role、SelfSubjectAccessReview-only
ClusterRole、`backoffLimit: 0`、300 秒 deadline、只读 root、非 root UID/GID 10001、全部 capability
drop 和 128 MiB memory limit。命令 ConfigMap immutable,作为单个 `subPath` 文件只读挂载,避免
Kubernetes atomic-writer symlink 与 CLI `O_NOFOLLOW` 冲突。
ServiceAccount 与 Pod 均关闭自动 token。无 volume mount 的同 Pod init container 必须先同时证明
Kubernetes API 可达、一个已由独立无策略 control Job 证明可达的 deny-canary 不可达,才允许主容器
启动;只有主容器挂载显式 projected ServiceAccount token/CA/namespacetoken 有效期固定 600 秒。
这关闭了新 Pod 创建时 CNI policy 尚未收敛、自动 token 已提前可读的窗口。
base NetworkPolicy 只允许集群 DNSCloudNativePG overlay 再只加入 exact PostgreSQL Pods 的 TCP
5432。Kubernetes API server 地址与端口依部署而异,因此仓库只提交 `/32 + TCP port` JSON patch
模板,不提供 `0.0.0.0/0`、namespace-wide 或默认可用的 API egress。部署者必须在私有 overlay 中
填入实际 control-plane endpoint。该 operation 不进入默认 `operations/kustomization.yaml`,每次命令
由 operator 显式创建,完成后不自动重试或轮询。
### 4. 不把 Secret adapter 宣称为最终 KMS/HSM
Kubernetes Secret 只关闭“Cluster 可执行 retirement CAS”这一层。要成为完整生产 material plane
运行时 Prompt application 必须从同一个 Secret 的只读投影或同一外部 authority 获取 active/history
key;首次 provision、active rotation、备份/恢复、encryption-at-rest、KMS wrapping/HSM non-exportable
语义、lost-key 演练和审计告警必须另行交付。当前常驻 cluster-control 不获得 Secret API token
retirement Job 也不获得 create/delete 或任意 Secret 权限。
## 低配与 Cluster 影响
- Edge/Standalone 继续使用 POSIX file-keyring,零新增依赖、进程、连接或 timer;
- workspace 保持 19 个 QL3 package;实现位于既有 `@qinglong/ai` manifest subpath 和
`@qinglong/cluster-admin` adapter/CLI subpath
- `@kubernetes/client-node` 只在既有 Cluster Admin image/一次性 Job 中加载,control runtime 和本机
Profile 闭包不增加该依赖;
- 每次退役最多读取/替换一个不超过 256 KiB 的 Secret,最多 16 个 key、64 个 retirement receipt
无 list/watch 和随集群规模增长的内存集合。
## 验证
- Kubernetes Secret adapter 3/3:正常退役/exact replay、响应丢失与并发 exact convergence、active/
UID 重建/非 canonical authority 拒绝;
- CLI/process 定向 3/3,并验证命令行只接受 command-file、widened shape 和 secret-bearing failure
失败关闭;
- Cluster Admin 整包 206 项中 204 通过、2 项外部集成条件跳过;
- Cluster deployment audit 39/39,包含 RBAC 扩权、去除单文件 `subPath`、公共 egress 三项 mutation
- base 与 CloudNativePG Kustomize 均可渲染,dependency/deployment audit 均
`findings=[]``compatible=true`
- PostgreSQL 18.4 arm64 physical HA 重新完成 `remote_apply`、timeline 1→2、旧主 fencing、
`pg_rewind` 只读同步重入、双 fresh controlPrompt output key retirement 的 durable/fenced gate 与
`gates.passed=true``ql3-ha-*` 容器、volume、network 零残留。
- 2026-08-03 的显式 opt-in 实机门使用 3 个 privileged K3s Docker 节点、Flannel、CloudNativePG
1.30.0 与 PostgreSQL 18.4 arm643/3 database instance Ready,核心/AI migration 为 52/15。
两次真实 Job 分别由持久状态判定为 `completed`/`existing`Secret identity 保持不变、generation
1→2、inactive key 删除、active key 保留、`resourceVersion` 只改变一次,preparation/completion
各 1 条。exact RBAC、同 Pod allow+deny 网络屏障、init 无 token、主容器 600 秒 projected token、
TLS 与 content-free report 全部通过,`gates.passed=true`;随机 Docker/K3s 资源零残留,既有
`ql3-cnpg-evidence-control-plane` 未被操作。
该实机门使用 dynamic local-path volume 和单 control-planedeny-canary 是集群内确定性 fixture;它
不等同于生产 CSI/基础设施/control-plane HA。Kubernetes Secret `resourceVersion` CAS 也不等同于
KMS wrapping、HSM non-exportability 或外部 key custody。运行时同源读取、首次 provision/active
rotation、外部 KMS/HSM、备份恢复与 lost-key recovery 仍是后续独立发布门,但不再阻止本 ADR 的
“真实 Kubernetes 一次性 Secret retirement CAS”边界被接受。
## 2026-08-03 后续证据修正
本 ADR 接受后,运行时同源读取已由独立、默认关闭的 `cluster-ai-prompt-output` Component 闭环。
它只把同一个 `ql3-prompt-output-keyring/keyring.json` 以 required、`0440`、read-only Secret volume
投影给既有 Cluster AI projected-keyring adapter;默认 Cluster/`cluster-ai` 仍为 live-onlyPod 无
ServiceAccount token、RBAC 或 Secret mutation authority。三节点 K3s v1.34.3 arm64 实跑已证明同一
Pod/进程观察 generation 1→2、真实 atomic-writer symlink、新 active key 生效和历史 encrypted
Artifact 解密;轮换窗口竞态失败关闭,稳定新代可重新读取。首次 provision/active rotation 的受审
管理面、KMS/HSM、备份恢复与 lost-key recovery 仍为后续发布门。本节只修正后续实现状态,不改写
本 ADR 当时接受的 retirement 边界。
## 被拒绝方案
1. **Cluster 复用 POSIX 共享卷 keyring**:没有对象 UID/resourceVersion CAS,节点与权限边界错误。
2. **control plane 直接取得 Secret update**:把短生命周期 destructive authority 提升为常驻网络面。
3. **Role 允许 create/delete/list/watch/patch**:退役只需 exact get/update,其他 verb 均扩大 blast radius。
4. **Job 自动重试或常驻 controller**:不确定响应必须由 durable facts 裁决,盲重试会掩盖冲突。
5. **提交公共 API egress**Kubernetes API endpoint 是部署事实,仓库不能以公网/全网规则代替。
6. **把 Kubernetes Secret 称作 KMS/HSM**:缺少 wrapping、non-exportable key、外部审计和灾难恢复语义。