feat(ql3): prevent release catalog tag overwrite

This commit is contained in:
whyour
2026-08-18 03:16:39 +08:00
parent 7ac5e678ba
commit dbd57b27d0
9 changed files with 601 additions and 18 deletions
+21
View File
@@ -11,6 +11,27 @@
最新增量证据(2026-08-18):
- D-349/ADR-0441(已接受;首份真实 GHCR conflict/reuse 证据待实际 release tag):关闭 durable catalog discovery tag 的覆盖窗口。
之前 workflow 虽声明 `v<version>-<scope>` 无部署 authority,却直接对该 tag 执行 `artifact put`;已有不同 digest 会先被覆盖,
response-loss 重跑也没有“相同复用、冲突拒绝”的可执行分支。catalog plan/receipt 现升级为 v2publisher 必须先在 runner 私有
`ocidir://` 生成 exact manifest;远端 repository 不存在时仅以 `staging-<plan-digest>` 非权威 tag 建立可读 inventory。
`qinglong/release-catalog-tag-inventory-decision@v1` 先执行 1 MiB、canonical line、OCI tag 字符集和无重复约束,随后
`qinglong/release-catalog-publication-decision@v1` 将 absent 映射为 `publish_if_absent`、exact digest 映射为
`reuse_exact_digest`,其他 digest 或无界/畸形 inventory 均在 discovery mutation 前失败。两条成功路径都按 immutable reference
回读 release-set 和 raw manifestreceipt 明确绑定 `fail_closed_before_mutation`
`reuse_exact_manifest_digest_only`。OCI registry 仍无 tag CAS,因此诚实保留 `registryTagCas=false`:同 ref concurrency 与组织
package 权限限制外部 writer,最终回读检测发布期间竞争,consumer 永远只信任签名/attested immutable digest。本 Gate 不修改
release-set v3/OCI media、package、生产依赖、数据库、Kubernetes object 或设备运行时。定向 receipt/set/catalog/consumption/
deployment-lock/image-release 回归 143/143;完整 backend 1,371 项为 1,369 pass/2 条件 skip/0 fail18-package clean
build/test 退出 0。12 项 package boundary、Cluster dependency、Edge import、Cluster/Worker/CloudNativePG 部署、backup、
Barman/cert-manager selection、image release、deployment-lock surface 与 release-version 审计全部 compatible14 档 Local
artifact 全部 compatible 且保持既有字节基线:默认 Edge/Standalone 为 2,589,890/2,589,968 bytesadopted 为
2,809,185/2,809,308 bytesapplication 为 3,632,769/3,632,889 bytesapplication-api 为
3,800,322/3,800,466 bytesAI 为 3,069,143/3,069,233 bytesapplication+AI 为 4,493,043/4,493,175 bytes
MCP 为 7,315,930/7,316,038 bytes。真实 PostgreSQL 18.6 arm64 physical HA 再次通过 142/142、timeline `1→2`,报告
SHA-256 为 `13e2f3793d7f418f0c1cc3b05206b393c4f09a45e1b9a5783219c12fb930b3dd`,离线审计通过且无 `ql3-ha-*`
Docker 容器、卷或网络残留。首份真实 GHCR conflict/reuse 结果仍只能由实际受保护 `v3` release tag 取得。
- D-348/ADR-0440(已接受;首份真实线上闭合待实际 release tag):补齐 D-347 确定性收据与实际 tag promotion 之间的 freshness 窗口。此前私有
job 在收据创建时验证 24 小时 freshness,但 release-set aggregate 只检查 `observedAt` 可解析与 receipt digest;镜像构建、扫描和 attestation
延迟后,创建时有效的收据可能在闭合时已经过期。现在 `cluster|all` aggregate 与紧随其后的 source-record audit 必须各自取得 runner-owned 当前
@@ -0,0 +1,93 @@
# ADR-0441Release Catalog Discovery Tag 无覆盖发布
- 状态:Accepted
- 日期:2026-08-18
- 关联 RFCQL-RFC-0001 D-03、D-14、D-336、D-347、D-349
- 关联 ADRADR-0428、ADR-0430、ADR-0439
- SupersedesADR-0428 中允许直接向 discovery tag 重发内容的恢复语义,以及 catalog plan/receipt v1
## 上下文
ADR-0428 把 `v<version>-<scope>` 定义为无部署 authority 的发现入口,并要求部署只信任验证后的 immutable manifest digest。
ADR-0439 又要求相同 source evidence 的合法 workflow 重跑生成相同 release-set 与 catalog identity。
但发布 workflow 仍直接对远端 discovery tag 执行 `regctl artifact put`。OCI Distribution tag 没有跨 registry CAS;如果该 tag 已被外部
写成另一个 digest,直接 put 会先覆盖冲突事实,再在新 digest 上完成验证。response-loss 重跑也无法区分“远端已经是同一确定内容”和“远端
曾指向不同内容”。workflow 的同 ref concurrency 只能串行本仓库 Actions 运行,不能证明 registry 没有其他 package writer。
## 决策
1. catalog plan 升为 `qinglong/release-catalog-plan@v2`publication policy 固定声明:
- staging/discovery tag authority 均为 `none`
- discovery 冲突必须在 mutation 前失败;
- response-loss 只能复用 exact manifest digest
- immutable digest 仍是唯一部署 authority。
2. publisher 必须先把 canonical release-set 发布到 runner 私有的 `ocidir://` layout,并从该 layout 取得 manifest digest 和 raw
manifest。绝不再直接对 discovery tag 执行 `artifact put`
3. publisher 随后读取远端 catalog tag inventory。首次发布时 repository 可能尚不存在;只有在 inventory 读取失败后,才允许把本地
immutable manifest copy 到 `staging-<plan-digest>`。该 deterministic staging tag 无 authority,写后必须解析为同一 manifest
digest,再重新读取 inventory。第二次读取失败即失败关闭。
4. `qinglong/release-catalog-tag-inventory-decision@v1` 对 tag inventory 执行最大 1 MiB、canonical line、OCI tag 字符集和
无重复校验。若 discovery tag 存在,必须读取其 digest;若缺失,则以显式 `absent` 状态进入 publication decision。
5. 新的 `qinglong/release-catalog-publication-decision@v1` 只接受已通过 exact manifest contract 的本地 manifest
- `absent` 产生 `publish_if_absent`
- exact digest 产生 `reuse_exact_digest`,用于 response-loss 恢复;
- 任意不同、缺失格式或非 SHA-256 观察值在 discovery mutation 前失败。
6. `publish_if_absent` 只能从本地 immutable reference copy 到 discovery tag`reuse_exact_digest` 不写 registry。两条路径都必须
再读取 discovery digest、按 immutable reference round-trip release-set、逐字节比较本地/远端 raw manifest,然后才允许签名、
provenance 和 receipt。
7. catalog receipt 升为 `qinglong/release-catalog-receipt@v2`,明确绑定
`fail_closed_before_mutation``reuse_exact_manifest_digest_only`。release-set v3 和 OCI media type 不变,因为被发布的
canonical release-set bytes 没有改变。
8. OCI registry 仍没有 tag CAS,因此本 Gate 不宣称能阻止仓库外 writer 在 inventory 与 copy 之间竞争。workflow 保持同 ref 串行,
package write authority 必须由组织治理;最终回读会发现写入期间的竞争,而任何稍后改写也不能改变 receipt/provenance 中的 immutable
digest。consumer 继续禁止把 discovery tag 当作 authority。
## 失败与恢复
- discovery 已指向不同 digest:不执行 discovery copy,不覆盖现场;操作者必须调查 tag/source/package authority,不能以重跑“修复”。
- discovery 已指向 exact digest:不重新发布,继续 immutable round-trip、签名和 provenance 验证,得到与首次成功相同的 durable
plan/receipt bytes。
- catalog repository 尚不存在:deterministic staging copy 只用于建立 repository 和确定可读 tag inventorystaging tag 永远不能进入
deployment lock。
- registry/auth/network 不可用:staging 或第二次 inventory 失败即停止;不能把不确定读取当成 tag 缺失。
- discovery copy response 丢失:重跑后 inventory 必须看到 exact digest 并走 reuse;看到其他 digest 则失败。
## 部署与资源影响
- Edge/Standalone/路由设备继续只接收 catalog-bound Local selection 和 immutable image digest,不安装 regctl、Node、Cosign 或 GitHub
CLI,不增加 RSS、I/O、timer、listener、updater 或后台任务。
- Cluster 节点、Kubernetes object、CloudNativePG、数据库、migration、SQL、Pool 与运行时镜像均无变化。
- 新工作只在短生命周期发布 runner:一个私有 OCI layout、至多 1 MiB tag inventory、一次 publication decision 和必要时一次 staging
copy;不新增 workspace package、生产依赖或常驻服务。
## 被拒绝的替代方案
### 继续直接 artifact put 后再验 digest
拒绝。后验一致只能证明覆盖后的结果,不能保留或拒绝原有冲突。
### 把 workflow concurrency 当作 registry CAS
拒绝。它只串行同仓库同 ref 的 Actions run,无法约束其他 package token、组织管理员或 registry 外部 writer。
### 让 discovery tag 成为不可变部署入口
拒绝。OCI tag 仍可改写;唯一可持久签名、attest 和部署锁定的 authority 是 manifest digest。
### 为 catalog 新增常驻协调服务
拒绝。发布路径不值得引入新的服务、数据库和低配设备依赖;确定性 staging、冲突决策与 immutable consumer 已能把剩余竞态限制在非权威 tag。
## 验证
- contract 覆盖空/存在/重复/畸形/超限 tag inventory、absent publish、exact response-loss reuse、不同 digest 冲突、非法观察值和
closed CLI
- workflow 静态门拒绝 direct discovery `artifact put`、缺失 publication decision、缺失 exact reuse、无界/malformed inventory 和未回读
immutable digest
- catalog consumption、deployment-lock、Local/Cluster post-publication gate 必须继续只消费 immutable reference
- 定向发布链 143/143,完整 backend 1,371 项为 1,369 pass/2 条件 skip/0 fail18-package clean build/test 退出 0
- 12 项静态审计和 14 档 Local artifact 全部 compatible,默认 Edge/Standalone 至 MCP 的既有字节/RSS 上限未漂移;
- PostgreSQL 18.6 arm64 physical HA 通过 142/142、timeline `1→2`,证据 SHA-256 为
`13e2f3793d7f418f0c1cc3b05206b393c4f09a45e1b9a5783219c12fb930b3dd`,离线审计 compatible 且 Docker 资源零残留;
- 完整证据记录于 QL-RFC-0001 D-349;首份真实 GHCR conflict/reuse 证据仍须由受保护 `v3` release tag 产生。
+1
View File
@@ -444,6 +444,7 @@
| [ADR-0438](./ADR-0438-content-free-private-release-evidence-receipt-chain.md) | 内容无关的私有发布证据收据链 | Accepted(首份真实公开收据待实际 release tag |
| [ADR-0439](./ADR-0439-deterministic-private-evidence-receipts-and-release-set-replay.md) | 确定性私有证据收据与 Release-set 重放 | Accepted(首份真实线上重放待实际 release tag |
| [ADR-0440](./ADR-0440-release-set-closure-private-evidence-freshness.md) | Release-set 闭合时私有证据 Freshness 重验证 | Accepted(首份真实线上闭合待实际 release tag |
| [ADR-0441](./ADR-0441-no-overwrite-release-catalog-discovery-publication.md) | Release Catalog Discovery Tag 无覆盖发布 | Accepted(首份真实 GHCR conflict/reuse 证据待实际 release tag |
## 规则
@@ -445,6 +445,20 @@ release-set、catalog immutable digest、两类 provenance 与 receipt 全部生
consumer 失败时不能宣布 Cluster release。不要给 consumer 临时增加写权限“修复”可见性或 tag,应修正 GHCR package visibility/retention
或发布配置后,对同一受保护 tag 重跑完整工作流并重新验证 exact digest。
catalog discovery tag 另有独立的无覆盖协议。publisher 先在 runner 私有 `ocidir://` 中生成 exact manifest;不得直接向
`v<version>-<scope>` 执行 `artifact put`。远端 repository 不存在时只允许创建
`staging-<catalog-plan-digest>` 非权威 tag,以便重新取得不超过 1 MiB 的完整 tag inventoryinventory 必须先通过
`qinglong/release-catalog-tag-inventory-decision@v1` 的 canonical line、OCI tag 字符集和无重复检查。随后:
- discovery 缺失:publication decision 为 `publish_if_absent`,从本地 immutable reference copy 后立即回读;
- discovery 已是 exact manifest digestdecision 为 `reuse_exact_digest`,不写 registry,继续 immutable 验证;
- discovery 是其他 digest、inventory 无界/重复/畸形,或 repository 建立后仍无法读取:在覆盖前失败。
catalog plan/receipt 因此分别为 v2receipt 固定声明 `fail_closed_before_mutation`
`reuse_exact_manifest_digest_only`。GHCR tag 没有 CAS,组织仍必须限制 package writerworkflow 的同 ref concurrency 只能串行受保护
tag 的本仓库 run。最终 digest/manifest 回读用于发现发布期间竞争,deployment consumer 仍只接受已签名和 attested 的 immutable reference
绝不能因为 discovery 当前“看起来正确”而把 tag 写入 rollout。
workflow bundle 当前保留 90 天;长期入口是 OCI catalog 的 immutable digest。GHCR 并非 WORMrelease owner 仍须维护
package 可见性、读取权限和满足组织要求的 retention/备份策略。任何归档或镜像过程都不得改写 canonical JSON,并须保留
原 catalog manifest digest、receipt 与 provenance 关联。