容器镜像CVE治理:如何一次性消除1400个漏洞
2026/8/31 21:45:16 网站建设 项目流程

NanoClaw 的容器镜像做了一件很值得拆解的事:一次性剔除 1,400 个 CVE。这个数字在镜像安全扫描报告里并不少见,很多镜像一扫描就是几十、几百甚至上千个漏洞条目,但真正愿意翻到底、逐个修复、再把流程固化下来的团队不多。CVE 全称是 Common Vulnerabilities and Exposures,也就是公开的漏洞编号;镜像里的 CVE 说明对应软件包存在已知安全公告,不代表机器一定已经被攻击,但它直接反映了供应链的暴露面。把 1,400 个 CVE 清掉,等于把攻击者最常用的“已知漏洞利用”路径大部分关掉。

这篇文章不打算只复述一条新闻,而是从 NanoClaw 这个案例出发,拆解容器镜像 CVE 治理的完整路径:先扫描拿到基线,再换基础镜像、做多阶段构建、生成 SBOM,最后把扫描接入 CI/CD 强制卡点。文章里会给出可以直接抄的 Dockerfile、扫描命令和 CI 配置示例,也会列出最常见的“删不完 CVE”的原因。适合正在做容器化改造、镜像瘦身、供应链安全审计,或者被镜像扫描报告追着改漏洞的读者。

1. 核心能力速览:NanoClaw 镜像 CVE 治理的关键指标

先说结论。从公开信息看,NanoClaw 这次做的事情本质上不是“修了 1,400 个漏洞”,而是通过镜像重构,让扫描器在镜像里找不到那么多可匹配的漏洞条目。这两种说法有区别:前者是补丁思维,后者是供应链精简思维。补丁思维是每个漏洞对应一个修复版本,非常被动;精简思维是去掉不需要的软件包、更换更小的基础镜像、锁定依赖版本,让漏洞根本没有机会进入最终镜像。

项目说明
治理对象NanoClaw 的容器镜像
公开目标镜像内已知 CVE 数量从 1,400 级别压到接近 0
核心手段基础镜像替换、多阶段构建、依赖裁剪、SBOM 追踪
主要收益漏洞暴露面下降、镜像体积变小、上线审计更简单
适用对象任何基于 Linux 的 Docker 镜像
扫描验证工具Trivy、Grype、Syft、Docker Scout 等
自动化接入CI/CD 构建后自动扫描,漏洞数超阈值即失败
不适用范围应用业务漏洞、运行时攻击防护、K8s 配置风险

注意一点:1,400 这个数字需要结合扫描工具和扫描时间来看。不同扫描器的漏洞库、严重级别过滤规则不同,扫描结果会有差异。更稳妥的判断是,NanoClaw 的真实收益在于把“每次扫描都有大量高危漏洞”变成了“高危漏洞清零或接近清零”,这个过程比具体数字更有参考价值。

2. 这 1400 个 CVE 到底是从哪来的

一个镜像里出现一千多个 CVE,通常不是“代码写错了”,而是组成镜像的材料本身带病。最常见的来源有四类。

2.1 基础镜像里带的历史漏洞

很多镜像以ubuntudebiancentos这类完整发行版镜像为基础。完整发行版自带系统库、包管理器、shell 和一堆你用不到的软件包,这些包的版本一旦滞后,就会在扫描结果里对应到大量 CVE。比如某个镜像的基础版本还是几年前的,那么libcopensslzlib这些底层库的几十个漏洞会全部被扫出来。这是 1400 个 CVE 的大头。

2.2 包管理器安装的依赖

项目用apt-get installpip installnpm install等方式安装依赖时,如果没固定版本,构建时会拉到当时的 latest,下一次构建可能版本就变了。依赖树上任何一个传递依赖存在漏洞,扫描结果都会增加一条记录。尤其 Python 和 Node.js 项目,依赖动辄几十上百个,传递依赖的 CVE 数量很容易膨胀。

2.3 构建阶段留下的缓存和调试工具

为了构建方便,很多 Dockerfile 会在构建阶段安装curlwgetvimgccbuild-essential,甚至把包管理器缓存一起留在最终镜像里。这些工具和缓存文件不是运行时需要的,但它们都在镜像文件系统里,扫描器会逐一匹配,结果就是又增加一批 CVE。还有编译产物里的旧动态库,比如某个二进制文件自带旧版libssl.so,也会被扫出来。

