镜像供应链安全防护的关键,在于把镜像当成一条完整的生产链路来治理,而不是只关注仓库登录或运行时漏洞。一个典型的攻击路径是:攻击者先在公共基础镜像中植入后门,或者把恶意依赖包上传到开发者常用的包源,然后利用 CI 系统自动构建,最终把带有恶意代码的镜像推送到内部仓库。如果集群在拉取镜像时只校验仓库地址和凭证,而不校验镜像摘要、签名和内容,攻击者就能长时间潜伏在业务容器中。因此,镜像供应链需要同时解决来源可信、构建可信、漏洞可见、运行可拦截这四个问题。

另一个容易被忽视的问题是标签漂移。很多人把 latest 当成稳定的版本号使用,这其实埋下了很大隐患。latest 是可变标签,基础镜像维护者每一次推送都会改变其指向,内部仓库也可能因为误操作覆盖已有标签。要稳定复现构建和回滚,最好在 Dockerfile 中使用摘要 digest 固定基础镜像,并让 CI 记录每次构建使用的完整引用。
一、先拆解镜像供应链的主要风险面
要建立有效防护,首先得清楚风险集中在哪些位置。上游基础镜像是最常见的问题来源。公共镜像仓库中有大量同名镜像,普通开发者很难分辨一个 nginx 镜像是否来自官方,是否被篡改过。即便基础镜像本身可信,它携带的底层操作系统和运行时组件也可能存在已知漏洞。例如一个半年前构建的 Node.js 镜像,可能包含多个高危系统库漏洞,如果继续用标签拉取而不更新摘要,CI 每次构建都会继承这些风险。
构建依赖和工具链同样危险。包管理器从远程源下载依赖时,如果源被投毒或 DNS 被劫持,构建产物就可能被植入恶意代码。Dockerfile 中常见的 curl http://xxx | bash 一类操作,等于把远程脚本当成任意代码执行,脚本内容一旦变化,镜像内容也随之变化,极难审计。CI runner 的缓存目录、共享数据卷、构建参数和构建环境变量,也可能被之前的任务污染。
仓库与分发环节的风险也不能忽略。如果镜像仓库的权限模型过粗,多个团队共用同一个写权限账号,就很容易出现标签被覆盖、镜像被替换的情况。Kubernetes 默认允许节点从多个仓库拉取镜像,如果集群策略未限制仓库白名单,攻击者可能诱导调度器拉取外部恶意镜像。下面这个 Dockerfile 片段演示了如何用摘要固定基础镜像,减少上游标签变化带来的不确定性:
# 使用固定摘要的基础镜像,避免 latest 漂移 FROM alpine:3.18@sha256:e2e16842c9b54c54f3e55556d7b64bcbd6d8b0a3f1e5f5a0e5f8c6f0b2f9b3e5 AS build RUN apk add --no-cache ca-certificates COPY . /src WORKDIR /src RUN go build -o /app . FROM gcr.io/distroless/static-debian12:nonroot COPY --from=build /app /app USER nonroot:nonroot ENTRYPOINT ["/app"]
二、用签名和来源证明建立信任链
固定的摘要只能保证拉取的内容不变,但不能回答这个镜像是否由可信的构建系统生成。签名正是用来解决来源认证问题。签名机制会把镜像的 manifest 摘要与某个密钥或身份绑定,验证时可以用公钥或 OIDC 身份确认镜像未被第三方篡改。Docker 早期的 Docker Content Trust 使用 Notary 实现,但部署复杂。现在更常用的是 Cosign,它能直接对 OCI 镜像签名,也支持无密钥签名,把 CI 系统的身份作为信任锚点。
在 CI 中启用签名并不复杂。构建并推送镜像后,使用私钥对镜像签名。私钥必须妥善保管,推荐放在 KMS 或 CI 的加密 secret 中,避免提交到代码仓库。验证端只需持有公钥即可。以下命令展示了 Cosign 生成密钥、签名和验证的基本流程:
cosign generate-key-pair cosign sign --key cosign.key registry.ipipp.com/team/app:v1.2.0 cosign verify --key cosign.pub registry.ipipp.com/team/app:v1.2.0
除了对镜像整体签名,还可以附加证明文件,例如 SBOM 物料清单、构建来源证明和漏洞扫描结果。Sigstore 提供的无密钥签名会临时生成短期证书,并通过 OIDC 身份认证关联到 GitHub Actions 等工作流。这样就不需要管理长期私钥,降低密钥泄露风险。管理员可以在准入控制中要求镜像必须同时具备有效签名和合规的 SBOM,从策略层面把不可信镜像挡在集群之外。
镜像签名不是一次性的动作,而应该嵌入发布流水线。每次构建完成后自动签名,发布前自动验证,并且把签名记录存档。对于回滚场景,同样要验证旧版本镜像的签名是否仍然有效。否则攻击者可能用一个未签名的旧镜像替换当前版本,绕过新版本的修复。
三、在 CI 中做漏洞扫描和 SBOM 生成
签名的镜像只能说明它没有被篡改,但不能说明它没有漏洞。漏洞扫描需要作为 CI 的必要步骤,在推送镜像到生产仓库前完成。扫描器会解析镜像内部的系统包、语言依赖和二进制文件,与漏洞库比对,输出漏洞等级和修复建议。Trivy、Grype 等工具都能直接扫描 OCI 镜像,速度快,适合放在流水线中。
扫描策略要与业务风险对齐。一般可以设置阻断规则:只要存在严重或高危漏洞就停止发布,中低危漏洞可以记录并安排修复。对于有固定修复版本但暂时无法升级的漏洞,可以结合实际运行环境评估可利用性,而不是只看漏洞数量。扫描命令可以很简单:
trivy image --severity HIGH,CRITICAL --ignore-unfixed registry.ipipp.com/team/app:v1.2.0 syft registry.ipipp.com/team/app:v1.2.0 -o spdx-json --file app-sbom.json cosign attest --predicate app-sbom.json --key cosign.key registry.ipipp.com/team/app:v1.2.0
SBOM 的作用在漏洞披露后更突出。一旦某个基础库爆发漏洞,团队可以通过 SBOM 快速检索哪些镜像包含了受影响组件,而不必逐个人工排查。把 SBOM 作为签名附件保存,还能保证物料清单本身的完整性。建议在 CI 中同时生成 SPDX 或 CycloneDX 格式的 SBOM,并将结果存储到制品仓库或安全平台,形成可持续追踪的资产台账。
四、镜像最小化与运行时准入控制
镜像越小,携带的组件越少,受攻击面也越小。一个完整的 Ubuntu 镜像可能包含数百个系统包,而 distroless 或 scratch 镜像几乎没有 shell 和包管理器,攻击者入侵后也难以横向扩展。多阶段构建是压缩镜像体积的常用手段:第一阶段用完整工具链编译产物,第二阶段只拷贝运行时需要的二进制和证书。比如下面这个 Go 服务的多阶段构建示例:
FROM golang:1.22 AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /app . FROM gcr.io/distroless/static-debian12:nonroot COPY --from=build /app /app USER nonroot:nonroot ENTRYPOINT ["/app"]
把镜像发布到仓库后,还需要在 Kubernetes 侧执行运行时策略。仅靠开发团队自觉维护签名和扫描是不够的,必须有准入控制器强制校验。Kyverno、OPA 等工具可以在创建 Pod 时检查镜像摘要、签名状态、来源仓库,甚至根据漏洞扫描结果决定是否放行。下面是一个 Kyverno 策略,要求所有 Pod 的镜像必须带有可验证的无密钥签名:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
validationFailureAction: Enforce
rules:
- name: check-image-signature
match:
resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "*"
attestors:
- count: 1
entries:
- keyless:
subject: "https://ipipp.com/team/app"
issuer: "https://token.actions.githubusercontent.com"
运行时策略还可以限制镜像仓库白名单,禁止使用 latest 标签,要求镜像 ID 与签名时一致。即便攻击者拿到了 CI 凭证,如果无法通过签名和扫描策略,恶意镜像也无法部署。真正落地时要关注策略的迭代过程:先以审计模式观察一段时间,确认误报范围后逐步切换为强制模式,避免阻断正常发布。
镜像供应链安全防护不是一次性加固,而是一套持续运转的工程体系。上游依赖在变化,漏洞库在更新,CI 环境和密钥也可能发生轮换。团队需要定期复盘策略覆盖率,检查签名密钥和扫描规则是否过期,并把镜像来源、签名记录和 SBOM 都纳入审计范围。只有在构建、签名、扫描和运行四个阶段都形成闭环,才能把镜像供应链的风险降到可控水平。