
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 基础镜像里带的历史漏洞很多镜像以ubuntu、debian、centos这类完整发行版镜像为基础。完整发行版自带系统库、包管理器、shell 和一堆你用不到的软件包这些包的版本一旦滞后就会在扫描结果里对应到大量 CVE。比如某个镜像的基础版本还是几年前的那么libc、openssl、zlib这些底层库的几十个漏洞会全部被扫出来。这是 1400 个 CVE 的大头。2.2 包管理器安装的依赖项目用apt-get install、pip install、npm install等方式安装依赖时如果没固定版本构建时会拉到当时的 latest下一次构建可能版本就变了。依赖树上任何一个传递依赖存在漏洞扫描结果都会增加一条记录。尤其 Python 和 Node.js 项目依赖动辄几十上百个传递依赖的 CVE 数量很容易膨胀。2.3 构建阶段留下的缓存和调试工具为了构建方便很多 Dockerfile 会在构建阶段安装curl、wget、vim、gcc、build-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 版权、隐私与合规边界在做镜像精简和漏洞修复时要注意软件许可证和授权边界。基础镜像换到distroless或scratch并不意味着所有组件都可以随意分发部分静态编译的二进制、动态库和字体文件仍然有各自的开源许可证。如果团队从互联网上拉取第三方基础镜像或二进制文件要确认来源可信、许可证允许再分发。涉及用户数据、模型文件、密钥证书的镜像一律不能推到公开仓库。所有安全测试应该在本地或隔离环境完成避免把内部信息混入镜像。4. 环境准备与前置条件在开始复现 NanoClaw 的镜像精简流程之前需要准备一套最小环境。下面给的是通用清单具体版本按实际环境调整。4.1 安装 Docker镜像治理的第一步是能构建和运行镜像。Docker 的安装方式很多Linux 下可以用包管理器macOS 和 Windows 可以用 Docker Desktop。安装完成后执行docker --version docker compose version能输出版本号说明 Docker 客户端和引擎可用了。4.2 安装镜像扫描工具TrivyTrivy 是目前使用最广的镜像扫描器之一支持容器镜像、文件系统、SBOM 和 Git 仓库。macOS 安装brew install aquasecurity/trivy/trivyLinux 安装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 --version4.3 安装 SBOM 工具SyftSyft 是 Anchore 开源的软件物料清单生成工具用来生成镜像的 SBOM。安装方式curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin验证syft version4.4 准备一个测试镜像建议不要直接拿生产镜像做实验先构建一个最小的测试镜像。比如下面这个 Go 程序镜像故意使用旧基础镜像并安装一些不必要的包用来观察 CVE 扫描结果。实验完成后可以再执行正式的精简流程。docker pull ubuntu:20.04 docker images | grep ubuntu5. 从 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漏洞数量往往会下降一个数量级。如果进一步换到scratch或distroless系统包几乎被清空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_ENABLED0 go build -o app . FROM scratch COPY --frombuilder /app/app /app ENTRYPOINT [/app]这个 Dockerfile 里最终镜像只有编译好的二进制文件不包含编译器、源码、包缓存和系统库。镜像体积从几百 MB 降到几十 MBCVE 数量也大幅下降。5.3 多阶段构建多阶段构建是镜像精简的基础。以前常见的做法是同一个阶段里先编译、再运行结果最终镜像里残留了大量构建工具。正确做法是用AS builder阶段完成编译、下载、打包然后在最终阶段只拷贝产物。Python 项目也可以用多阶段构建但要注意 Python 运行时依赖。如果应用的依赖里有原生扩展库并不是所有情况下都能直接上scratch更稳妥的选择是使用python:3.12-slim或distroless。下面是一个多阶段 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 --frombuilder /install /usr/local COPY . . ENV PYTHONUNBUFFERED1 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 锁定依赖版本并生成 SBOMCVE 数量反弹最常见的场景是依赖没有锁定。pip install flask和pip install flask3.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 --frombuilder /app/app /app COPY --frombuilder /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 结果给审计人工看表格不方便归档可以把扫描结果导出成 JSONtrivy 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 export或dive工具分析层内容。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/checkoutv4 - name: Build image run: docker build -t my-app:${{ github.sha }} . - name: Run Trivy scanner uses: aquasecurity/trivy-actionmaster with: image-ref: my-app:${{ github.sha }} format: table exit-code: 1 ignore-unfixed: true severity: HIGH,CRITICALexit-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 217.3 不涉及业务 API 服务的说明NanoClaw 这次镜像 CVE 治理处理的是容器镜像制品不是对外业务 API 服务所以没有标准的 REST API 可以单独开放给外部调用。团队如果要把扫描能力暴露给内部其他系统通常是调用 Trivy 的 Server 模式或者直接封装命令行工具。部署在 Kubernetes 里的应用更关注的是镜像仓库和 CI 流水线之间的联动而不是业务接口的鉴权设计。8. 资源占用与性能观察镜像精简不只是安全收益也会直接影响构建和运行效率。8.1 镜像体积用docker images可以对比精简前后的体积。从完整 Ubuntu 基础镜像换成distroless或scratch体积通常下降 70% 到 90%。体积变小之后镜像拉取时间、磁盘占用、节点冷启动时间都会变好。8.2 构建时间多阶段构建首次可能需要重新拉取基础镜像后续利用 Docker 层缓存会变快。但要注意基础镜像变化越频繁缓存失效概率越高。把依赖安装和代码复制拆成不同层能在源码变更时避免重新下载所有依赖。8.3 运行时性能和调试成本scratch或distroless镜像没有 shell也没有包管理器运行时的系统资源占用通常更低。但调试成本会上升容器内无法直接执行curl、vim、bash排查问题往往需要依赖 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 ls或pip freeze查依赖树生成 lock 文件使用全量依赖锁定删除包后扫描仍然报 CVE包管理器缓存或旧层保留文件docker history和docker export查看删除缓存重新构建不保留中间层扫描器在 CI 中下载漏洞库很慢网络受限或漏洞库过大查看 CI 日志预先缓存漏洞库或使用内网镜像漏洞库镜像体积没明显下降最终阶段复制了完整依赖目录dive查看层体积精简 COPY 目录去掉调试文件和源码改用scratch后容器内无法抓包排查镜像没有 shell 和网络工具观察外部日志和监控临时采用调试镜像运行同一程序SBOM 生成不了镜像格式或平台不兼容syft version检查版本更新 Syft改用--platform参数10. 最佳实践与使用建议从 NanoClaw 的镜像 CVE 治理经验里可以沉淀出下面这些工程实践。第一把“漏洞扫描基线”固化到项目里。每次镜像变更前先生成一份基线报告变更后用同一命令重新扫描。没有基线就没法判断 1,400 到接近 0 是真实改善还是扫描器的漏洞库变了、忽略规则变了导致的结果。第二最终镜像越小越好但不能牺牲可观测性。生产镜像用distroless或scratch完全可以但一定要保留标准输出和标准错误日志。云原生场景下日志全部输出到 stdout不要写本地文件不然镜像精简后连日志文件都很难找到。第三依赖锁定要自动化。手工维护版本号很容易遗漏。Go 用go.sumPython 用requirements.txtpip-tools或uvNode.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 配置保存下来下次给镜像做安全审计时直接用。