Docker 基础体系 · 第 41/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。

Docker 构建证明:Provenance、SBOM、Attestation 和 SLSA

在 Docker 软件供应链中,“镜像构建成功”只说明某个构建过程产生了可运行的镜像,不能说明:

  • 使用了哪个源码版本;
  • 使用了哪个基础镜像及其摘要;
  • 构建过程由哪个构建器执行;
  • 构建时是否使用了网络、密钥或未声明的输入;
  • 镜像中包含哪些软件包;
  • 这些信息是否与最终镜像绑定;
  • 记录是否被签名、验证和策略系统接受。

Provenance、SBOM、Attestation 和 SLSA 用来回答这些问题,但它们解决的不是同一个问题:

术语 主要回答的问题
Provenance 这个制品是怎样构建出来的?
SBOM 这个制品包含哪些组件?
Attestation 如何把上述声明绑定到某个制品,并让工具读取或验证?
SLSA 如何定义和提升构建供应链的可信度?

这些信息通常不是镜像文件系统中的普通文件,而是与 OCI 镜像制品关联的构建证明。理解它们需要先理解 Docker 镜像的身份和分发结构。

镜像摘要是构建证明的锚点

Docker 标签不是稳定身份。example/app:1.4.0 只是一个可变的名称,Registry 可以将它重新指向另一个镜像。不可变身份是内容摘要,例如:

sha256:7c3b...e91a

在 OCI Image Specification 中,一个镜像通常由以下对象组成:

Image Index
├── Image Manifest
│   ├── Config Blob
│   └── Layer Blob × N
└── 其他平台的 Image Manifest

单平台镜像的 Manifest 会引用一个配置对象和若干层;多平台镜像使用 Image Index 引用多个不同架构的 Manifest。每个对象的摘要由其序列化内容计算得到:

D=H(B)D = H(B)

其中:

  • BB 是对象的字节内容;
  • HH 通常是 SHA-256;
  • DD 是该对象的内容摘要。

只要对象内容发生变化,摘要就会变化。因此,构建证明必须明确指向一个摘要,而不能只写:

subject: example/app:latest

正确的逻辑是:

subject:
  name: example/app
  digest:
    sha256:...

对于多平台构建,还要区分两个摘要:

  1. Image Index digest:整个多平台制品的摘要;
  2. 平台 Image Manifest digest:例如 linux/amd64 镜像的摘要。

如果证明描述的是多平台构建结果,通常应绑定 Image Index digest;如果证明只适用于某个平台,则必须绑定对应平台 Manifest digest。把 amd64 的 SBOM 错误地当成整个多平台镜像的 SBOM,会造成覆盖范围误判。

Docker 使用 BuildKit 构建镜像时,证明可以作为 OCI 分发结构中的附加对象保存。实现细节会随 BuildKit、Registry 和客户端版本变化,但核心关系不变:

目标镜像摘要 D
   │
   ├── Provenance Attestation
   └── SBOM Attestation

OCI Image Specification 定义了镜像 Manifest、Index、Config 和 Layer 等对象;OCI Runtime Specification 则描述如何将镜像配置转换为容器运行时环境。Provenance 和 SBOM 属于构建与分发阶段的元数据,不是 OCI Runtime Specification 中的容器运行时配置。

Attestation:对制品作出的机器可读声明

Attestation 可以翻译为“证明”或“声明”。它是一份结构化文档,声明某个对象满足某些事实,并通过 subject 字段与该对象绑定。

一个抽象的 Attestation 可以表示为:

{
  "type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "registry.example.com/team/app",
      "digest": {
        "sha256": "..."
      }
    }
  ],
  "predicateType": "https://slsa.dev/provenance/v1",
  "predicate": {
    "buildDefinition": {},
    "runDetails": {}
  }
}

这里有三个层次:

  1. Statement:通用声明外壳;
  2. subject:被声明的制品;
  3. predicate:具体声明内容,例如 Provenance 或 SBOM。

