容器镜像的漏洞问题往往不是业务代码引起的,而是来自基础操作系统包、语言运行时和构建阶段引入的依赖。镜像构建完成后立刻执行漏洞扫描,把结果作为后续发布的前置条件,是 DevSecOps 落地中成本较低但收益很高的一步。下面围绕扫描工具选型、CI 接入方式和修复闭环展开。

一、为什么必须把扫描左移到构建阶段
镜像由多个只读层叠加而成。开发者在 Dockerfile 中写下的 FROM 指令,往往直接决定了一半以上的攻击面。像 openssl、curl、glibc、systemd 这类系统组件一旦被爆出高危漏洞,即使应用代码没有变化,已经构建好的镜像也会变成风险载体。等镜像进入生产集群后再由安全团队扫描,通常已经过去数天甚至数周,攻击窗口被拉长。
把扫描放进 CI 构建之后,可以让漏洞信息在镜像推送到仓库之前就暴露出来。流水线只需要在 build 阶段生成镜像,在 scan 阶段调用扫描器,如果高危或严重漏洞数量超过阈值就直接退出,后续 deploy 阶段不会执行。这样开发人员提交代码后几分钟内就能看到失败原因,而不是等到发布评审时才被拦截。
左移不只提升响应速度,也把安全责任从单一安全团队分散到开发团队。扫描器每次运行都基于最新漏洞库,基础镜像是否过期会在构建阶段持续暴露,团队自然会形成及时升级依赖和基础镜像的习惯。
二、常见镜像漏洞扫描工具与选型
目前开源社区比较常用的扫描器有 Trivy、Grype 和 Clair。Trivy 由 Aqua Security 维护,安装方式简单,单个二进制文件即可运行,支持容器镜像、文件系统、Git 仓库和 Terraform 等目标。它的扫描速度较快,适合嵌入 CI 流程。
Grype 来自 Anchore,常与同一团队的 Syft 配合使用。Syft 负责生成 SBOM,Grype 再基于 SBOM 做漏洞匹配。这种组合适合需要软件物料清单、且希望把扫描范围从镜像层扩展到源码依赖的场景。Clair 则是 Quay 项目背后的静态分析引擎,很多私有镜像仓库会内置 Clair 做批量扫描,适合已把 Harbor 或 Quay 作为镜像中心的团队。
| 工具 | 核心优势 | 适用场景 |
|---|---|---|
| Trivy | 安装简单,扫描快,支持多目标 | CI 流水线、本地排查 |
| Grype | 与 Syft 联动,SBOM 友好 | 软件成分分析、细粒度清单 |
| Clair | 静态分析能力强,与 Quay 和 Harbor 集成 | 镜像仓库批量扫描 |
工具选型不必追求功能最全。刚开始接入时,重要的是降低使用成本,让扫描稳定跑通。Trivy 的命令行参数清晰,CI 镜像也不大,是多数团队的起点。如果后续需要统一管理多仓库镜像,可以再引入 Harbor 加 Clair 或 Trivy 的集中式扫描。
三、在 GitLab CI 中接入并配置门禁
下面以 GitLab CI 和 Trivy 为例,展示一个最精简的三阶段流水线。build 阶段负责构建并推送镜像,scan 阶段对刚构建的镜像执行漏洞扫描,只有扫描通过才会进入 deploy 阶段。扫描命令设置 --exit-code 1,当发现 HIGH 或 CRITICAL 级别漏洞时返回非零退出码,GitLab Runner 会将任务标记为失败。
stages:
- build
- scan
- deploy
build_image:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
scan_image:
stage: scan
image: aquasec/trivy:0.50.0
variables:
TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db
script:
- trivy image --exit-code 1 --severity HIGH,CRITICAL --no-progress $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
allow_failure: false
deploy:
stage: deploy
script:
- echo "deploy after scan passed"
only:
- main
这里需要注意的是 allow_failure 必须设为 false,否则漏洞扫描失败后流水线仍然会继续,门禁就形同虚设。变量 TRIVY_DB_REPOSITORY 用来指定漏洞库下载源,如果流水线运行在无法直连公网的环境,可以提前准备内网漏洞库镜像。
如果团队暂时不想让所有高危漏洞都阻断构建,可以用 --exit-code 0 先收集数据,再把结果输出为 JSON 报告。也可以使用 .trivyignore 文件精确忽略某些暂时无法修复的 CVE。
trivy image --format json --output trivy-report.json myapp:latest trivy image --exit-code 0 --severity HIGH,CRITICAL myapp:latest
白名单文件的用法非常简单,每行写入一个 CVE 编号即可。不过白名单应当有审批记录,不能随意追加,否则扫描门禁会逐渐失效。
CVE-2023-1234 CVE-2024-5678
四、用 SBOM 和基础镜像升级形成修复闭环
扫描失败只是开始。真正有价值的工作是把漏洞清单转成可执行任务。生成 SBOM 可以让团队知道镜像里到底装了什么,漏洞落在哪个包、哪个版本上。Syft 可以直接从镜像生成 CycloneDX 或 SPDX 格式的物料清单,方便归档和审计。
syft myapp:latest -o cyclonedx-json > sbom.json
修复镜像漏洞最直接的方法是升级基础镜像。如果 Dockerfile 固定使用某个旧版本标签,即使重新构建也无法获取新的安全补丁。更推荐使用可被重新解析的标签,例如 node:20 或 debian:12-slim,并在构建时定期拉取最新补丁。
进一步的优化是采用多阶段构建和精简基础镜像。编译阶段保留完整工具链,运行阶段只复制编译产物,基础镜像切换到 distroless 或 scratch。这样既减少镜像体积,也大幅缩小可被漏洞扫描器标记的系统包数量。下面是一个 Go 服务的示例。
FROM golang:1.22 AS builder WORKDIR /src COPY . . RUN go build -o app . FROM gcr.io/distroless/base-debian12 COPY --from=builder /src/app /app ENTRYPOINT ["/app"]
如果扫描结果中存在大量无法立即修复的漏洞,可以按严重级别设定处理时限:CRITICAL 当天处理,HIGH 一周内处理,MEDIUM 纳入版本升级计划。将 Trivy 或 Grype 的 JSON 输出接入缺陷系统,自动创建工单,也能避免漏洞清单长期无人认领。
镜像漏洞扫描接入 DevSecOps 后,构建失败率短期可能上升,但这是把原本隐藏在上线后的风险提前暴露。先卡住高危漏洞,再通过 SBOM、白名单和基础镜像升级逐步收敛,流水线会从被动检查变成团队日常开发的一部分。