供应链证据闭包:把 SBOM、扫描、签名与部署绑定到同一 Digest
构建、SBOM、漏洞扫描和签名任务如果各自重新解析一次可变 Tag,即使全部显示绿色,也可能描述不同字节。安全报告对应旧镜像、签名保护中间镜像、部署拉取最后一次覆盖后的镜像时,文件数量再多也没有形成证据。任务的独立交付物不是某个工具输出,而是围绕同一 OCI Digest 可复核、可复制、可恢复的证据闭包。
三个产品各自持有一段事实
Syft把明确 source 解析成 package、relationship 和 SBOM;Grype消费软件包目录与可追溯漏洞数据库,产生匹配和门禁状态;Sigstore Cosign把内容摘要绑定到密钥或身份,并验证签名与 Attestation。三个入口分别维护安装、版本、配置、故障和升级,本任务只消费它们的输出。
SBOM 描述组件,不证明组件没有漏洞。漏洞报告描述特定数据库和策略对组件的判断,不证明制品来自获准构建者。签名证明某个密钥或身份认可指定摘要,不证明关联声明内容正确。Attestation 绑定 Subject 与结构化 Predicate,但消费方仍要检查类型、Schema 和策略。证据闭包要求这些事实共同指向一个 Digest,并能在发布后重新读取。
Digest 是跨 Job 的唯一主键
镜像构建完成后只解析一次 Digest,写入只读流水线输出。后续 Job 不再接收 Tag:
set -euo pipefail
IMAGE_REPO='registry.example.com/team/service'
IMAGE_TAG="${IMAGE_REPO}:candidate"
docker push "$IMAGE_TAG"
DIGEST="$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE_TAG")"
case "$DIGEST" in
"$IMAGE_REPO"@sha256:*) ;;
*) printf 'unexpected immutable subject: %s\n' "$DIGEST" >&2; exit 1 ;;
esac
printf '%s\n' "$DIGEST" > artifacts/image-subject.txt
sha256sum artifacts/image-subject.txt > artifacts/image-subject.sha256部署、签名、SBOM 和扫描任务读取同一个 image-subject.txt。文件传递过程还要由平台 Artifact 摘要或受信存储保护;能修改这个文件的 Job 等价于能替换发布对象。
多架构镜像需要先选择证据粒度。若部署的是 image index,闭包至少记录 index Digest,并为每个平台 manifest 提供可追踪 SBOM 或明确聚合规则。只扫描 amd64 后给整个 index 签“已扫描”会掩盖 arm64 差异。
证据清单先于最终裁决
各产品 Job 输出自己的材料和机器状态,汇总 Job 不重新生成事实,只验证并编制清单:
set -euo pipefail
SUBJECT="$(cat artifacts/image-subject.txt)"
jq -e '.artifacts | type == "array"' artifacts/sbom.syft.json
jq -e '.matches | type == "array"' artifacts/grype.json
jq -e '.built != null or .schemaVersion != null' artifacts/grype-db-status.json
sha256sum \
artifacts/image-subject.txt \
artifacts/sbom.syft.json \
artifacts/sbom.spdx.json \
artifacts/grype.json \
artifacts/grype-db-status.json \
artifacts/cosign-verify.json \
artifacts/attestation-verify.json \
> artifacts/evidence.manifest.sha256清单还应记录 Syft、Grype、Cosign 版本,配置摘要、漏洞数据库标识、签名策略版本、流水线身份和源 commit。单独保存 finding 数量无法解释数据库更新后的变化;单独保存 Verified OK 也无法重建命中的 identity 与 issuer。
Attestation 要验证 Subject 和 Predicate
Attestation 不是“仓库里有一个附件”。验证端应约束签发者与身份,选择期望的 Predicate Type,并解码结果核对 subject[].digest。同一 Subject 可能挂载多份声明,不能无条件取第一份。
SUBJECT="$(cat artifacts/image-subject.txt)"
EXPECTED_DIGEST="${SUBJECT##*@sha256:}"
jq -e --arg digest "$EXPECTED_DIGEST" '
any(.payloads[]?.subject[]?; .digest.sha256 == $digest)
' artifacts/attestation-verify.json
jq -e '
all(.payloads[]?; .predicateType != null and .predicate != null)
' artifacts/attestation-verify.json实际 Cosign 输出结构以锁定版本为准,解析脚本必须有正反 fixture。签名正确但 Subject 错、Predicate Type 错、Schema 无法解析、身份不在允许列表,都应成为不同失败类型。Predicate 使用 SPDX SBOM 时,原始 SBOM 哈希和 Attestation 验证结果同时保留,避免只保存一个不可排障的布尔值。
发布门禁验证闭包,不重做产品工作
门禁按固定顺序检查目标可拉取、SBOM 结构与关键组件、扫描器执行状态与数据库证据、签名身份、Attestation Subject/类型、证据清单完整性。任一步骤工具失败、证据缺失或目标漂移都停止晋级。
漏洞阈值命中与扫描器故障必须分开。批准例外要绑定组件、漏洞、制品 Digest、owner、理由和到期条件,不能写成永久全局 ignore。扫描数据随后更新时,可以对同一 SBOM 复扫并产生新的报告;新报告不能覆盖历史发布时采用的数据库与决策。
流水线 DAG 可以并行生成 SBOM 与准备签名环境,但所有任务必须先读取同一 Subject。汇总与发布 Job 只接受受保护分支产物,fork 和不可信脚本不能接触 Registry 写权限、OIDC 发布身份或 KMS signer。
反向实验比绿色路径更能证明绑定
第一类实验替换 image-subject.txt 中的 Digest,但保留旧 SBOM 与报告。汇总脚本应因 Subject 或清单不一致失败。第二类实验保留 Digest,篡改 SBOM 一个字节,哈希清单必须失败。第三类实验使用未授权 identity 的合法签名,密码学验证可能成功,组织身份策略必须拒绝。
第四类实验删除一份 Attestation 或更换 Predicate Type,闭包检查应明确报告“证据缺失/类型错误”,不能降级成警告。第五类实验让漏洞数据库不可用,扫描 Job 应记录工具失败而不是生成空报告。每个反例都保存退出码和最小诊断,不把真实 Token、完整私有 SBOM 或签名口令写入公开日志。
这些实验回答不同问题:摘要绑定能否抓住目标漂移,清单能否抓住文件篡改,身份策略能否拒绝错误发布者,闭包能否区分缺证和通过,扫描门禁能否避免 fail-open。只做“正常签一次”无法证明其中任何一项。
复制和垃圾回收必须保持闭包
跨 Registry 复制镜像后,目标仓库不一定自动保留 Referrers、旧式签名 Tag 或 Attestation。复制完成必须在目标仓重新执行签名和 Attestation 验证,并比较 Subject。只比较镜像 Tag 数量会漏掉关联证据。
证据保留与镜像 GC 需要共同设计。仍被部署、回滚或审计记录引用的 Digest,其签名、Attestation、SBOM、扫描报告和策略版本不能提前删除。定期选择历史 Digest 做恢复抽查,证明 Registry、证据仓、信任根和验证工具仍能读取它。
更完整的 OCI Referrers、代理 Media Type 与保留模型由Registry 证据保留负责;任务流水线只要确保复制前后闭包可枚举、可验证。
回滚使用上一个已验证 Digest
回滚不是把旧 Tag 再推一次。部署记录应保存上一个 Digest、证据清单哈希、验证策略和环境批准状态;切换前重新读取其签名、Attestation 与发布时门禁结果。旧证据不可读时停止自动回滚,进入人工处置,不能重新签名后伪装成历史事实。
部分失败按外部状态恢复。镜像已推送但未签名,可以在确认 Digest 后补齐证据;签名已存在但 Attestation 缺失,只补缺失对象;证据齐全但部署未发生,重新验证闭包后继续。任何重跑都先盘点 Registry 与证据仓,避免为错误 Subject 再生成一套看似完整的材料。
权限、成本与责任落到对象上
构建 Job 只写候选镜像,SBOM 与扫描 Job 只读目标和数据库,签名 Job 获得受保护短期身份,验证 Job 只读 Registry 与信任材料,部署 Job 只消费已经通过的 Digest。权限按阶段分离后,一个解析器或 PR 脚本被利用,不会直接得到完整发布能力。
成本按每个 Digest 统计:扫描时间、峰值内存、临时磁盘、Registry 请求、KMS 调用、证据字节和历史复扫次数。数据库快照可用内容哈希引用共享只读对象,不必为每次发布复制一份;但引用仍存活时,底层对象不能被清理。
构建平台负责 Subject、Runner 和工具版本;安全团队负责数据库、身份策略、阈值与例外;Registry 团队负责不可变性、复制和证据可用性;服务 owner 解释组件与风险并承担发布决定。日常审计抽样检查同一 Digest 是否贯穿 build、SBOM、scan、sign、verify 与 deploy,而不是只数绿色 Job。
