mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
210 lines
11 KiB
Markdown
210 lines
11 KiB
Markdown
# ADR-0148:Plugin Package 管理 Live Evidence 与默认拒绝 Egress
|
||
|
||
- 状态:Accepted(证据协议、采集器、审计器、默认拒绝 egress 与静态门已实现;真实报告待采集)
|
||
- 日期:2026-07-25
|
||
- 关联:ADR-0144 至 ADR-0147、QL-RFC-0001 D-142/D-143
|
||
|
||
## 背景
|
||
|
||
管理入口已经具备 TLS 1.3、外部强身份验证、双 User separation-of-duty、双副本清单、
|
||
durable quota 和全副本重启 keyset ledger,但这些单元/静态/本机 HA 证据不能替代:
|
||
|
||
- 真实外部 IdP 的两名不同 User;
|
||
- 三控制面、至少两工作节点的真实 Kubernetes;
|
||
- CNI 实际执行的 ingress/egress NetworkPolicy;
|
||
- 双 Pod 零不可用 keyset/TLS rotation;
|
||
- manager role 对 Kubernetes Secret、executor mutation 和公网 egress 的真实否定。
|
||
|
||
CI 自签两枚 JWT 只能验证协议路径,不能被记录为真实 IdP 证据。仅看到两个 Ready Pod
|
||
也不能证明请求、轮换或网络隔离。
|
||
|
||
## 决策
|
||
|
||
### 1. 版本化低敏证据
|
||
|
||
新增 `qinglong/plugin-package-management-live-evidence@v1` 和
|
||
`ql3-plugin-package-management-live-evidence-audit.cjs`。报告最大 1 MiB,必须为绝对
|
||
路径上的 non-symlink regular file,且不可 group/world writable。
|
||
|
||
报告精确绑定:
|
||
|
||
- Kubernetes 精确 `gitVersion`(至少 1.32,允许受限的 managed-distribution
|
||
suffix)、架构、CNI 名称/版本、三控制面和至少两工作节点;
|
||
- management/PostgreSQL 实际 image ID;
|
||
- 两个 Ready management replica、两个不同 Pod/Node identity 摘要、零 token mount;
|
||
- `ql3_package_manager`、25 migration、capability v24、38 表;
|
||
- 非本机、非 IP、非保留测试域名的 canonical HTTPS OIDC issuer;
|
||
- discovery/JWKS 摘要、两名不同 User subject 摘要和 MFA/hardware assurance;
|
||
- 三个严格递增 keyset generation、最终 durable ledger generation 与 revoked 数量;
|
||
- propose、自批拒绝、另一 User approve、authorized inspect 和 durable Audit;
|
||
- labelled/unlabelled ingress、错误端口、Kubernetes API/公网/PostgreSQL egress;
|
||
- Secret read 与 executor mutation 权限否定;
|
||
- overlap、新 key、旧 key revoke、TLS serial/Secret generation 变化和 TLS 1.3;
|
||
- 九个独立 summary gate 与总 `passed`。
|
||
|
||
报告禁止出现 assertion、Authorization、bearer/token、password、Secret、DSN、
|
||
connection string、private key 或 JWT-like material。这个审计器验证报告契约,不凭空
|
||
生成事实;没有真实采集报告时,生产 gate 仍未通过。
|
||
|
||
### 2. 拒绝测试身份冒充生产证据
|
||
|
||
issuer 必须是 canonical HTTPS URL,并拒绝:
|
||
|
||
- localhost、`.localhost`、`.local`;
|
||
- `.test`、`.invalid`、`.example`;
|
||
- 任意 IP literal;
|
||
- userinfo、query、fragment 或非规范 URL。
|
||
|
||
两名 User 的 subject 只记录 SHA-256 摘要但必须不同,assurance 只能是
|
||
`multi_factor` 或 `hardware`。CI fixture 可以继续用于回归,但不能通过 live evidence
|
||
审计。
|
||
|
||
### 3. 默认拒绝管理 Pod egress
|
||
|
||
基础 management NetworkPolicy 现在同时声明 `Ingress` 与 `Egress`:
|
||
|
||
- ingress 只接受同命名空间带
|
||
`qinglong.io/plugin-package-management-client=true` 的客户端访问 TCP 8443;
|
||
- base egress 只允许 kube-system `kube-dns` 的 UDP/TCP 53;
|
||
- CloudNativePG overlay 只额外允许同命名空间
|
||
`cnpg.io/cluster=ql3-postgres` 的 TCP 5432;
|
||
- 外部 PostgreSQL 或不同 DNS 实现必须在私有 overlay 精确声明目标,不能回退为 `{}`、
|
||
namespace-wide 或公网 egress。
|
||
|
||
管理 Pod 不需要访问 IdP discovery/JWKS;受审 keyset 仍由独立部署 authority 投影,
|
||
避免把网络发现、watcher 或新 credential 放进公网 parser 进程。
|
||
|
||
### 4. 采集事实,而不是接受最终布尔值
|
||
|
||
新增 `ql3-plugin-package-management-live-evidence-collect.cjs`,但不新增 workspace
|
||
package、常驻进程或第三方依赖。它只由集群 operator 显式运行,edge/standalone
|
||
制品与运行时不导入。
|
||
|
||
采集器拒绝输入 `gates`、`passed` 等最终判断。它直接:
|
||
|
||
- 从显式 kubeconfig/context 查询 Kubernetes server、Ready/schedulable Node、
|
||
management Deployment/Service/ServiceAccount/Pod、CloudNativePG Pod、CNI DaemonSet
|
||
和实际 container image ID;
|
||
- 以 `kubectl auth can-i --as=system:serviceaccount:...` 验证 manager 不能读取 Secret,
|
||
不能 create Job、patch Deployment 或 update ConfigMap;
|
||
- 精确比较已部署的 CloudNativePG management NetworkPolicy,空 selector、额外 peer/
|
||
port、`ipBlock` 或其他宽化均拒绝;
|
||
- 仅把 `service=<name>` 交给 `psql`,通过 mode 0600 的 `PGSERVICEFILE` 以
|
||
`ql3_package_manager` 查询 PostgreSQL 18.4、25 migration、capability v24、38 表、
|
||
三条指定 durable Audit 和最终 keyset ledger;
|
||
- 从外部 HTTPS issuer 获取 discovery/JWKS,禁止 redirect,限制每份 1 MiB,验证
|
||
discovery issuer 并对原始响应计算 SHA-256;
|
||
- 将私有 exercise 中的 User subject、TLS serial 和 Secret resourceVersion 加域后
|
||
摘要化,不把原值、三个 Audit UUID 或 probe transcript 写入最终报告。
|
||
|
||
标准 API 无法重放的真实 HTTP 四眼、CNI 数据面探针和三阶段轮换仍由私有 exercise
|
||
记录原始 HTTP status/operation/error、probe outcome 与三份 readiness sample。该文件
|
||
必须是 canonical、non-symlink、最大 1 MiB、mode 0600,24 小时后过期,并且不得包含
|
||
assertion、credential 或预计算 gate。采集器从这些原始字段派生最终 boolean;这提高
|
||
了可复核性,但不能阻止拥有主机权限的人伪造输入,因此真实运行日志和变更记录仍属于
|
||
发布审计链。
|
||
|
||
### 5. 三阶段真实 exercise runner
|
||
|
||
新增 caller-driven `ql3-plugin-package-management-live-exercise.cjs`,将 raw exercise
|
||
拆为 `before → overlap → revoked → finalize`,不新增 workspace package、第三方依赖
|
||
或常驻进程:
|
||
|
||
- `before` 真实执行 requester propose、自批 403、reviewer approve/inspect,并用
|
||
tokenless、non-root、无 Secret 的临时 Pod 验证 labelled/unlabelled/错误端口 ingress;
|
||
两个 management Pod 内分别验证 Kubernetes API/公网 deny 与 PostgreSQL allow;
|
||
- operator 随后必须人工把 keyset 切到 old/new overlap、增加 generation,并轮换 TLS
|
||
Secret 后以零不可用 rollout 进入 `overlap`;同一 old/new assertion 都必须返回 200;
|
||
- operator 再显式 revoke old kid、增加 generation且保持 overlap TLS,进入
|
||
`revoked`;runner 要求 overlap 中完全相同的 old/new assertion,旧 assertion 在仍未
|
||
过期时返回 401,新 assertion 继续返回 200;
|
||
- 每阶段写入一个新的 mode 0600、不可覆盖状态文件。状态只保存 assertion SHA-256,
|
||
不保存 assertion 本身,并以 `previousStateSha256` 形成顺序链;该摘要链只证明连续
|
||
性,不是主机签名,拥有主机写权限者仍可重算,因此 operator 日志和发布变更记录仍是
|
||
信任边界;
|
||
- `finalize` 只接受三阶段严格递增时间/generation、三份双副本 TLS 1.3 readiness
|
||
sample 和五分钟内的 revoked 状态,再生成 collector 接受的私有 raw exercise。
|
||
|
||
runner 会写入一条专用 Package proposal/approval/Audit,但不会 consume、dispatch、
|
||
下载或激活 Package。`action-input` 必须是经过复核、无生产副作用的专用证据提案。
|
||
所有 assertion 文件、action input、kubeconfig 和阶段状态都必须是 canonical mode
|
||
0600 文件;临时探测 Pod 删除失败会使该阶段失败。
|
||
|
||
## 验证
|
||
|
||
- live evidence audit:5/5;
|
||
- live evidence collector:7/7,覆盖真实对象派生、空 selector/策略宽化、manager-only
|
||
SQL、OIDC 原始摘要与 SSRF/流式大小边界、低敏输出与私有文件边界;
|
||
- live exercise runner:6/6,覆盖强身份派生、私钥拒绝、三阶段摘要链、同 assertion
|
||
撤销、篡改/跳阶段拒绝、tokenless/non-root/有界 probe Pod 与仅清理本轮成功创建的
|
||
Pod;
|
||
- 上述 collector/audit 在只读、无网络的 `node:24-bookworm-slim` 容器中合并复验
|
||
12/12;
|
||
- cluster deployment audit:`findings=[]`;
|
||
- deployment audit tests:新增公网 egress 和宽化 PostgreSQL selector 负向门;
|
||
- management base/CloudNativePG Kustomize 均可 render;
|
||
- 未新增 workspace package、第三方依赖、timer、watcher 或常驻进程。
|
||
|
||
## 尚未完成
|
||
|
||
必须由真实环境采集并通过:
|
||
|
||
```bash
|
||
export QL3_PLUGIN_PACKAGE_MANAGEMENT_LIVE_EXERCISE=1
|
||
|
||
pnpm evidence:plugin-package-management-live-exercise:ql3 -- before \
|
||
--kubeconfig=/absolute/private/kubeconfig \
|
||
--context=production \
|
||
--endpoint=https://management.example.org/api/v3/plugin-packages/management \
|
||
--ca-file=/absolute/private/management-ca.crt \
|
||
--requester-assertion=/absolute/private/requester.jwt \
|
||
--reviewer-assertion=/absolute/private/reviewer.jwt \
|
||
--action-input=/absolute/private/evidence-action.json \
|
||
--output-state=/absolute/private/management-before.json
|
||
|
||
# operator: old/new key overlap + generation increase + TLS Secret rotation +
|
||
# zero-unavailable rollout; old.jwt/new.jwt must remain valid through revoked.
|
||
pnpm evidence:plugin-package-management-live-exercise:ql3 -- overlap \
|
||
--kubeconfig=/absolute/private/kubeconfig \
|
||
--context=production \
|
||
--endpoint=https://management.example.org/api/v3/plugin-packages/management \
|
||
--ca-file=/absolute/private/management-ca.crt \
|
||
--input-state=/absolute/private/management-before.json \
|
||
--old-assertion=/absolute/private/old.jwt \
|
||
--new-assertion=/absolute/private/new.jwt \
|
||
--output-state=/absolute/private/management-overlap.json
|
||
|
||
# operator: explicitly revoke old kid + generation increase; keep overlap TLS.
|
||
pnpm evidence:plugin-package-management-live-exercise:ql3 -- revoked \
|
||
--kubeconfig=/absolute/private/kubeconfig \
|
||
--context=production \
|
||
--endpoint=https://management.example.org/api/v3/plugin-packages/management \
|
||
--ca-file=/absolute/private/management-ca.crt \
|
||
--input-state=/absolute/private/management-overlap.json \
|
||
--old-assertion=/absolute/private/old.jwt \
|
||
--new-assertion=/absolute/private/new.jwt \
|
||
--output-state=/absolute/private/management-revoked.json
|
||
|
||
unset QL3_PLUGIN_PACKAGE_MANAGEMENT_LIVE_EXERCISE
|
||
pnpm evidence:plugin-package-management-live-exercise:ql3 -- finalize \
|
||
--input-state=/absolute/private/management-revoked.json \
|
||
--output=/absolute/private/management-exercise.json
|
||
|
||
pnpm evidence:plugin-package-management-live:ql3 -- \
|
||
--kubeconfig=/absolute/private/kubeconfig \
|
||
--context=production \
|
||
--cni-daemonset=kube-system/cilium \
|
||
--cni-container=cilium-agent \
|
||
--cni-name=cilium \
|
||
--exercise=/absolute/private/management-exercise.json \
|
||
--pg-service-file=/absolute/private/pg_service.conf \
|
||
--pg-service=ql3_evidence \
|
||
--output=/absolute/private/plugin-package-management-live.json
|
||
|
||
pnpm audit:plugin-package-management-live-evidence:ql3 -- \
|
||
--report=/absolute/private/plugin-package-management-live.json
|
||
```
|
||
|
||
在报告不存在或审计失败时,真实 IdP 四眼、双 Pod ingress/rotation 与 Kubernetes
|
||
control-plane HA 仍是未完成状态,生产入口继续失败关闭。
|