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

Docker 软件供应链:SBOM、签名、扫描、可信基础镜像和策略门禁

Docker 镜像不是一个不可分割的“软件包”。一个可运行的 Linux 容器镜像至少涉及以下对象:

  • 镜像清单(image manifest)或多平台镜像索引(image index)
  • 镜像配置(image config)
  • 一个或多个根文件系统层(filesystem layers)
  • 构建过程产生的 SBOM、来源证明(provenance)等证明材料
  • 注册表中的标签、摘要、认证信息和访问控制
  • 部署时使用的运行时配置、挂载、网络和 Linux 内核

因此,软件供应链安全不是“扫描一次镜像”这么简单,而是要回答一组相互关联的问题:

  1. 构建和部署的到底是不是同一个镜像?
  2. 镜像中包含了哪些软件及版本?
  3. 这些软件是否存在当前策略禁止的漏洞?
  4. 基础镜像从哪里来,是否被固定且可追溯?
  5. 谁构建了镜像,构建过程是否符合要求?
  6. 镜像是否由受信任的主体签名?
  7. 当任意一项不满足时,发布流程是否真的会停止?

下面以现代 Docker Engine、BuildKit、OCI 镜像格式和 Linux 容器为边界,逐步建立这些概念之间的关系。


一、先确定供应链中真正需要保护的对象

1. OCI 镜像的身份由内容摘要决定

OCI Image Specification 定义了镜像的基本对象。一个单平台镜像通常由一个 manifest 指向:

  • 一个 config 对象;
  • 若干 layer 对象;
  • 每个对象的媒体类型和内容摘要。

摘要通常是 SHA-256,例如:

sha256:8f4c...a91e

摘要不是标签的别名,而是内容寻址标识。可以把一个镜像的可验证身份抽象为:

D=H(M)D = H(M)

其中:

  • MM 是镜像 manifest;
  • HH 是摘要函数;
  • DD 是 manifest digest。

如果 manifest 没有变化,摘要就不会变化;如果 manifest、配置或层引用发生变化,摘要通常也会变化。

标签则是注册表中的可变名称:

example/app:prod

今天它可能指向:

example/app@sha256:aaa...

明天维护者可以把它重新推送为:

example/app@sha256:bbb...

所以,以下两个引用具有不同的供应链含义:

docker pull example/app:prod
docker pull example/app@sha256:aaa...

第一个表达“取得当前 prod 标签指向的内容”,第二个表达“取得精确的内容对象”。

这也是策略门禁中“必须使用 digest”条件的基础。

2. 多平台镜像需要区分 index digest 和 platform digest

多平台镜像通常不是单个 manifest,而是一个 image index:

index
├── linux/amd64 manifest
├── linux/arm64 manifest
└── linux/arm/v7 manifest

如果使用:

docker pull alpine:3.20

Docker 会根据当前主机平台选择对应的 manifest。一个多平台标签可能对应:

alpine:3.20@sha256:index-digest

而实际拉取的 linux/amd64 manifest 又有自己的 digest。

这带来一个容易被忽略的边界:

  • 固定 index digest,可以固定“多平台发布集合”;
  • 固定平台 manifest digest,可以固定某一平台实际运行的镜像;
  • 策略必须明确验证哪一个对象。

例如生产环境只允许运行 Linux x86-64 时,应当在构建、签名和部署策略中明确 linux/amd64,而不能只检查一个没有平台约束的标签。


二、SBOM:描述镜像中有什么,但不证明它值得信任

1. SBOM 的定义

SBOM(Software Bill of Materials,软件物料清单)描述一个软件制品由哪些组件组成。对容器镜像而言,典型条目包括:

  • 操作系统发行版及其软件包;
  • 应用依赖,例如 Go module、npm package、Python package;
  • 组件版本;
  • 包管理器来源;
  • 许可证;
  • 某些情况下的文件路径、校验值和依赖关系。

SBOM 常见格式包括 SPDX 和 CycloneDX。它可以作为一个 JSON 文档,也可以作为 OCI 注册表中的关联证明材料存储。

SBOM 的核心作用是将:

“这个镜像可能包含某些依赖”

变成:

“针对这个精确镜像 digest,构建系统观察到了这些组件”。

