Files
qinglong/docs/adr/ADR-0494-postgresql-secret-binding-and-mounted-provider-live-rotation.md
T

131 lines
7.0 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-0494PostgreSQL Secret Binding 与 Mounted Provider 在线轮换门
- 状态:Accepted
- 日期:2026-08-24
- 决策:D-399
- 关联:ADR-0129、ADR-0141、ADR-0233、ADR-0491、ADR-0492、ADR-0493
## 背景
ADR-0233 已提供 production `mounted-files` Secret provider,但原有证明主要来自
单元测试、静态部署审计和受限容器资源门。D-397 又要求 Cluster Secret migration
不能借用 Local SQLite/POSIX authority,必须证明 PostgreSQL durable approval、精确
Secret 投影和多节点运行时在同一真实 Kubernetes 门中仍然失败关闭。
旧的 Secret binding live contract 还落后于当前 executor basebase 已支持创建
action-scoped Job,而兼容门仍假设外层 executor 直接挂载值;同时成功 Pod 的 kubelet
logs 偶发 `EOF` 会让已经完成的证据在报告阶段丢失。门禁不能通过增加 Secret API
权限、ServiceAccount token、watcher、sidecar 或把值写入报告来规避这些问题。
## 决策
### 1. 复用现有生产边界,不增加新的运行时闭包
本门直接构建正式 `ql3-cluster-admin``ql3-cluster-control` 镜像,provider 使用
ADR-0233 的 `createClusterMountedSecretProvider`。不新增 workspace package、生产依赖、
PostgreSQL migration/table/role、daemon、timer、watcher、listener、sidecar 或 Secret
API clientEdge/Standalone import graph 不加载本门或 Cluster 依赖。
兼容 Secret binding executor 在 fixture 中显式进入 direct exact-key 模式:关闭
action controller、禁用 ServiceAccount token,只挂载当前批准计划所需的一个
projection key,并设置 `QL3_PLUGIN_PACKAGE_EXECUTOR_SECRET_ROOT`。这证明既有
PostgreSQL approval/binding 语义和 direct provider 消费,不改写 production base 的
action-scoped controller 决策;controller 的 digest-pinned Job/admission contract 继续由
其独立测试与部署审计证明。
### 2. 三节点拓扑与 durable approval 必须同时成立
live fixture 启动一台 K3s server 和两台 agent。两个 management Pod 使用
anti-affinity 分布到不同节点,通过正式 mTLS client 完成 plan、跨副本 replay、propose、
四眼 decide 和 inspect。PostgreSQL 18.4 保存 approval、execution 和 binding;数据库中
只能出现 SecretRef/plan/evidence digest,不能出现 Secret material。最小权限 manager
不能读取 binding 表,management Pod 不挂载业务 Secret。
外层 executor 只读挂载 exact-key `0440` projection,不能 get/list Kubernetes Secret
且没有 ServiceAccount token。它必须发布一次 binding、消费一次 approval 并成功完成;
任一持久化 fence 或 projection 漂移都失败关闭。
### 3. 两个 provider observer 必须无重启观察原子轮换
另建与 approval 中相同 SecretRef/projection key 对应的可变 Kubernetes Secret,并启动
两个 observer Job。required pod anti-affinity 要求它们位于不同节点;二者都运行正式
Cluster provider、非 root、只读 root filesystem、drop ALL capability,并满足:
- `automountServiceAccountToken=false`
- ServiceAccount 对 Secret `get/list/patch` 均为 `no`
- deny-all NetworkPolicy,无 ingress/egress
- projection read-only、`defaultMode=0440`,只包含一个精确 hash key
- 第一代 material 被观察后才以 resourceVersion-fenced `replace` 写入第二代;
- 两个 Pod 都在不重启的情况下观察到第二代;
- stdout、termination message 和最终报告不含两代值或 SecretRef。
随后删除 Secret,并以 `optional: true` 空 projection 启动一次性 observer;生产 provider
必须返回 `QL3_CLUSTER_MOUNTED_SECRET_UNAVAILABLE`。这里的 optional 只允许 Pod 启动,
不允许 material resolve 降级成功。
### 4. 证据不依赖 kubelet 日志可用性
observer 只输出 content-free JSON,并同时写入 `/dev/termination-log`。审计优先读取
PodStatus 中的终止消息,只有没有该消息时才回退 kubelet logs。因此已完成 Job 的证据
不会因节点日志通道 `EOF` 丢失,也不需要扩大 Kubernetes API/RBAC。
私有报告使用 `qinglong/plugin-package-secret-binding-kubernetes-live@v2`,以 owner-only
`0600` 原子发布。v2 在原 v1 字段上增加 control image、provider 拓扑、轮换、RBAC、
投影模式、脱敏和 missing fail-closed 证据;离线 verifier 继续接受 immutable v1 shape
但绝不允许 v1 报告伪装成 v2 provider 证明。
## 被拒绝的替代方案
### 给 control Pod 增加 Secret API 权限
拒绝。轮换由 Kubernetes atomic writer 投影完成;get/list/watch 会扩大 credential、网络、
缓存和审计面。
### 用 `disk-pressure` toleration 或降低 kubelet eviction 阈值通过门禁
拒绝。这会掩盖真实资源不足。本机运行先清理明确未使用且可重建的镜像/缓存,并在用户
授权后只回收未被容器引用的匿名卷,再从健康磁盘启动全新集群。
### 只验证单副本或重启后读取新值
拒绝。单副本不能证明 topology separation;重启后读取只能证明重新挂载,不能证明
atomic writer rotation 被现有 provider 请求观察。
### 把 Secret 值或 SecretRef 写入报告便于排障
拒绝。报告只保留 digest、计数、布尔值、错误码和节点名哈希。排障不得扩大 material
custody。
## 当前验证
2026-08-24 本机 Apple Silicon 完整 live gate 已通过:
- K3s `v1.34.3+k3s1`3 个 Ready 节点;
- PostgreSQL `server_version_num=180004`
- 2 个 management replica 位于不同节点;
- direct exact-key executor Job 成功,binding exactly once,数据库 material match 为 0
- 2 个 provider observer 位于不同节点,第一代与轮换代均为 `2/2`
- resourceVersion 前进,Pod 未重启,Secret API 三个权限均为 false
- read-only `0440`、无 token、deny-all network,输出脱敏;
- Secret 删除后以 `QL3_CLUSTER_MOUNTED_SECRET_UNAVAILABLE` 失败关闭;
- v2 报告 24/24 gates 为 true,离线审计 findings 为空,文件权限 `0600`
- v1 verifier 兼容与 v2 drift/sensitive rejection 定向测试通过。
CI 新增独立 `cluster-secret-binding-mounted-provider-kubernetes-live` Job,固定 K3s、
PostgreSQL 和 kubectl 版本,运行完整 live gate、离线复核并上传低敏报告。
## 边界与后续门禁
本 ADR 关闭 ADR-0491 的 Cluster mounted-files provider live 子门,但不把 D-397 整体
转为 Accepted,也不声明:
1. Kubernetes control-plane HA;本 fixture 是单 server + 双 agent
2. PostgreSQL 物理 failover;它由独立 125-gate HA contract 证明;
3. 直接 Vault/KMS/HSM adapter、CSI/Vault Agent 自身故障或 credential rotation
4. Cluster Legacy Env migration 的专用 SERIALIZABLE ledger、Task/Trigger mutation 和
promotion 后 receipt replay
5. 固定低性能路由设备上的真实空间、写放大、断电与恢复证据。
因此 ADR-0491 仍保持 Proposed。下一步优先完成固定 Edge 硬件空间门,随后实现并证明
Cluster migration ledger;直接外部 custody adapter 作为可选、独立供应链继续设计。