因此,Provenance 和 SBOM 通常都是 Attestation 的一种内容,而不是与 Attestation 并列的完全独立机制。

Attestation 不等于签名

这是最容易混淆的地方。

一份 Attestation 可以是:

  • 未签名的 OCI 附属对象;
  • 使用 Sigstore Cosign 等工具签名后的对象;
  • 存放在 Registry 中,并由另一个签名系统保护;
  • 被复制到离线存储后由策略系统验证。

“文件中写着某个构建结论”不等于“这个结论可信”。可信性至少需要考虑:

可信声明=正确绑定+完整性保护+签名身份验证+策略接受\text{可信声明} = \text{正确绑定} + \text{完整性保护} + \text{签名身份验证} + \text{策略接受}

其中:

  • 正确绑定:声明的 digest 等于实际要部署的制品 digest;
  • 完整性保护:内容未在传输或存储中被修改;
  • 签名身份验证:签名者身份符合预期;
  • 策略接受:例如只接受受信任工作流生成的证明。

一个 unsigned provenance 仍然有诊断和审计价值,但不能单独证明构建器没有被攻击。

Provenance:描述构建因果链

Provenance 是构建来源证明,描述“输入经过什么过程产生了这个输出”。

它关注的不是镜像运行时有哪些文件,而是构建过程本身。例如:

源码提交 abc123
   │
   ├── Dockerfile
   ├── 构建上下文
   ├── 基础镜像 digest
   ├── BuildKit builder
   ├── 构建参数
   └── 构建步骤
          │
          ▼
     镜像 digest D

现代 BuildKit 生成的 Provenance 通常采用 in-toto Statement 结构,并使用 SLSA Provenance v1 predicate。具体字段会因 BuildKit 版本、mode 和构建方式而变化,但常见信息包括:

  • 构建器身份;
  • 构建入口或工作流信息;
  • 源码仓库和版本;
  • 构建参数;
  • Dockerfile 或构建定义;
  • 输入材料;
  • 构建开始和结束时间;
  • 最终输出摘要;
  • 构建步骤或完整命令信息。

mode=minmode=max

BuildKit 常见的 Provenance 模式可以理解为信息量的取舍:

  • mode=min:保留足以说明构建来源的最小信息;
  • mode=max:保留更完整的构建细节。

示例:

docker buildx build \
  --provenance=mode=max \
  --tag registry.example.com/team/app:1.0.0 \
  --push \
  .

该命令的前提是:

  • Docker Engine 使用支持 BuildKit 的现代版本;
  • 已配置可用的 Buildx builder;
  • 已登录目标 Registry;
  • Registry 能保存镜像及其附加证明。

--push 很重要。构建结果如果只导出为传统 Docker 本地镜像,附加证明可能无法被本地镜像存储完整保留。Docker 的经典 image store 对 OCI 附加对象的支持有限;使用支持 OCI 对象的 containerd image store,或直接推送到 Registry,通常更适合验证证明。

mode=max 不是“更安全”的开关,而是“记录更多构建细节”。它也可能暴露构建参数、源码路径、依赖来源或命令行信息。密码、令牌和私钥不应放入 ARG、普通环境变量或会进入 Provenance 的构建参数中。

错误示例:

ARG NPM_TOKEN
RUN npm config set //registry.example.com/:_authToken=$NPM_TOKEN

即使最终层中删除了配置文件,令牌仍可能出现在构建历史、缓存或 Provenance 中。应使用 BuildKit secret:

# syntax=docker/dockerfile:1

FROM node:22-bookworm AS build
WORKDIR /src

COPY package*.json ./

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci

COPY . .
RUN npm run build

构建命令:

docker buildx build \
  --secret id=npmrc,src="$HOME/.npmrc" \
  --provenance=mode=max \
  --tag registry.example.com/team/app:1.0.0 \
  --push \
  .

Secret mount 只在对应的 RUN 步骤中可见,不应作为镜像层内容或普通构建参数进入结果。但这不代表构建器绝对不会观察到秘密:受攻击的构建器仍可能读取构建时秘密,因此构建器本身也属于信任边界。

