Docker 基础体系 · 第 14/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker 软件供应链:SBOM、签名、扫描、可信基础镜像和策略门禁
Docker 镜像不是一个不可分割的“软件包”。一个可运行的 Linux 容器镜像至少涉及以下对象:
- 镜像清单(image manifest)或多平台镜像索引(image index)
- 镜像配置(image config)
- 一个或多个根文件系统层(filesystem layers)
- 构建过程产生的 SBOM、来源证明(provenance)等证明材料
- 注册表中的标签、摘要、认证信息和访问控制
- 部署时使用的运行时配置、挂载、网络和 Linux 内核
因此,软件供应链安全不是“扫描一次镜像”这么简单,而是要回答一组相互关联的问题:
- 构建和部署的到底是不是同一个镜像?
- 镜像中包含了哪些软件及版本?
- 这些软件是否存在当前策略禁止的漏洞?
- 基础镜像从哪里来,是否被固定且可追溯?
- 谁构建了镜像,构建过程是否符合要求?
- 镜像是否由受信任的主体签名?
- 当任意一项不满足时,发布流程是否真的会停止?
下面以现代 Docker Engine、BuildKit、OCI 镜像格式和 Linux 容器为边界,逐步建立这些概念之间的关系。
一、先确定供应链中真正需要保护的对象
1. OCI 镜像的身份由内容摘要决定
OCI Image Specification 定义了镜像的基本对象。一个单平台镜像通常由一个 manifest 指向:
- 一个 config 对象;
- 若干 layer 对象;
- 每个对象的媒体类型和内容摘要。
摘要通常是 SHA-256,例如:
sha256:8f4c...a91e
摘要不是标签的别名,而是内容寻址标识。可以把一个镜像的可验证身份抽象为:
其中:
- 是镜像 manifest;
- 是摘要函数;
- 是 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 至少有三个边界:
- 发现边界:扫描器没有识别到的组件不会出现在 SBOM 中;
- 时间边界:SBOM 反映生成时的内容,不会自动随漏洞数据库更新;
- 对象边界:镜像 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. 签名保护的对象是什么
数字签名可以抽象为:
其中:
- 是制品摘要;
- 是签名元数据,例如主体身份、时间或证书信息;
- 是签名私钥;
- 是签名结果。
验证时使用公钥:
这只能说明“持有对应私钥的一方签署了这个对象”。它不自动说明:
- 镜像没有高危漏洞;
- 基础镜像来自可信来源;
- 构建过程没有下载恶意依赖;
- 运行时一定安全;
- 签名私钥管理得当。
签名解决的是身份和完整性问题,不直接解决质量和漏洞问题。
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. 漏洞扫描的判断对象
扫描器通常执行以下过程:
- 解析镜像层和配置;
- 识别操作系统发行版及软件包;
- 识别应用依赖;
- 将组件名称和版本映射到漏洞数据库;
- 根据修复版本、严重性和环境规则输出结果。
设组件为 ,当前版本为 ,漏洞数据库给出受影响版本区间 ,则基础判断可以写为:
但生产门禁不能只用这个结果。还需要考虑:
- 漏洞是否有修复版本;
- 是否在当前平台实际可利用;
- 应用是否真正加载该组件;
- 是否需要特定配置或输入;
- 组件是否只存在于构建阶段;
- 是否被运行时权限和网络策略隔离。
因此,扫描结果是风险证据,不是绝对的漏洞证明。
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. “可信”不是一个绝对属性
基础镜像的可信度至少包含四个不同问题:
- 来源可信:镜像由哪个组织发布;
- 内容可验证:是否固定到 digest;
- 构建可追溯:是否有来源证明和构建记录;
- 当前风险可接受:已知漏洞是否符合策略。
“官方镜像”通常意味着发布者经过某种平台或组织管理,但不表示:
- 镜像没有漏洞;
- 镜像永远不会改变;
- 镜像中的所有软件都由同一方编译;
- 镜像符合你的业务合规策略。
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
重新扫描结果
重新签名结果
可接受的更新流程是:
- 发现基础镜像发布了修复版本;
- 验证新 digest、发布者签名和平台;
- 更新 Dockerfile 或依赖锁定文件;
- 重新构建,而不是直接替换生产容器层;
- 对新的最终镜像生成 SBOM 和 provenance;
- 重新扫描;
- 重新签名;
- 通过策略后再部署新 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. 一个发布策略的形式化表达
设最终制品为 ,其 digest 为 。可以定义发布接受条件:
其中:
- :部署引用使用不可变 digest;
- :存在与该 digest 关联的 SBOM;
- :构建来源证明满足要求;
- :签名由信任集合 中的主体完成;
- :扫描结果符合漏洞策略 ;
- :所有基础镜像满足允许来源集合 。
这几个条件是合取关系。签名失败时,即使扫描通过,也必须拒绝;SBOM 缺失时,即使镜像来自官方仓库,也不应伪装成“已完成软件清单”。
2. 将门禁分层
不同检查适合放在不同阶段。
代码提交阶段
检查 Dockerfile 和依赖文件:
- 是否使用禁止的
latest; - 是否使用 digest 固定基础镜像;
- 是否把 Secret 写入
ARG、ENV或普通文件; - 是否存在未审查的远程安装脚本;
- 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 部署]
关键路径是:
- BuildKit 根据源代码、Dockerfile、依赖和基础镜像生成镜像对象;
- 镜像 manifest 或 index 产生最终 digest;
- SBOM 和 provenance 绑定到这个制品;
- 签名绑定到这个 digest;
- 扫描器针对同一个 digest 分析内容;
- 策略引擎综合所有结果;
- 部署阶段继续使用这个 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 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker 容器安全:非 root、Capabilities、Seccomp、只读根和隔离
- 下一篇:Docker Registry 与镜像分发:Tag、Digest、认证、缓存和清理
- 延伸:Docker 多阶段构建:最小运行时、依赖固定、调试层和体积优化
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论