2.4 多版本残留和未清理的临时文件

有些镜像在构建过程中下载源码包、安装多个版本的工具链,最后没有rm -rf清理。这些残留文件既占空间,又贡献漏洞记录。镜像层是只读的,任何一层里留下的文件都会保留到最后,所以“先安装、后删除”并不能真正清理干净,正确做法是用多阶段构建把最终产物单独拷贝出来。

3. 镜像 CVE 治理的适用场景与使用边界

3.1 适合谁

如果你正在做以下工作,NanoClaw 这个案例的参考价值很大:

  • 容器镜像要推送到公网镜像仓库,安全团队要求先过扫描。
  • 政企、金融、医疗等场景交付镜像时,甲方要求提供漏洞扫描报告。
  • 镜像体积已经很大,想顺便做精简。
  • 团队有 CI/CD,想把镜像安全卡点自动化。

这个思路适合任何基于 Linux 的应用镜像,不管语言是 Go、Java、Python 还是 Node.js,核心路径基本一致。

3.2 不适合什么

镜像 CVE 治理不能覆盖所有安全问题。它只解决“已知漏洞条目”和“镜像内容精简”,不解决应用自身的业务逻辑漏洞,不代替 Web 应用防火墙,也不能防止攻击者利用未公开的 0day。Kubernetes 集群的 RBAC 配置、Seccomp、网络策略等风险,需要另外一套工具和流程。换句话说,CVE 清零是安全基线之一,不是安全全部。

3.3 版权、隐私与合规边界

在做镜像精简和漏洞修复时,要注意软件许可证和授权边界。基础镜像换到distrolessscratch并不意味着所有组件都可以随意分发,部分静态编译的二进制、动态库和字体文件仍然有各自的开源许可证。如果团队从互联网上拉取第三方基础镜像或二进制文件,要确认来源可信、许可证允许再分发。涉及用户数据、模型文件、密钥证书的镜像,一律不能推到公开仓库。所有安全测试应该在本地或隔离环境完成,避免把内部信息混入镜像。

4. 环境准备与前置条件

在开始复现 NanoClaw 的镜像精简流程之前,需要准备一套最小环境。下面给的是通用清单,具体版本按实际环境调整。

4.1 安装 Docker

镜像治理的第一步是能构建和运行镜像。Docker 的安装方式很多,Linux 下可以用包管理器,macOS 和 Windows 可以用 Docker Desktop。安装完成后执行:

docker --version docker compose version

能输出版本号说明 Docker 客户端和引擎可用了。

4.2 安装镜像扫描工具:Trivy

Trivy 是目前使用最广的镜像扫描器之一,支持容器镜像、文件系统、SBOM 和 Git 仓库。macOS 安装:

brew install aquasecurity/trivy/trivy

Linux 安装:

sudo apt-get install wget apt-transport-https gnupg lsb-release wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add - echo deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list.d/trivy.list sudo apt-get update sudo apt-get install trivy

安装后确认版本:

trivy --version

4.3 安装 SBOM 工具:Syft

Syft 是 Anchore 开源的软件物料清单生成工具,用来生成镜像的 SBOM。安装方式:

curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin

验证:

syft version

4.4 准备一个测试镜像

建议不要直接拿生产镜像做实验,先构建一个最小的测试镜像。比如下面这个 Go 程序镜像,故意使用旧基础镜像并安装一些不必要的包,用来观察 CVE 扫描结果。实验完成后可以再执行正式的精简流程。

docker pull ubuntu:20.04 docker images | grep ubuntu

5. 从 1400 到接近 0:镜像精简实战拆解

NanoClaw 的案例可以拆成六步,每一步都对应镜像扫描报告里的一个漏洞来源。

5.1 先扫描,拿到基线

不要凭感觉判断镜像哪里有问题,先用扫描器生成一份基线报告。以 Trivy 为例:

trivy image --severity HIGH,CRITICAL --format table ubuntu:20.04

这个命令会列出ubuntu:20.04里高危和严重漏洞。如果机器没法访问漏洞库,可以先更新:

trivy image --download-db-only

扫描结果出来后,把漏洞总数按严重级别记录到一个文件里。后续每次修改 Dockerfile,都用同一份命令和同一个漏洞库版本重新扫描,才能对比出真实的修复效果。

5.2 更换基础镜像

这是削减 CVE 数量最有效的一步。从完整发行版ubuntu:20.04换到alpine:3.20,漏洞数量往往会下降一个数量级。如果进一步换到scratchdistroless,系统包几乎被清空,CVE 数量会非常低。