SBOM:描述制品中的软件组成

SBOM(Software Bill of Materials) 是软件物料清单。对容器镜像而言,它通常列出:

  • 操作系统发行版及其软件包;
  • 应用依赖;
  • 依赖版本;
  • 包管理器来源;
  • 软件包标识;
  • 有时还包括许可证、文件哈希和关系信息。

SBOM 主要回答:

这个镜像里有什么?

它不自动回答:

这个镜像是由谁构建的?

也不自动回答:

镜像中的包是否安全?

例如,SBOM 可以列出 openssl 的版本,但漏洞是否可利用、是否被实际加载、是否满足组织策略,需要扫描器和策略系统进一步判断。

SBOM 的范围

SBOM 的范围必须明确。一个最终镜像可能来自多阶段构建:

FROM golang:1.23 AS build
WORKDIR /src
COPY . .
RUN go build -o /out/app ./cmd/app

FROM debian:bookworm-slim
COPY --from=build /out/app /usr/local/bin/app
ENTRYPOINT ["/usr/local/bin/app"]

最终镜像通常不会包含 Go 编译器和源码,但构建阶段使用了它们。因此至少有两种不同的 SBOM 语义:

  1. 最终运行时 SBOM:描述最终镜像文件系统;
  2. 构建环境 SBOM:描述构建器、编译器和构建依赖。

部署扫描通常关心第一种;构建供应链审计可能还关心第二种。不能因为 Provenance 记录了 golang:1.23,就断言 Go 编译器位于最终运行时镜像中。

使用 BuildKit 生成 SBOM

可以显式请求 SBOM:

docker buildx build \
  --sbom=true \
  --tag registry.example.com/team/app:1.0.0 \
  --push \
  .

也可以指定 SBOM 生成器:

docker buildx build \
  --sbom=generator=docker/scout-sbom-indexer:latest \
  --tag registry.example.com/team/app:1.0.0 \
  --push \
  .

生成器的镜像、输出格式和支持范围属于实现能力,不是 OCI Image Specification 的统一保证。不同版本的 Docker、BuildKit 和 SBOM 生成器可能得到不同的包识别结果。生产环境应固定生成器版本或摘要,并验证生成结果的格式和覆盖范围。

若同时生成 Provenance 和 SBOM:

docker buildx build \
  --provenance=mode=max \
  --sbom=true \
  --tag registry.example.com/team/app:1.0.0 \
  --push \
  .

命令成功通常只说明构建器生成并导出了结果,不代表 Registry、客户端和后续复制工具一定保留了这些证明。应在目标 Registry 上检查,而不是只看构建命令的退出码。

从构建到 Registry 的数据流

典型流程如下:

sequenceDiagram
    participant C as Client
    participant B as BuildKit
    participant R as Registry
    participant V as Verifier

    C->>B: Dockerfile、context、build args、secrets
    B->>B: 解析 Dockerfile 并执行构建
    B->>B: 计算 image config、layers、manifest
    B->>B: 生成 Provenance 和 SBOM
    B->>R: 推送 layers
    B->>R: 推送 image manifest/index
    B->>R: 推送 attestation objects
    C->>R: 获取 tag 对应的 digest
    V->>R: 读取目标 digest 与 attestations
    V->>V: 校验绑定、格式、签名和策略

这里有几个容易被忽略的状态变化:

  1. 构建器先生成镜像内容和摘要;
  2. Provenance、SBOM 的 subject 必须引用该摘要;
  3. 镜像及证明分别上传;
  4. Registry 最终保存它们之间的关联;
  5. 客户端按 tag 拉取时,未必会把证明作为容器运行时文件拉到本地;
  6. 验证器必须重新解析 tag,确认它当前仍指向被证明的 digest。

推送过程中可能出现部分成功:

  • 层已经上传,但 Manifest 尚未上传;
  • 镜像 Manifest 已上传,但证明上传失败;
  • SBOM 上传成功,但签名上传失败;
  • 网络重试导致客户端只看到部分关联对象。

