Files
qinglong/docs/adr/ADR-0127-bounded-postgresql-private-ca-file-binding.md
T

144 lines
6.8 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-0127:有界 PostgreSQL 私有 CA 文件绑定
- 状态:Accepted
- 日期:2026-07-24
- 关联 RFCQL-RFC-0001 D-06、D-34、D-105、D-123、D-124、D-125
- 关联 ADRADR-0042、ADR-0045、ADR-0125、ADR-0126
## 背景
ADR-0126 已提供独立 QL3 Cluster 镜像和 Kubernetes 双副本基线,但
PostgreSQL `verify-full` 只能使用 Node 系统 trust store。多数 Kubernetes
PostgreSQL operator 使用集群私有 CA;把 CA 烘焙进镜像会把证书轮换和应用发布
耦合,把任意文件路径直接交给 `pg` 又缺少大小、类型、权限和证书用途边界。
runtime、worker-ingress 和 migration 使用不同数据库角色与 Secret 域。即使它们
信任同一 operator CA,也不能因此合并 URL、pepper 或 DDL credential。
## 决策
### 1. 在既有 cluster-postgres runtime subpath 提供唯一 CA loader
不新增 package。`loadPostgresCertificateAuthorityFile()` 在打开文件后验证:
- 路径绝对、无控制字符且不超过 4096 字节;
- `fstat` 结果是普通文件;
- group/world write bit 均未设置;
- 文件为 1256 KiB
- 单次有界读取必须与打开时大小一致;
- 内容是严格 UTF-8,且除 PEM certificate block 外没有其他数据;
- 只允许 116 张唯一 X.509 certificate
- 每张证书的 Basic Constraints 必须为 CA。
Kubernetes projected Secret 的外层路径可以是 symlinkloader 验证已打开 target
的真实文件,因此兼容 kubelet 原子 `..data` 切换,又不把目录或 socket 当信任
材料。
输出为规范化 PEM bundle,只存在于当前进程内并直接绑定到 `pg`
`ssl.ca`;文件路径、PEM 和证书 subject 不进入普通进程日志。
### 2. TLS disable 与 CA file 互斥
以下配置分别接入同一 loader
- control runtime`QL3_POSTGRES_TLS_CA_FILE`
- migration`QL3_POSTGRES_TLS_CA_FILE`
- Worker ingress`QL3_WORKER_INGRESS_POSTGRES_TLS_CA_FILE`
只有 `verify-full` 可以使用 CA file。即使设置了 insecure 双门,
`TLS_MODE=disable` 与 CA file 同时存在也必须拒绝,避免部署者误以为连接仍在
校验证书。
CA file 可省略以继续支持系统 trust store,但 QL3 Kubernetes Cluster base 固定
配置私有 CA 路径。
### 3. Runtime 与 migration 保持独立投影
runtime Deployment 只从 `ql3-cluster-control-runtime` Secret 投影
`postgres-ca.crt` 到:
`/var/run/secrets/qinglong3/postgres-runtime/ca.crt`
migration Job 只从 `ql3-cluster-migration` Secret 投影同名 key 到:
`/var/run/secrets/qinglong3/postgres-migration/ca.crt`
两个 volume 都是 read-only、mode 292(八进制 0444),并用 `items` 只投影 CA
key。control Pod 不取得 migration URLmigration Job 不取得 runtime URL 或 API
pepper。
### 4. Trust rotation 以新 activation 生效
CA 在 config 装载时读取一次。kubelet 更新 Secret 后,现有 Pool 不热切换信任
根;必须滚动创建新 Pod,新 activation 重新读取 bundle,再依次通过 TLS、
schema/role readiness、startup recovery 与 lifecycle gate。
这与 ADR-0125 的 one-way availability fence 一致:数据库失联后的旧 activation
不能因文件变化原地恢复 ready。
### 5. 显式 DNS 身份与 old → overlap → new 契约
production `verify-full` 配置必须显式提供 DNS servername;不得使用 IP literal、
省略后依赖 driver 隐式推断,或用 URL `ssl*` 参数覆盖。runtime、migration 与
worker-ingress 三条配置链在创建 Pool 前执行同一 DNS servername 约束。
`audit:postgres-ca-overlap:ql3` 复用上述有界 CA loader 读取 old、overlap 和 new
三个 bundle,并只输出低敏 anchor count 与集合 SHA-256。overlap 必须是 old/new
anchor 的精确并集,不得遗漏旧 anchor 或夹带第三方 anchor;新旧集合必须至少
引入和退役一张证书。部署固定按“扩信任并全量滚动 → operator 轮换 endpoint
certificate → 再滚动新连接 → 收缩信任并全量滚动”的顺序执行,回退则先恢复
overlap trust,再回退 server certificate。
具体命令、停止条件、回退顺序和证据字段冻结在
`deploy/kubernetes/ql3-cluster/operations/postgres-ca-rotation.md`
## 不代表什么
本 ADR 证明 CA 文件到 `pg ssl.ca` 的本地和清单契约,不证明:
- 任一真实 PostgreSQL operator/proxy endpoint 的证书链和 SAN
- 在真实 operator 上执行 CA 重叠轮换、旧 CA 移除和 rollback 演练;
- operator primary Service、promotion 或基础设施 STONITH
- Pod 网络分区、raw PostgreSQL packet loss,或 ADR-0128 远端多架构 release
workflow 的成功记录。
这些仍需使用实际 operator 与集群故障注入验证。
## 替代方案
- **把私有 CA 烘焙进镜像 trust store**:拒绝。轮换需要重建镜像,并扩大所有
Node TLS consumer 的信任面。
- **URL `sslrootcert`/`sslmode` 参数**:拒绝。会绕过统一 TLS 配置和静态审计。
- **把整个 Secret volume 暴露给进程**:拒绝。只投影 CA key,避免无意暴露
URL、pepper 或其他 authority。
- **监听文件并热更新 Pool**:拒绝。增加 watcher 和 trust 切换竞态,且不能为
已建立连接重新完成 readiness/recovery。
- **runtime 与 migration 共用 Secret**:拒绝。信任根相同不等于数据库权限相同。
## 验证
- CA loader 正向覆盖 projected-Secret symlink
- 负向覆盖相对/缺失/目录/可写/超限文件;
- 负向覆盖 trailing data、非 CA、重复和超过 16 张证书;
- runtime、migration、worker-ingress config 验证 exact `tls.ca`
- 三条 `verify-full` 配置链拒绝缺失、IP literal 或非法 DNS servername
- TLS disable + CA file 必须拒绝;
- old/overlap/new auditor 正向验证精确集合并集,负向拒绝遗漏、夹带、无退役和
非规范 fingerprint
- `qinglong/postgresql-tls-rotation@v1` 在本机 arm64 `postgres:18`
PostgreSQL 18.4)建立真实 TLS 1.3 会话:错误 SAN servername 以
`ERR_TLS_CERT_ALTNAME_INVALID` 拒绝;old certificate 下 old-only/overlap
通过,SIGHUP 切换 new certificate 后 old-only 以
`UNABLE_TO_VERIFY_LEAF_SIGNATURE` 拒绝而 new-only/overlap 通过;回退 old
certificate 后 new-only 被拒绝、old-only/overlap 恢复。每个成功连接同时
证明 `pg_stat_ssl.ssl=true``pg_is_in_recovery()=false`
`transaction_read_only=off`
- x64/arm64 原生 PostgreSQL 18 physical-promotion CI job 在 promotion 门前执行
独立 TLS rotation gate
- 部署静态门拒绝错误路径、Secret、mount、mode 或投影 key
- runtime base 通过离线 Kustomize 渲染,`generateName` migration Job 通过严格
YAML/静态契约验证;
- 最小镜像在 UID 10001、只读根、drop ALL、no-new-privileges 下从正式 mount
path 为 runtime/migration 读取同一 CA bundle
- cluster-postgres、cluster-control 全量类型和测试门通过。