2. 镜像 SBOM 不等于完整的软件事实

SBOM 的准确性取决于生成方式。

例如,构建阶段执行:

RUN curl -L https://example.invalid/tool.tar.gz | tar -xz

如果下载的内容没有经过包管理器安装,扫描器可能只能看到解压后的文件,未必能准确识别其上游项目和版本。

同样,以下内容通常不在静态镜像 SBOM 的完整覆盖范围内:

  • 运行时从网络下载的插件;
  • 启动后生成的配置;
  • Secret 和外部挂载卷;
  • 宿主机 Linux 内核;
  • Docker Engine、containerd、runc 等宿主侧组件;
  • 运行时实际注入的环境变量;
  • 外部数据库和云服务依赖。

因此,SBOM 至少有三个边界:

  1. 发现边界:扫描器没有识别到的组件不会出现在 SBOM 中;
  2. 时间边界:SBOM 反映生成时的内容,不会自动随漏洞数据库更新;
  3. 对象边界:镜像 SBOM 描述用户空间文件,不等于整个运行环境的 SBOM。

3. 使用 BuildKit 生成 SBOM 和构建证明

现代 BuildKit 可以在构建时生成 SBOM 和 provenance(构建来源证明)。一个典型构建命令是:

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

各参数的含义是:

  • --platform linux/amd64:明确构建目标平台;
  • --sbom=true:请求构建器生成 SBOM;
  • --provenance=mode=max:请求记录更完整的构建来源信息;
  • --push:将镜像及相关证明材料推送到注册表。

BuildKit 版本、Docker Desktop/Engine 集成方式和注册表能力会影响证明材料的展示与存储。OCI 规范定义了镜像对象和关联对象的基本模型,但它并不规定“某个扫描器必须如何生成 SBOM”,也不规定“所有注册表必须以相同 UI 展示证明材料”。

构建完成后,可以先查看镜像摘要:

docker buildx imagetools inspect \
  registry.example.com/acme/app:1.4.2

输出通常会包含 manifest 或 index 的 digest,以及平台信息。生产流程应记录这个 digest,而不是只记录标签。

需要注意:SBOM 是对镜像的描述,不能证明镜像未被篡改。即使攻击者构建了一个带恶意程序的镜像,也可以同时生成一份“内容准确但不可信”的 SBOM。


三、签名:证明某个主体认可某个精确制品

1. 签名保护的对象是什么

数字签名可以抽象为:

σ=Signsk(DM)\sigma = Sign_{sk}(D \parallel M)

其中:

  • DD 是制品摘要;
  • MM 是签名元数据,例如主体身份、时间或证书信息;
  • sksk 是签名私钥;
  • σ\sigma 是签名结果。

验证时使用公钥:

Verifypk(DM,σ)=trueVerify_{pk}(D \parallel M, \sigma) = true

这只能说明“持有对应私钥的一方签署了这个对象”。它不自动说明:

  • 镜像没有高危漏洞;
  • 基础镜像来自可信来源;
  • 构建过程没有下载恶意依赖;
  • 运行时一定安全;
  • 签名私钥管理得当。

签名解决的是身份和完整性问题,不直接解决质量和漏洞问题。

2. Cosign 的基本流程

Cosign 是常见的 OCI 制品签名工具。下面使用密钥签名演示完整流程。

生成测试密钥:

cosign generate-key-pair

命令会要求设置密钥口令,并生成私钥和公钥。私钥只能放在受控的签名环境中,不能提交到 Git 仓库,也不应放入普通构建容器。

先取得实际 digest:

IMAGE=registry.example.com/acme/app
TAG=1.4.2

docker buildx imagetools inspect "$IMAGE:$TAG"

假设输出中的目标 digest 是:

sha256:1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff

构造不可变引用:

DIGEST=sha256:1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff
REF="$IMAGE@$DIGEST"

签名:

cosign sign --key cosign.key "$REF"

验证:

cosign verify --key cosign.pub "$REF"

验证必须针对 digest,而不是仅针对标签:

cosign verify --key cosign.pub "$IMAGE:1.4.2"

