ADR 0172 — 发布在签名之前按 digest 用 Trivy 扫描它所发布的东西
状态
已接受。 维护者于 2026-09-27 决定(AT-179):扫描并拒绝。引用 ADR 0119(按 digest 发布的镜像)、ADR 0149(按 digest 的 keyless cosign 签名)和 ADR 0162(第五个镜像)。都不编辑。
背景
Trivy 只存在于 ci.yml 的 images 作业中,它扫描 PR 的一个本地构建(runner 守护进程中的 brabo-api:prod)。release.yml 重新构建,推送到 GHCR,在 .release/images.json 中登记 digest 并签名——没有任何扫描。AT-110 的调查测量了为什么这不是同一个工件:PR 写入的 type=gha 缓存不能被 tag 读取(仓库的 663 个 buildkit 缓存全都在 refs/pull/N/merge 中),所以 tag 是冷构建,最终阶段的 apk upgrade 会再次运行,并可能带来另一组包。AT-110 关闭了 PR 这一侧(bake 中的 no-cache-filter = ["runtime"]),但安装程序下载的镜像仍然是唯一没人扫描的那个。
决策
- 在哪里。 在
release.yml的release作业中,在Registrar as imagens publicadas之后、Instalar o cosign/Assinar as imagens publicadas之前。顺序就是机制:被拒绝的镜像不会得到签名,而没有签名install.sh就不会安装它。 - 针对什么。 针对
.release/images.json中登记的每个镜像的 DIGEST(repositorio@sha256:…),从 registry 读取(--image-src remote)——绝不按 tag,理由与签名相同:tag 是可移动的指针,而 digest 才是被发布的东西。列表来自scripts/ci/trivy-do-release.ts referencias,它拒绝空清单或格式错误的 digest。 - 尺子会拒绝。 有可用修复的 HIGH 或 CRITICAL 会让作业失败。标志与
ci.yml的逐字节相同:--scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --ignorefile .trivyignore.yaml --skip-db-update --exit-code 1。决定的是 Trivy 的退出码,绝不是 TypeScript 中的第二把尺子——它会在第一个新的Status上与 Trivy 的产生分歧。在拒绝之前扫描所有镜像,以便日志点名哪些失败了。 - 没有修复的会被报告。 每个镜像第二次扫描,不带
--ignore-unfixed,带--exit-code 0,生成 JSON;scripts/ci/trivy-do-release.ts resumo组装 markdown(每个镜像的两个数字以及没有修复的列表),它进入作业摘要,并作为trivy-sem-correcao.md附加到 Release。这一步在闸门之前运行,以便闸门拒绝时摘要也存在。 - 没有新的允许清单。 唯一的例外文件是
ci.yml已经使用的.trivyignore.yaml——经过策展,顶部有三个条件,每个条目都有expired_at。不使用它会让发布对维护者已经接受的同样例外应用一把比 PR 更严的尺子:测得v6.1.0的engine会有 56 个可修复的发现,全都在该文件记录的第三方扫描器二进制中。spec 会拒绝第二个--ignorefile、--skip-files、--skip-dirs、--vex、--ignore-policy或--ignore-status。 - 同一版本,声明两次,受到守护。
TRIVY_VERSION和TRIVY_SHA256以ci.yml的值进入release.yml的env:。一个地方放不下:工作流的env:不能从另一个文件导入,而为两个变量写一个组合 action 会是第三个要维护的地方。二进制通过curl下载,并在解压前经过sha256sum -c,与ci.yml中一样。没有新的 action(没有任何uses:进入)。 - 证明。
release.yml只在最终 tag 上运行,所以没有 PR 会执行它——与install-e2e.yml的情况相同。scripts/ci/trivy-do-release.spec.ts在每个 PR 中守护:闸门在登记之后、cosign 之前;路径上没有continue-on-error/always();尺子的标志与ci.yml的相同;报告不带--ignore-unfixed且带--exit-code 0;版本和哈希相同;curl→sha256sum -c→tar;gh release create中的资产。通过变异证明:从闸门中去掉--ignore-unfixed,或把cosign-installer移到它之前,都会让 spec 变红。
后果
- 拒绝会留下已发布但未签名的镜像。 推送发生在扫描之前(正是它创建 digest),所以被拒绝的 tag 会在 GHCR 中留下
:X.Y.Z和:<sha>,没有签名,没有 Release,也没有附加的images.json。这是正确的状态——安装程序拒绝没有签名的镜像——但它是一个公开的残留物。重做就是修正基础镜像并切一个新 tag;通过workflow_dispatch重新发布同一个 tag 会重新构建并重新扫描。 - 下一个 tag 可能因 PR 之后公布的 CVE 而以红色诞生。 tag 的扫描使用 tag 当天的漏洞库,而不是 PR 的。这正是要点:到达安装程序的东西,用它到达那一天所知的信息来评判。2026-09-27 针对
v6.1.0的 digest 测得:api和web今天会因CVE-2026-45447(HIGH,libcrypto3/libssl33.3.7-r0→3.3.7-r1)而被拒绝,这是那个 tag 之后的 CVE;engine和backup通过;没有无修复的 HIGH/CRITICAL。新 tag 冷构建并运行apk upgrade,所以应该会带来3.3.7-r1——这里没有证明,因为那需要切一个 tag。 - 时间成本。 在本地针对
v6.1.0的四个 digest 测得:下载库 13 秒,八次扫描(闸门和报告)约 17 秒,经网络。在 runner 上真实的数字只会在第一个 tag 出现;作业保持timeout-minutes: 30,而最近三个发布用时 4–11 分钟。 .trivyignore.yaml开始也对被发布的东西有效。 一个过期的条目(当前的是expired_at: 2026-10-31)会让 PR 和下一个 tag 失败。审阅列表的纪律不变;改变的是,忘了它现在会卡住发布,而不只是合并。- 声明的重复。 版本、哈希和标志存在于两个工作流中。spec 拒绝分歧;升级 Trivy 的人在同一个 PR 中升级两处。