把零 CVE 变成默认状态:从镜像构建到开发机策略的供应链防线

2026-08-17 36 预计阅读时间: 1 分钟
来源: docker.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

随着供应链攻击持续增加、AI 生成代码进入更多生产系统,容器安全不能再等到发布前才集中清理。Docker 最新更新所指向的方向很明确:增加从源码构建的软件,在软件生命周期结束后继续提供安全覆盖,让定制镜像继承基础镜像的安全保证,并把策略检查前移到每台开发机。

这里的“零 CVE”更适合作为默认工作方式,而不是永久不变的承诺。漏洞数据库会更新,昨天通过扫描的镜像今天可能出现新记录。真正重要的是让构建、扫描、修复和重新发布成为持续运行的闭环。

从源码构建,减少无法解释的二进制

传统基础镜像经常包含发行版软件包、包管理器、Shell 和调试工具。它们方便排障,却也扩大了镜像的软件清单和攻击面。增加从源码构建的软件,价值不只是“版本更新”,还包括更清楚地回答几个问题:

  • 源码来自哪里,固定在哪个提交或标签?
  • 构建过程使用了什么编译器和依赖?
  • 最终镜像中究竟留下了哪些运行时文件?
  • 某个 CVE 是否真的影响镜像中的代码路径?

从源码构建并不会自动消除漏洞。编译器、模块依赖和构建脚本本身仍属于供应链。它带来的核心收益,是可追溯性以及删除非必要运行时组件的机会。

可以这样实践:用多阶段构建编译一个静态 Go 服务,最终镜像只保留应用二进制和证书。下面示例假设本机已经安装 Docker,并且构建时可以访问 Go 模块仓库。

go.mod

module example.com/zero-cve-demo

go 1.23

main.go

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
        w.Header().Set("Content-Type", "text/plain")
        fmt.Fprintln(w, "ok")
    })

    log.Println("listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Dockerfile

# 实际项目应把镜像固定到经过验证的 digest。
FROM golang:1.23-alpine AS build
WORKDIR /src

COPY go.mod ./
RUN go mod download
COPY main.go ./

RUN CGO_ENABLED=0 GOOS=linux go build \
    -trimpath \
    -ldflags="-s -w" \
    -o /out/server ./main.go

FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /out/server /server
USER 65532:65532
EXPOSE 8080
ENTRYPOINT ["/server"]

构建并验证服务:

docker build -t local/zero-cve-demo:dev .
docker run --rm -d --name zero-cve-demo -p 8080:8080 local/zero-cve-demo:dev
curl --fail http://127.0.0.1:8080/healthz
docker stop zero-cve-demo

这个例子展示的是减小运行时镜像的方法,不代表镜像天然没有漏洞。生产环境还应固定基础镜像 digest、校验源码来源、生成 SBOM,并保存构建证明。

生命周期结束后,安全责任并不会结束

很多团队仍在运行已进入生命周期末期的软件:升级可能涉及兼容性验证、数据迁移或硬件认证,无法在停止支持的当天完成。来源摘要强调,新的安全覆盖会延伸到生命周期结束之后,这能缩短“业务仍要运行,但上游已经不再修复”的暴露窗口。

不过,延长覆盖不等于取消升级计划。它更像一座过渡桥:团队获得修复关键漏洞的时间,但仍需明确最终迁移日期。采用此类覆盖时,应确认支持范围至少包含以下信息:

  • 哪些基础镜像、软件版本和 CPU 架构受到支持;
  • 修复的是所有已知 CVE,还是特定严重级别和可利用漏洞;
  • 修复如何发布,镜像 digest 和 SBOM 是否同步更新;
  • 支持结束后,工作负载必须迁移到哪个版本。

定制镜像不能成为保证失效的分界线

企业很少直接运行原始基础镜像。实际镜像通常会加入应用、配置、证书和代理组件。如果安全保证只覆盖未经修改的上游镜像,那么大部分生产工作负载仍然需要团队自行拼接证据。

Docker 更新所强调的“让保证延续到定制镜像”,关键在于保留来源关系:能够识别定制镜像继承了哪个受支持基础镜像,同时把应用层新增的软件与基础层分开评估。这也意味着团队必须避免破坏可追踪性的构建方式,例如从不受控地址下载未校验的二进制,或者反复覆盖系统库。

可以在 Dockerfile 中为远程制品加入显式校验:

ARG TOOL_VERSION=1.2.3
ARG TOOL_SHA256

ADD https://downloads.example.com/tool-${TOOL_VERSION}-linux-amd64 /usr/local/bin/tool
RUN echo "${TOOL_SHA256}  /usr/local/bin/tool" | sha256sum -c - \
    && chmod 0755 /usr/local/bin/tool

运行前需要把示例域名替换为真实制品地址,并通过构建参数传入发布方公布的 SHA-256:

docker build \
  --build-arg TOOL_SHA256='<published-sha256>' \
  -t registry.example.com/team/app:1.0.0 .

校验和只能证明下载内容与预期一致,不能证明发布方本身可信。对关键组件还应验证签名、来源证明和构建身份。

把策略放到开发机,而不是只放在 CI 末端

如果漏洞策略只在合并后的 CI 流水线执行,开发者会在提交代码很久之后才收到反馈。策略下沉到开发机后,基础镜像过期、出现高危漏洞或缺少必要元数据等问题,可以在本地构建阶段直接暴露。

如果团队使用 Docker Scout,可以这样实践一个最小本地检查脚本。具体可用参数取决于已安装的 Docker Scout 版本,扫描也可能需要登录并更新漏洞数据库。

scripts/check-image.sh

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

image="${1:?usage: scripts/check-image.sh IMAGE}"

docker scout cves "$image" \
  --only-severity critical,high \
  --exit-code

执行方式:

chmod +x scripts/check-image.sh
docker build -t local/zero-cve-demo:dev .
./scripts/check-image.sh local/zero-cve-demo:dev

同一个脚本应同时用于开发机和 CI,避免两套规则逐渐分叉。真实项目还需要定义例外机制:每个豁免都应包含负责人、原因、到期时间和补偿措施,而不是永久维护一个没有上下文的忽略列表。

落地时关注四个边界

把零 CVE 设为默认值,不应简化成“扫描结果必须显示 0”。更稳妥的采用顺序是:

  1. 固定基础镜像和构建依赖,生成 SBOM 与来源证明。
  2. 优先阻断可利用的严重漏洞,并为误报和无修复版本建立限时豁免。
  3. 确认定制镜像仍能继承基础镜像的支持与安全元数据。
  4. 在开发机、拉取请求和镜像仓库中执行同一套策略,并持续重新扫描已发布镜像。

零 CVE 是一个会随漏洞情报变化的状态,而不是一次性认证。真正成熟的目标,是让每个镜像都有可解释的来源、明确的维护期限、可重复执行的策略,以及在新漏洞出现后快速重建和替换的路径。


相关推荐