这条命令的风险在于:标签可能已经被重新指向另一个对象。即使工具能够解析标签,策略也应进一步确认签名对应的实际 digest。

Cosign 通常将签名作为与镜像关联的 OCI 制品存储。注册表需要正确支持相关对象和访问权限;某些旧注册表、代理或镜像同步系统可能只复制镜像 manifest 和 layer,遗漏签名、SBOM 或 provenance。

3. 密钥签名与无密钥签名

密钥签名的信任根是公钥:

生产验证器 -> 固定的公钥 -> 验证镜像签名

它的主要风险是公钥分发和私钥保护。

无密钥签名通常使用短期身份凭证和 OIDC 身份,并依赖证书透明日志等系统记录签名。验证策略不能只写“签名有效”,而必须限制:

  • 允许的身份;
  • 允许的 OIDC issuer;
  • 允许的构建工作流或仓库;
  • 允许的组织和分支;
  • 证书有效期和撤销策略。

例如,生产环境不应接受“任意 GitHub 用户签署的镜像”,而应接受某个组织、某个仓库、某个发布工作流签署的镜像。具体命令和参数取决于所使用的签名系统和版本,不能把“存在签名”误认为“满足身份策略”。


四、扫描:把 SBOM 与漏洞数据库连接起来

1. 漏洞扫描的判断对象

扫描器通常执行以下过程:

  1. 解析镜像层和配置;
  2. 识别操作系统发行版及软件包;
  3. 识别应用依赖;
  4. 将组件名称和版本映射到漏洞数据库;
  5. 根据修复版本、严重性和环境规则输出结果。

设组件为 cic_i,当前版本为 viv_i,漏洞数据库给出受影响版本区间 RiR_i,则基础判断可以写为:

Vi={1,viRi0,viRiV_i = \begin{cases} 1, & v_i \in R_i \\ 0, & v_i \notin R_i \end{cases}

但生产门禁不能只用这个结果。还需要考虑:

  • 漏洞是否有修复版本;
  • 是否在当前平台实际可利用;
  • 应用是否真正加载该组件;
  • 是否需要特定配置或输入;
  • 组件是否只存在于构建阶段;
  • 是否被运行时权限和网络策略隔离。

因此,扫描结果是风险证据,不是绝对的漏洞证明。

2. 在构建后针对精确 digest 扫描

可以使用 Syft 生成独立 SBOM:

syft "$REF" -o spdx-json > sbom.spdx.json

输入是带 digest 的镜像引用,输出是 SPDX JSON 文件。针对 digest 扫描可以使用 Grype:

grype "$REF"

在组织策略要求高危漏洞阻断时,可以使用退出码门禁:

grype "$REF" --fail-on high

通常约定:

  • 扫描完成且不超过策略阈值时退出码为 0;
  • 发现达到阈值的漏洞时返回非 0;
  • CI 将非 0 视为失败。

完整的最小门禁片段如下:

#!/usr/bin/env bash
set -euo pipefail

REF="${1:?usage: $0 REGISTRY/IMAGE@DIGEST}"

echo "Verifying signature: $REF"
cosign verify --key cosign.pub "$REF"

echo "Scanning vulnerabilities: $REF"
grype "$REF" --fail-on high

echo "Supply-chain checks passed: $REF"

这里的顺序并不意味着签名验证比扫描更重要。它只是先确认扫描对象确实是预期制品,再对同一个 digest 执行扫描,避免出现“签名检查一个标签、扫描另一个重新解析结果”的对象不一致问题。

3. 为什么扫描结果会变化

同一个镜像 digest 在不同日期扫描,结果可能不同,因为漏洞数据库会新增记录或修正影响范围:

T1:没有 CVE-XXXX-YYYY 记录 -> 通过
T2:数据库新增记录 -> 失败
T3:上游发布修复版本,镜像未重建 -> 仍失败

镜像内容没有变化,但风险知识发生了变化。因此应区分:

  • 发布时门禁:阻止明显不合规的新构建;
  • 持续扫描:重新扫描仓库中已经发布的 digest;
  • 运行中清单:将生产部署的 digest 与新漏洞事件关联。

扫描数据库也存在误报和漏报。一个典型误报是:

  • 软件包版本处于漏洞影响区间;
  • 但发行版维护者已经通过 backport 修复;
  • 扫描器没有正确读取发行版修订号。

反过来,只有文件名而没有包元数据时,扫描器也可能无法识别真实版本。修复流程应回到发行版包、上游依赖或基础镜像更新,而不是简单删除扫描报告中的文件。


五、可信基础镜像:信任必须可验证、可更新、可回溯

1. “可信”不是一个绝对属性

基础镜像的可信度至少包含四个不同问题:

  1. 来源可信:镜像由哪个组织发布;
  2. 内容可验证:是否固定到 digest;
  3. 构建可追溯:是否有来源证明和构建记录;
  4. 当前风险可接受:已知漏洞是否符合策略。

“官方镜像”通常意味着发布者经过某种平台或组织管理,但不表示:

  • 镜像没有漏洞;
  • 镜像永远不会改变;
  • 镜像中的所有软件都由同一方编译;
  • 镜像符合你的业务合规策略。

OCI Image Specification 本身定义内容格式和描述关系,不定义“官方”“可信发布者”这样的安全结论。信任模型必须由组织通过签名根、公钥、身份规则和允许的注册表来建立。

2. 固定基础镜像 digest

不应只写:

FROM alpine:3.20

因为 alpine:3.20 仍然是可变引用。更明确的写法是:

FROM alpine:3.20@sha256:已验证的镜像摘要

摘要不能凭名称猜测,必须从受信任来源取得并记录。例如先查看远程镜像的 manifest:

docker buildx imagetools inspect alpine:3.20

然后人工或自动化选择目标平台的 manifest digest,并将它写入 Dockerfile 或生成构建参数。

一种适合多阶段构建的结构是:

# syntax=docker/dockerfile:1

ARG BUILD_IMAGE
ARG RUNTIME_IMAGE

FROM ${BUILD_IMAGE} AS build
WORKDIR /src

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

COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/app

FROM ${RUNTIME_IMAGE}
COPY --from=build /out/app /usr/local/bin/app
USER 65532:65532
ENTRYPOINT ["/usr/local/bin/app"]

构建时显式传入已经验证过的引用:

docker buildx build \
  --build-arg BUILD_IMAGE=golang:1.23-alpine@sha256:构建镜像摘要 \
  --build-arg RUNTIME_IMAGE=alpine:3.20@sha256:运行时镜像摘要 \
  --platform linux/amd64 \
  --sbom=true \
  --provenance=mode=max \
  --tag registry.example.com/acme/app:1.4.2 \
  --push .

这里的两个基础镜像不一定要相同。构建镜像包含编译器和开发工具,运行时镜像只保留执行程序及其必要的用户空间依赖。这正是多阶段构建降低运行时攻击面的作用。

但“使用 scratch”也不是自动更安全。若程序需要 CA 证书、时区数据、非 root 用户或动态链接库,盲目切换到 scratch 可能导致:

  • HTTPS 请求失败;
  • 时间格式化错误;
  • 进程无法解析用户和组;
  • 二进制启动时报缺少共享库。

最小运行时必须通过实际测试和 SBOM 验证,而不是仅凭镜像大小判断安全性。

3. 基础镜像更新必须保留变更关系

固定 digest 防止标签漂移,但也会阻止自动获得安全修复。供应链流程需要同时保存:

旧基础镜像 digest
新基础镜像 digest
变更原因
重新构建后的应用 digest
重新扫描结果
重新签名结果

可接受的更新流程是:

  1. 发现基础镜像发布了修复版本;
  2. 验证新 digest、发布者签名和平台;
  3. 更新 Dockerfile 或依赖锁定文件;
  4. 重新构建,而不是直接替换生产容器层;
  5. 对新的最终镜像生成 SBOM 和 provenance;
  6. 重新扫描;
  7. 重新签名;
  8. 通过策略后再部署新 digest。

六、构建供应链不只包括 Dockerfile

即使最终镜像使用了可信基础镜像,构建过程仍然可能引入风险。

1. Dockerfile 中的常见供应链入口

以下命令会把外部供应链引入构建:

RUN apt-get update && apt-get install -y curl
RUN npm install
RUN pip install -r requirements.txt
RUN curl -fsSL https://example.invalid/install.sh | sh

风险包括:

  • 依赖标签或版本没有锁定;
  • 包索引在不同时间返回不同版本;
  • 下载地址遭到替换;
  • 安装脚本执行了未审查命令;
  • 构建缓存保留了旧的脆弱依赖;
  • 私有依赖凭证泄露到层或构建日志。

依赖固定通常需要同时固定:

  • 包版本;
  • lockfile;
  • 包仓库或镜像源;
  • 必要时的包文件校验和;
  • 基础镜像 digest。

例如,只写:

RUN pip install flask

并不表示可重复构建。更接近可复现的方式是提交锁定文件,并在构建中使用:

COPY requirements.txt requirements.lock ./
RUN pip install --no-cache-dir \
    --require-hashes \
    -r requirements.lock

--require-hashes 要求锁定文件包含包文件哈希,从而拒绝只凭名称和版本取得任意内容。具体参数应以所用包管理器和版本为准,不能把“锁定版本”与“校验下载内容”混为一谈。

2. Secret 不应通过 ARG 或 ENV 传入

以下写法会把敏感信息暴露给构建历史、配置或后续诊断:

ARG NPM_TOKEN
RUN npm install

现代 BuildKit 支持 secret mount,示例:

# syntax=docker/dockerfile:1

FROM node:22-alpine 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" \
  --tag registry.example.com/acme/web:1.0.0 \
  --push .

Secret mount 的关键性质是:凭证只在对应 RUN 步骤的临时挂载中可见,不应成为镜像层内容。它不能防止恶意构建脚本在执行期间读取并外传凭证,因此构建器本身和依赖脚本仍需受到信任。


七、策略门禁:把“安全要求”变成可执行条件

1. 一个发布策略的形式化表达

设最终制品为 II,其 digest 为 DD。可以定义发布接受条件:

Accept(I)=Pin(D)SBOM(I)Provenance(I)Signature(I,T)Scan(I,P)Base(I,B)Accept(I) = Pin(D) \land SBOM(I) \land Provenance(I) \land Signature(I, T) \land Scan(I, P) \land Base(I, B)

其中:

  • Pin(D)Pin(D):部署引用使用不可变 digest;
  • SBOM(I)SBOM(I):存在与该 digest 关联的 SBOM;
  • Provenance(I)Provenance(I):构建来源证明满足要求;
  • Signature(I,T)Signature(I,T):签名由信任集合 TT 中的主体完成;
  • Scan(I,P)Scan(I,P):扫描结果符合漏洞策略 PP
  • Base(I,B)Base(I,B):所有基础镜像满足允许来源集合 BB

这几个条件是合取关系。签名失败时,即使扫描通过,也必须拒绝;SBOM 缺失时,即使镜像来自官方仓库,也不应伪装成“已完成软件清单”。

2. 将门禁分层

不同检查适合放在不同阶段。

代码提交阶段

检查 Dockerfile 和依赖文件:

  • 是否使用禁止的 latest
  • 是否使用 digest 固定基础镜像;
  • 是否把 Secret 写入 ARGENV 或普通文件;
  • 是否存在未审查的远程安装脚本;
  • lockfile 是否与依赖声明一致。

这类检查速度快,但只能分析声明,不能证明最终镜像内容。

构建阶段

BuildKit 生成最终镜像、SBOM 和 provenance。构建器应使用受控的网络、凭证和构建节点,并记录:

  • 源代码版本;
  • Dockerfile;
  • 基础镜像摘要;
  • 构建参数;
  • 目标平台;
  • 构建器身份。

注册表阶段

推送后执行:

cosign verify --key cosign.pub "$REF"
grype "$REF" --fail-on high

同时检查:

  • SBOM 是否与同一 digest 关联;
  • 签名主体是否符合组织规则;
  • provenance 是否来自允许的 CI 工作流;
  • 目标平台是否正确。

部署阶段

Compose 或编排系统应使用 digest:

services:
  app:
    image: registry.example.com/acme/app@sha256:1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff
    read_only: true
    cap_drop:
      - ALL

部署工具需要在拉取或启动前完成签名和策略验证。仅仅在 CI 中签名,不能阻止运维人员后来手工部署另一个未签名标签。

如果使用 Compose 的 build 配置生成 SBOM 或 provenance,应确认当前 Docker Compose、BuildKit 和注册表版本支持相应字段。Compose 规范描述配置模型,具体能力仍取决于实现版本;生产策略不应只根据 YAML 是否能解析来判断证明材料是否确实生成。

3. 一个可执行的最小门禁

下面的脚本验证签名并阻断高危漏洞:

#!/usr/bin/env bash
set -euo pipefail

if [ "$#" -ne 1 ]; then
  echo "usage: $0 REGISTRY/IMAGE@DIGEST" >&2
  exit 2
fi

REF="$1"

case "$REF" in
  *@sha256:*) ;;
  *)
    echo "拒绝:镜像引用必须包含 digest:$REF" >&2
    exit 1
    ;;