因此,“镜像能拉取”不等于“证明完整”。生产流水线应将“镜像存在”“Provenance 存在”“SBOM 存在”“签名有效”“策略通过”作为独立检查项。

查看构建证明

构建并推送后,可以先查看目标引用:

docker buildx imagetools inspect \
  registry.example.com/team/app:1.0.0

该命令通常用于查看镜像索引、平台和摘要;现代 Docker/Buildx 版本也可能显示关联的 Attestation 条目。输出形式受客户端版本和 Registry 实现影响,因此不能把某一种人类可读输出当成稳定 API。

更稳妥的验证原则是:

  1. 解析 tag 得到当前 digest;
  2. 读取该 digest 的 OCI Manifest 或 Index;
  3. 找到引用该 digest 的 Provenance/SBOM 对象;
  4. 解码 Statement;
  5. 检查 subject.digest
  6. 再验证签名和策略。

例如,使用 Cosign 检查签名和 Provenance 时,命令形式可能如下:

cosign verify-attestation \
  --type slsaprovenance \
  registry.example.com/team/app@sha256:...

该命令要求:

  • Cosign 版本支持目标证明格式;
  • Registry 保存了对应证明;
  • 签名存在;
  • 验证环境配置了正确的信任根、证书或身份约束。

如果构建时只生成了 unsigned attestation,cosign verify-attestation 不能凭空为它建立签名信任。构建证明和签名必须分别配置。

直接处理 OCI 对象时,也可以使用 ORAS、Registry API 或专用供应链工具读取附属对象。但不同 Registry 对 OCI Referrers API、旧式 Index 关联方式和复制策略的支持并不完全一致。验证工具应明确支持哪种关联方式。

SLSA:对供应链安全等级的结构化描述

SLSA(Supply-chain Levels for Software Artifacts) 是一套软件供应链安全框架。它不是一个 Docker 命令,也不是一种 SBOM 格式。

SLSA 的核心思想是:构建输出不应只有一个文件,还应有可验证的 Provenance,用于说明:

  • 输入是什么;
  • 构建由什么构建器执行;
  • 构建过程产生了什么输出;
  • 哪个身份负责执行构建;
  • 证明是否可以防篡改和验证。

SLSA Provenance v1 的结构重点可以抽象为:

Statement
├── subject
├── predicateType = SLSA Provenance v1
└── predicate
    ├── buildDefinition
    │   ├── buildType
    │   ├── externalParameters
    │   └── resolvedDependencies
    └── runDetails
        ├── builder
        ├── metadata
        └── byproducts

buildDefinitionrunDetails

buildDefinition 描述“应该执行什么”:

  • 构建类型;
  • 外部参数;
  • 解析后的依赖;
  • 输入源码和基础镜像等材料。

runDetails 描述“实际由谁执行”:

  • 构建器身份;
  • 执行元数据;
  • 开始和结束时间;
  • 构建副产物或相关信息。

这样的分离很重要。仅有源码提交号并不能证明实际使用了该源码;仅有构建器身份也不能证明构建输入没有被替换。验证器需要把声明中的输入、构建器和输出联系起来。

SLSA 等级不能由 --provenance 自动推出

启用:

--provenance=mode=max

表示 BuildKit 生成更完整的 Provenance,不等于整个系统自动满足某个 SLSA Level。

一个组织是否达到某个 SLSA 等级,还取决于:

  • 构建是否在受保护的托管服务中执行;
  • 构建定义是否不可被未授权修改;
  • Provenance 是否由可信构建器生成;
  • 证明是否不可篡改;
  • 构建器是否隔离;
  • 输入依赖是否被固定和记录;
  • 签名身份是否可验证;
  • 是否存在重放、注入和权限提升防护。

因此,SLSA 是对系统能力的评估,BuildKit Provenance 是其中的重要证据,但不是完整的等级认证。

一个完整的构建示例

目录结构:

app/
├── Dockerfile
├── go.mod
├── go.sum
└── cmd/
    └── app/
        └── main.go

Dockerfile:

# syntax=docker/dockerfile:1

FROM golang:1.23-bookworm AS build
WORKDIR /src

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 go build -trimpath -o /out/app ./cmd/app

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]

构建:

docker buildx build \
  --platform=linux/amd64 \
  --provenance=mode=max \
  --sbom=true \
  --tag registry.example.com/team/app:1.0.0 \
  --push \
  .

每个参数的意义是:

  • --platform=linux/amd64:明确本次输出的平台;
  • --provenance=mode=max:生成较完整的构建来源证明;
  • --sbom=true:请求生成 SBOM;
  • --tag:为输出提供可读引用;
  • --push:把镜像和关联证明推送到 Registry;
  • .:构建上下文,必须注意其中哪些文件会作为输入。

.dockerignore 也会影响 Provenance 和最终输入:

.git
node_modules
*.log
.env

忽略 .git 后,构建器可能无法从上下文自动提取 Git 元数据;忽略 .env 可以避免将本地秘密带入上下文。构建上下文不是“当前目录的透明快照”,而是经过 Dockerfile 和 .dockerignore 共同决定的输入集合。

随后使用摘要而不是 tag 进行部署:

docker buildx imagetools inspect \
  registry.example.com/team/app:1.0.0

假设输出中得到:

Digest: sha256:abc...

部署配置应固定为:

services:
  app:
    image: registry.example.com/team/app@sha256:abc...

这样,后续即使 1.0.0 标签被重新指向其他镜像,部署对象仍然不变。验证 Provenance 和 SBOM 时,也应使用同一个 digest。

Compose 中的构建证明

Compose Build Specification 支持将部分构建属性写入服务的 build 配置。示例:

services:
  app:
    image: registry.example.com/team/app:1.0.0
    build:
      context: .
      dockerfile: Dockerfile
      provenance: mode=max
      sbom: true

执行:

docker compose build app

是否自动推送、是否保存附加证明以及具体支持的属性,取决于 Compose、Docker Engine 和 BuildKit 版本。需要推送时,应使用当前版本文档中确认过的 Compose 推送流程,或直接使用等价的 docker buildx build --push

Compose 文件描述的是应用构建配置,不是签名策略。即使写入了:

provenance: mode=max
sbom: true

也不表示:

  • Provenance 已签名;
  • 镜像一定不可变;
  • SBOM 一定覆盖所有构建阶段;
  • Registry 一定保存了附属证明;
  • 部署系统一定会拒绝缺少证明的镜像。

这些要求仍需在构建流水线和部署门禁中验证。

验证的形式化条件

设:

  • II 是即将部署的镜像;
  • DI=H(I)D_I = H(I) 是镜像摘要;
  • PP 是 Provenance;
  • SS 是 SBOM;
  • DPD_PDSD_S 是证明对象自身的摘要;
  • KK 是签名公钥或信任根;
  • RR 是组织策略。

一个基本验证过程可以写成:

Accept(I,P,S)    {P.subject.digest=DIS.subject.digest=DIFormatValid(P)FormatValid(S)SignatureValid(P,K)SignatureValid(S,K)Policy(P,S,R)\operatorname{Accept}(I,P,S) \iff \begin{cases} P.subject.digest = D_I \\ S.subject.digest = D_I \\ \operatorname{FormatValid}(P) \\ \operatorname{FormatValid}(S) \\ \operatorname{SignatureValid}(P,K) \\ \operatorname{SignatureValid}(S,K) \\ \operatorname{Policy}(P,S,R) \end{cases}

逐步解释:

  1. 先计算实际部署镜像的 digest;
  2. 检查 Provenance 的 subject 是否指向该 digest;
  3. 检查 SBOM 的 subject 是否指向该 digest;
  4. 验证 JSON、predicate type 和必需字段;
  5. 验证证明签名及签名者身份;
  6. 根据策略检查来源、构建器、基础镜像、漏洞和许可证。

任何一个条件失败,结论都不能是“供应链可信”。但不同失败应区分处理:

  • subject 不匹配:可能是 tag 漂移、复制错误或证明绑定错误;
  • 找不到证明:可能是未生成、未推送或 Registry 丢失;
  • 签名失败:可能是内容被改动、证书过期或信任配置错误;
  • 策略失败:证明有效,但不满足组织要求;
  • SBOM 缺包:可能是生成器能力或最终文件系统扫描范围有限。

供应链策略如何使用这些信息

一个具体策略可以定义为:

允许部署,当且仅当:

1. 镜像使用 digest 引用;
2. Provenance subject 等于部署 digest;
3. 构建器身份属于受信任 CI 工作流;
4. 源码版本属于受保护分支;
5. 基础镜像来自允许的 Registry;
6. 基础镜像使用 digest 固定;
7. SBOM 存在且格式可解析;
8. 阻断级漏洞不超过组织阈值;
9. Provenance 和 SBOM 的签名通过验证。

这里的条件并非 Docker 或 OCI 的统一强制规则,而是组织策略。Docker BuildKit 负责产生部分证据;Registry 负责保存和分发对象;Cosign 或其他签名系统负责签名和验证;扫描器负责漏洞判断;Admission Controller、CI 门禁或部署平台负责执行策略。

策略必须针对 digest,而不是 tag。否则会出现典型竞态:

t0: 验证 example/app:1.0.0,得到 digest A
t1: tag 被重新推送并指向 digest B
t2: 部署系统根据 tag 拉取 digest B

验证的是 A,运行的却是 B。使用 digest 引用可以消除这条竞态路径。

常见误解与反例

“有 SBOM 就表示没有漏洞”

反例:

SBOM:
  openssl = 3.0.x

这只说明镜像包含该版本包。漏洞判断还要结合:

  • 漏洞数据库;
  • 发行版补丁回溯策略;
  • 实际架构;
  • 包是否被加载;
  • 运行配置;
  • 组织风险阈值。

SBOM 是输入证据,不是安全结论。

“有 Provenance 就证明构建没有被篡改”

如果构建器被攻击,它可以生成一份内容格式正确但事实虚假的 Provenance。未签名的证明尤其无法回答“是谁生成的”。即使签名有效,如果签名密钥位于被攻陷的构建环境中,签名也只能证明“该密钥签了这份声明”,不能自动证明声明真实。

因此,签名验证必须结合:

  • 签名身份;
  • 构建器来源;
  • 工作流配置;
  • 权限隔离;
  • 审计日志;
  • 可重现或独立复核能力。

mode=max 可以恢复所有构建事实”

Provenance 只能记录构建器知道或实现选择记录的事实。它无法自动发现:

  • 构建脚本内部偷偷访问的外部服务;
  • 未被构建器观测到的环境依赖;
  • 被恶意工具篡改后的源代码;
  • 运行时通过网络下载的文件;
  • 构建器主机内核或宿主机层面的攻击。

mode=max 提高可见性,不会把不可信构建器变成可信构建器。

“最终镜像 digest 一定包含所有证明”

镜像 digest 只覆盖被摘要对象本身。Provenance 和 SBOM 通常是独立的 OCI 对象,不会改变原镜像层内容。复制镜像时,如果工具只复制 Manifest 和 Layer,而没有复制关联对象,目标 Registry 可能只有镜像没有证明。

验证时应在最终部署使用的 Registry 上检查,而不是只检查源 Registry。

“镜像内放一个 /sbom.json 就等于 SBOM Attestation”

把 SBOM 文件复制到镜像中有一个明显缺点:文件会成为镜像内容的一部分,可能暴露构建信息,并且不一定能被供应链工具按标准关联和验证。

镜像内文件可以作为运行时或离线审计辅助信息,但它与以 OCI 附属对象保存、以 digest 绑定的 SBOM Attestation 不是同一件事。

Linux 容器边界

本文示例的边界是 Linux 容器和 Linux 镜像:

  • Dockerfile 中的基础镜像是 Linux 用户空间;
  • linux/amd64linux/arm64 等是 OCI 平台变体;
  • SBOM 主要描述 Linux root filesystem 中的包;
  • 运行时行为还受到 Linux 内核、namespace、cgroup、seccomp 和宿主机配置影响。

容器镜像不携带独立 Linux 内核。因此,Provenance 和 SBOM 不能证明:

  • 宿主机内核没有漏洞;
  • Docker daemon 没有被篡改;
  • containerd、runc 或 OCI runtime 没有被攻击;
  • Kubernetes 节点配置安全;
  • 运行时加载的外部配置和秘密安全。

OCI Runtime Specification 描述运行时配置和生命周期;它不规定 Docker BuildKit 如何生成 Provenance 或 SBOM。构建证明覆盖的是制品生成过程,运行时安全需要单独的节点、运行时和部署策略。

生产故障诊断顺序

当策略系统报告“镜像缺少证明”时,不应立即重新构建。可以按以下因果顺序诊断:

  1. 确认部署 digest

    docker buildx imagetools inspect \
      registry.example.com/team/app:1.0.0
    

    记录实际 digest,避免继续使用漂移的 tag。

  2. 确认构建命令是否请求证明

    检查是否存在:

    --provenance=...
    --sbom=...
    

    如果显式使用了 --provenance=false,Provenance 不会生成。

  3. 确认构建结果是否推送到 Registry

    仅执行本地 docker build,不能假定附属证明已进入远端 Registry。

  4. 确认本地镜像存储能力

    经典 Docker image store 可能丢弃不支持的附加对象。可以改用 containerd image store,或直接由 Buildx 推送。

  5. 确认 Registry 和复制工具

    镜像复制、代理缓存、镜像签名同步和清理任务可能只处理主 Manifest,没有处理证明对象。

  6. 确认 subject 是否匹配

    Provenance/SBOM 指向的摘要必须等于部署摘要。tag 相同不代表 digest 相同。

  7. 确认验证器版本和格式

    不同工具可能支持不同的 in-toto predicate、OCI 关联方式和签名格式。格式不兼容不等于证明内容一定错误,但生产策略必须固定兼容矩阵。

  8. 区分缺失、无效和不合规

    • 缺失:对象没有找到;
    • 无效:对象存在但格式、绑定或签名错误;
    • 不合规:对象有效,但构建器、源码或漏洞策略不接受。

这种区分决定了恢复动作:重新推送证明、修复复制流程、重新签名,还是重新构建。

最小可行的可信链

对于 Linux 容器,较完整的一条链路可以表示为:

源码提交 digest
      │
      ▼
受信任 CI 工作流
      │
      ▼
隔离的 BuildKit builder
      │
      ├── 固定 Dockerfile
      ├── 固定基础镜像 digest
      ├── 受控构建参数
      ├── Secret mount
      └── 网络和权限限制
      │
      ▼
镜像 Index/Manifest digest D
      │
      ├── SLSA Provenance Attestation
      ├── SBOM Attestation
      └── 签名
      │
      ▼
Registry
      │
      ▼
按 digest 拉取
      │
      ▼
部署前策略验证

这条链路中的每一项都承担不同职责:

  • digest 防止 tag 漂移;
  • Provenance 记录构建因果;
  • SBOM 描述软件组成;
  • Attestation 提供标准化绑定载体;
  • SLSA 提供供应链安全的模型和要求;
  • 签名提供身份和完整性证据;
  • Registry 保存和分发对象;
  • 策略门禁决定是否允许部署。

最终,构建证明不是“给镜像加一段说明文字”,而是把“输入—构建器—过程—输出—验证策略”连接成一条可检查的证据链。只有当证明绑定到正确的镜像 digest,并且由受信任身份签名、由策略系统验证时,它才真正进入软件供应链安全控制面。


系列导航与关联阅读

官方资料

本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。