# ADR-0127:有界 PostgreSQL 私有 CA 文件绑定 - 状态:Accepted - 日期:2026-07-24 - 关联 RFC:QL-RFC-0001 D-06、D-34、D-105、D-123、D-124、D-125 - 关联 ADR:ADR-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 均未设置; - 文件为 1–256 KiB; - 单次有界读取必须与打开时大小一致; - 内容是严格 UTF-8,且除 PEM certificate block 外没有其他数据; - 只允许 1–16 张唯一 X.509 certificate; - 每张证书的 Basic Constraints 必须为 CA。 Kubernetes projected Secret 的外层路径可以是 symlink;loader 验证已打开 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 URL,migration 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 全量类型和测试门通过。