scratch是 Docker 保留的空镜像,最终镜像里只有你自己拷贝进去的文件,没有 shell、没有包管理器、没有系统库。gcr.io/distroless是 Google 提供的一组精简镜像,只包含运行应用需要的最小运行时,没有 shell 和包管理器。Go 应用非常适合直接基于scratch构建:

FROM golang:1.22 AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o app . FROM scratch COPY --from=builder /app/app /app ENTRYPOINT ["/app"]

这个 Dockerfile 里,最终镜像只有编译好的二进制文件,不包含编译器、源码、包缓存和系统库。镜像体积从几百 MB 降到几十 MB,CVE 数量也大幅下降。

5.3 多阶段构建

多阶段构建是镜像精简的基础。以前常见的做法是同一个阶段里先编译、再运行,结果最终镜像里残留了大量构建工具。正确做法是用AS builder阶段完成编译、下载、打包,然后在最终阶段只拷贝产物。

Python 项目也可以用多阶段构建,但要注意 Python 运行时依赖。如果应用的依赖里有原生扩展库,并不是所有情况下都能直接上scratch,更稳妥的选择是使用python:3.12-slimdistroless。下面是一个多阶段 Python 镜像示例:

FROM python:3.12-slim AS builder WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir --prefix=/install -r requirements.txt FROM python:3.12-slim WORKDIR /app COPY --from=builder /install /usr/local COPY . . ENV PYTHONUNBUFFERED=1 CMD ["python", "app.py"]

--prefix=/install可以把依赖安装到指定目录,最终阶段只拷贝这个目录,避免把pip缓存和中间构建文件带进来。

5.4 精简系统依赖

如果镜像必须安装系统包,务必使用最小化参数并及时清理缓存。比如基于 Debian 系的镜像:

FROM debian:bookworm-slim RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates \ && rm -rf /var/lib/apt/lists/*

这里的两个关键点:

  • --no-install-recommends禁止安装推荐包,减少无关包进入镜像。
  • rm -rf /var/lib/apt/lists/*删除 apt 索引缓存,避免扫描器在缓存里匹配到大量包记录。

5.5 锁定依赖版本并生成 SBOM

CVE 数量反弹最常见的场景是依赖没有锁定。pip install flaskpip install flask==3.0.3在安全审计上是完全不同的状态。前者每次构建都可能拉到新版本,容易无意中引入带漏洞的版本;后者是确定性的。

在锁定依赖的基础上,用 Syft 生成镜像的软件物料清单:

syft packages docker:ubuntu:20.04 --scope all-layers -o spdx-json > ubuntu.spdx.json

生成 SBOM 后,可以随时知道镜像里有哪些组件、版本多少、来自哪个层。后续做漏洞修复时,SBOM 也能帮助定位哪个组件对应哪个 CVE。

5.6 最终阶段删除多余文件和敏感信息

构建时复制到镜像里的.env、密钥、证书、源码文件,一旦进入镜像层就难以彻底删除。正确做法是只复制运行必需的文件,敏感信息通过 Kubernetes Secret、环境变量或外部配置中心注入。针对会读取文件的场景,注意设置文件权限:

FROM scratch COPY --from=builder /app/app /app COPY --from=builder /app/config /config RUN chmod 755 /app

镜像里没有敏感文件,扫描器扫不到,审计也干净。

6. 镜像 CVE 扫描与效果验证

完成 Dockerfile 修改后,需要重新构建并扫描,验证“从 1400 到接近 0”的效果。

6.1 重新构建

docker build -t naclaw-demo:v2 .

6.2 扫描新镜像

trivy image --severity HIGH,CRITICAL --ignore-unfixed --format table naclaw-demo:v2

--ignore-unfixed只显示有修复版本的漏洞,这个参数在评估“能不能尽快修”时非常有用。如果只关心镜像里实际存在的已知漏洞,不加这个参数更合适。

6.3 输出 JSON 结果给审计

人工看表格不方便归档,可以把扫描结果导出成 JSON:

trivy image --severity HIGH,CRITICAL --format json -o trivy-report.json naclaw-demo:v2

把 JSON 文件存档,配合 SBOM 文件,可以形成一份可追溯的镜像安全报告。

6.4 判断成功的标准

  • 高危和严重漏洞数量从几百降到 0,或者降到团队设定的阈值以下。
  • 镜像体积明显下降,docker images可以看到实际大小。
  • 运行测试通过,应用功能没有因为裁剪依赖而报错。
  • 重复扫描两次结果一致,排除漏洞库缓存不一致导致的误判。

6.5 失败时先查什么

如果扫描结果仍然很高,先查是否在最终镜像里遗留了包管理器缓存、旧基础镜像层、未清理的下载文件。可以执行:

docker history naclaw-demo:v2

看镜像层的指令,哪一层引入了大文件,哪一层安装了包,都能看出来。再用docker run --entrypoint /bin/sh进入镜像查看文件系统,但注意scratch镜像没有 shell,这时可以用docker exportdive工具分析层内容。

7. CI/CD 自动化与批量镜像治理

NanoClaw 的“消除 1,400 个 CVE”如果只做一次,价值有限。真正有价值的做法是把扫描变成自动化卡点,每次构建镜像后自动执行扫描,超过阈值就中断流水线。

7.1 镜像扫描作为构建后置检查

以下是一个 GitHub Actions 示例,把 Trivy 扫描插入到镜像构建之后:

name: image-scan on: push: paths: - 'Dockerfile' - 'src/**' jobs: scan: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Build image run: docker build -t my-app:${{ github.sha }} . - name: Run Trivy scanner uses: aquasecurity/trivy-action@master with: image-ref: my-app:${{ github.sha }} format: table exit-code: '1' ignore-unfixed: true severity: HIGH,CRITICAL

exit-code: '1'表示扫描到高危或严重漏洞时,当前任务失败,流水线中断。这个卡点能阻止带漏洞镜像进入仓库。

7.2 批量扫描已有镜像

如果团队有很多存量镜像,可以写一个循环批量扫描:

for image in $(cat images.txt); do echo "=== Scanning $image ===" trivy image --severity HIGH,CRITICAL --ignore-unfixed "$image" done

批量扫描时建议加超时和日志输出,避免某个镜像卡住整个流程:

timeout 600 trivy image --severity HIGH,CRITICAL "$image" > /tmp/san_$(basename $image).log 2>&1

7.3 不涉及业务 API 服务的说明

NanoClaw 这次镜像 CVE 治理处理的是容器镜像制品,不是对外业务 API 服务,所以没有标准的 REST API 可以单独开放给外部调用。团队如果要把扫描能力暴露给内部其他系统,通常是调用 Trivy 的 Server 模式,或者直接封装命令行工具。部署在 Kubernetes 里的应用,更关注的是镜像仓库和 CI 流水线之间的联动,而不是业务接口的鉴权设计。

8. 资源占用与性能观察

镜像精简不只是安全收益,也会直接影响构建和运行效率。

8.1 镜像体积

docker images可以对比精简前后的体积。从完整 Ubuntu 基础镜像换成distrolessscratch,体积通常下降 70% 到 90%。体积变小之后,镜像拉取时间、磁盘占用、节点冷启动时间都会变好。

8.2 构建时间

多阶段构建首次可能需要重新拉取基础镜像,后续利用 Docker 层缓存会变快。但要注意,基础镜像变化越频繁,缓存失效概率越高。把依赖安装和代码复制拆成不同层,能在源码变更时避免重新下载所有依赖。

8.3 运行时性能和调试成本

scratchdistroless镜像没有 shell,也没有包管理器,运行时的系统资源占用通常更低。但调试成本会上升:容器内无法直接执行curlvimbash,排查问题往往需要依赖 Kubernetes 的kubectl logs、临时挂载调试工具,或者使用带 shell 的开发镜像。生产镜像用最精简版本,开发调试镜像用完整版本,是常见做法。

8.4 如何观察资源占用

运行时可以用docker stats观察容器 CPU 和内存:

docker stats nclaw-demo

构建时可以用/usr/bin/time记录构建耗时:

/usr/bin/time -v docker build -t naclaw-demo:v2 .

如果担心构建期间磁盘占用,可以定时执行df -h观察 Docker 存储目录变化。

9. 常见问题与排查方法

镜像 CVE 治理过程中,下面这些问题最容易出现。

问题现象可能原因排查方式解决方案
换掉基础镜像后 CVE 数量变化不大最终镜像里仍有编译产物或静态库docker history查镜像层改成多阶段构建,只拷贝最终产物
镜像从ubuntu换成alpine后应用启动失败应用依赖 glibc,而 Alpine 使用 musl看容器启动日志和 ldd 输出使用distroless或兼容 glibc 的镜像
扫描结果里 CVE 显示 no fix漏洞在漏洞库中还没有修复版本查 NVD、发行版安全公告换组件版本或加 ignore 记录并评估风险
依赖版本锁定了,但扫描出旧版依赖传递依赖没有锁定npm lspip freeze查依赖树生成 lock 文件,使用全量依赖锁定
删除包后扫描仍然报 CVE包管理器缓存或旧层保留文件docker historydocker export查看删除缓存,重新构建,不保留中间层
扫描器在 CI 中下载漏洞库很慢网络受限或漏洞库过大查看 CI 日志预先缓存漏洞库,或使用内网镜像漏洞库
镜像体积没明显下降最终阶段复制了完整依赖目录dive查看层体积精简 COPY 目录,去掉调试文件和源码
改用scratch后容器内无法抓包排查镜像没有 shell 和网络工具观察外部日志和监控临时采用调试镜像运行同一程序
SBOM 生成不了镜像格式或平台不兼容syft version检查版本更新 Syft,改用--platform参数

10. 最佳实践与使用建议

从 NanoClaw 的镜像 CVE 治理经验里,可以沉淀出下面这些工程实践。

第一,把“漏洞扫描基线”固化到项目里。每次镜像变更前先生成一份基线报告,变更后用同一命令重新扫描。没有基线,就没法判断 1,400 到接近 0 是真实改善,还是扫描器的漏洞库变了、忽略规则变了导致的结果。

第二,最终镜像越小越好,但不能牺牲可观测性。生产镜像用distrolessscratch完全可以,但一定要保留标准输出和标准错误日志。云原生场景下,日志全部输出到 stdout,不要写本地文件,不然镜像精简后连日志文件都很难找到。

第三,依赖锁定要自动化。手工维护版本号很容易遗漏。Go 用go.sum,Python 用requirements.txt+pip-toolsuv,Node.js 用package-lock.json。lock 文件进入版本库,CI 构建时严格使用 lock 文件。

第四,CVE 扫描结果要分级处理。不是所有 CVE 都必须立刻修。优先处理高危和严重漏洞,再看有没有--ignore-unfixed项。无修复版本的漏洞需要做风险评估,记录到安全评审表里,而不是一直挂起。

第五,镜像仓库要设置不可变 tag。每次构建生成新的镜像 digest,不能覆盖同一个 tag。这样扫描结果和镜像版本能一一对应,回滚也安全。

第六,涉及人脸、声音、版权素材、用户数据的业务,一旦把这些内容打进镜像,就必须非常谨慎。生产镜像里禁止出现真实用户数据和未授权素材,所有数据处理任务都应该通过挂载卷或外部对象存储完成,镜像只负责程序逻辑。合规和授权问题比 CVE 数量更敏感,不能因为追求镜像精简而把敏感信息一起带进制品。

11. 总结与下一步

NanoClaw 这次消除 1,400 个 CVE,本质上是一个典型的镜像供应链治理案例。它告诉我们,镜像安全不是靠补丁堆出来的,而是靠去掉不必要的组件、缩小攻击面、锁住依赖、生成 SBOM,再把扫描变成 CI 卡点来实现的。值得先验证的功能是:拿一个现有镜像跑一遍 Trivy 扫描,看基线结果;然后把 Dockerfile 改成多阶段构建,换一个更小的基础镜像,再扫描一次。两次结果对比,就能直观感受到 CVE 数量下降的速度。

最容易踩的坑有两个:一是只改基础镜像,不清理最终阶段里残留的文件,导致 CVE 数量下降不明显;二是依赖锁定不完整,扫描结果清零后过几天又反弹。建议在项目里一次性配好三件事:trivy 扫描脚本、Syft SBOM 生成、CI 失败阈值。这三件事跑通后,镜像 CVE 治理就算进入可维护状态了。

下一步可以继续做的是:把 Trivy 扫描结果接入监控大盘,每两周自动生成一份安全报告;对无修复版本的高危漏洞建立跟踪表,定期检查上游是否发布了新版本;如果镜像里有自研二进制,考虑增加二进制层面的依赖分析,进一步缩小盲区。镜像 CVE 治理不是一次性的活动,而是一套需要长期维护的流程。建议把这篇文章里的扫描脚本和 CI 配置保存下来,下次给镜像做安全审计时直接用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询