esac

cosign verify --key cosign.pub "$REF"

grype "$REF" --fail-on high

echo "accepted: $REF"

它明确拒绝:

./gate.sh registry.example.com/acme/app:prod

并要求:

./gate.sh \
  registry.example.com/acme/app@sha256:1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff

但这个脚本仍不是完整生产策略,因为它没有解析 provenance 身份,也没有严格检查 SBOM 是否已作为 OCI 关联对象上传。SBOM 存在性应通过构建系统、注册表 API、Docker Scout、Notary Project 工具链或组织自己的制品元数据服务验证,而不是仅仅依赖“扫描器可以重新生成一份 SBOM”。


八、OCI、签名和证明材料之间的关系

可以把一次构建后的制品关系表示为:

flowchart LR
    S[源代码与 lockfile] --> B[BuildKit 构建器]
    B --> M[镜像 manifest 或 image index]
    B --> L[镜像 layers]
    B --> C[镜像 config]
    B --> SBOM[SBOM attestation]
    B --> PROV[Provenance attestation]

    M --> D[镜像 digest]
    D --> SIG[签名]
    D --> SCAN[漏洞扫描]
    D --> POLICY[策略门禁]
    SBOM --> POLICY
    PROV --> POLICY
    SIG --> POLICY
    SCAN --> POLICY
    POLICY --> DEPLOY[按 digest 部署]

关键路径是:

  1. BuildKit 根据源代码、Dockerfile、依赖和基础镜像生成镜像对象;
  2. 镜像 manifest 或 index 产生最终 digest;
  3. SBOM 和 provenance 绑定到这个制品;
  4. 签名绑定到这个 digest;
  5. 扫描器针对同一个 digest 分析内容;
  6. 策略引擎综合所有结果;
  7. 部署阶段继续使用这个 digest。

OCI Image Specification 保证的是对象表示、引用和摘要关系。OCI Runtime Specification 则描述容器运行时如何根据 bundle、配置和 root filesystem 创建运行环境。两者都不自动保证:

  • 镜像来自可信主体;
  • 容器只能使用签名镜像;
  • 运行时不会挂载恶意宿主目录;
  • Linux 内核没有漏洞;
  • 容器进程具有最小权限。

因此,签名校验和运行时隔离属于不同层次。一个已签名镜像仍可能以高权限启动:

docker run --privileged ...

也可能挂载宿主机敏感路径:

docker run -v /:/host ...

这已经超出镜像签名本身的保护范围,必须由运行时策略、主机加固和编排系统控制。


九、失败路径与诊断方法

1. 标签已更新,部署内容与测试不同

表现:

CI 测试通过
生产按 :prod 拉取后行为不同

诊断:

docker buildx imagetools inspect registry.example.com/acme/app:prod
docker image inspect registry.example.com/acme/app:prod

应比较 CI 记录的 digest 与生产节点实际使用的 RepoDigest。恢复方法是回滚到已知 digest,而不是把标签重新推回旧内容。

2. 签名验证失败,但镜像可以正常运行

常见原因:

  • 签名对应另一个 digest;
  • 使用了错误的公钥;
  • 注册表同步时丢失了签名对象;
  • 签名主体不在允许身份集合;
  • 多平台 index 与单平台 manifest 验证对象不一致。

处理时应先打印并确认完整引用:

docker buildx imagetools inspect "$IMAGE:$TAG"
cosign verify --key cosign.pub "$IMAGE@$DIGEST"

不要为了让部署继续而关闭签名检查。应修复制品关联、注册表同步或信任配置。

3. 扫描突然失败

需要区分三种情况:

  • 镜像内容确实包含受影响版本;
  • 漏洞数据库更新导致原有 digest 重新被识别;
  • 扫描器误判发行版 backport 或包版本。

诊断应保存:

  • 镜像 digest;
  • 扫描器版本;
  • 漏洞数据库版本或更新时间;
  • 命中的包名、版本、来源和修复版本;
  • 是否只存在于构建阶段。

如果漏洞只存在于构建阶段,多阶段构建可能已经将其排除;如果漏洞存在于最终运行时层,则不能仅因为构建镜像已删除而关闭告警。

4. SBOM 存在,但策略仍然拒绝

SBOM 必须与最终 digest 绑定。以下情况不能视为满足要求:

  • 只上传了镜像仓库根目录下的一个独立 JSON;
  • SBOM 针对的是构建中间镜像;
  • SBOM 针对标签生成,但标签后来移动;
  • SBOM 由部署节点临时生成,却没有保留审计记录;
  • 注册表只保存镜像层,没有保存关联证明对象。

诊断重点是确认:

SBOM.subject.digest == deployed_image.digest

而不是只检查“是否存在一个名为 sbom.json 的文件”。


十、常见误解与真实边界

误解一:签名镜像就是安全镜像

签名只能证明某个主体签署了某个对象。受信任主体可能误发布含漏洞的软件,也可能其构建环境被入侵。因此需要把签名、provenance、SBOM 和扫描结合起来。

误解二:有 SBOM 就可以发现所有漏洞

SBOM 取决于识别能力,并且只描述生成时观察到的制品。动态下载、宿主机内核和外部服务仍需单独管理。

误解三:固定 digest 后就不需要扫描

Digest 提供可重复获取和完整性保护,但不改变镜像本身的漏洞状态。一个完全固定的旧 digest 仍可能包含多年未修复的漏洞。

误解四:镜像越小,供应链风险越低

体积优化可以减少组件数量,但不能替代来源验证。一个很小的恶意二进制仍然具有完整风险;一个较大的官方镜像也可能更容易审计和更新。

误解五:Docker Engine 能替代 Linux 主机安全

Linux 容器共享宿主机内核。镜像扫描通常不能发现宿主机内核漏洞、Docker socket 暴露、错误的 capabilities、过度宽松的 seccomp 配置或危险 bind mount。这些属于运行时和主机边界。


十一、生产取舍:严格性、可用性和恢复能力

漏洞门禁需要明确例外规则,否则团队通常会在事故时直接关闭门禁。一个合格的例外至少包含:

  • 具体镜像 digest;
  • 具体漏洞编号;
  • 风险判断和受影响路径;
  • 临时补偿措施;
  • 批准人;
  • 到期时间;
  • 到期后的重新评估动作。

策略也不应只按 CVSS 分数机械阻断。可以将“严重性、是否有修复版本、是否暴露网络、是否运行在高权限、是否实际加载”组合评估。例如,一个只存在于构建阶段的开发工具漏洞,与运行时可远程触发的解析库漏洞,不应自动视为同一风险。

最后,供应链的恢复能力与验证能力同样重要。生产环境应保留:

  • 已部署的完整 image digest;
  • 对应 SBOM、provenance 和签名记录;
  • 基础镜像 digest;
  • 构建日志和源代码版本;
  • 扫描时使用的数据库版本;
  • 可重新拉取的制品或可信缓存。

这样在标签被覆盖、注册表故障、漏洞数据库更新或签名系统迁移时,仍可以回答“生产运行的到底是什么”,并按 digest 回滚到已经验证过的对象。

Docker 软件供应链的核心不是给镜像贴上更多标签,而是让每一个安全判断都绑定到同一个不可变制品:SBOM 描述它包含什么,扫描评估已知风险,签名证明谁认可它,provenance 说明它如何产生,可信基础镜像限制输入来源,策略门禁则决定它能否进入下一阶段。


系列导航与关联阅读

官方资料

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