容器让交付变简单,却也让安全边界变得更隐蔽:别人打好的基础镜像里可能躺着一摞已知 CVE,你 pip install 的依赖也许带着高危漏洞,Dockerfile 里一句 RUN curl | sh 就把密钥写进了层。传统”上线前扫一遍”太晚,而 Trivy 这类开源镜像扫描器,能把检测左移到构建阶段。本文用一条命令带你跑通从漏洞发现到 CI 准入的完整闭环。
为什么容器镜像需要专门扫描
虚拟机时代,安全团队通常盯着操作系统补丁;到了容器时代,镜像由多层叠加而成,每一层都可能引入新的攻击面。一个 nginx:1.25 基础镜像可能包含几十个已分配 CVE 编号的 OS 包,业务镜像再叠加 Python/Node 依赖,风险面进一步扩大。更棘手的是:这些漏洞在本地能跑、在测试能过,直到被扫描器或攻击者点名才暴露。把扫描做成 CI 的硬性闸门,是性价比最高的安全投入之一。
更隐蔽的是基础镜像漂移:你今天基于 python:3.11-slim 构建的镜像是干净的,三个月后同一个标签重新拉取,底层 Debian 可能已经累积了新 CVE。这意味着”曾经扫过”不等于”永远安全”,需要把定期重扫纳入运维节奏,而不是只在首次构建时扫一次。配合镜像签名与准入控制,才能形成闭环。
Trivy 快速上手:一条命令扫出漏洞
Trivy 由 Aqua Security 开源,单二进制、零依赖,装完即可扫。最常见的用法是扫描一个镜像:
# 安装(macOS)
brew install trivy
# 扫描一个镜像,默认联网拉取漏洞库后输出报告
trivy image nginx:1.25
# 只看高危及以上,结果更聚焦
trivy image --severity CRITICAL,HIGH my-registry/app:latest
首次运行会自动下载漏洞数据库(约几十 MB),之后走本地缓存。输出按包列出 CVE 编号、严重等级与可升级的修复版本,开发者一眼就能定位该升哪个依赖。
扫描维度:不止 CVE
很多人以为镜像扫描就是查漏洞,Trivy 实际上覆盖了四个维度,缺一个都可能留口子:
1. 漏洞(Vulnerability)
扫描 OS 包(apk/apt/rpm)与语言依赖(npm/pip/go mod 等)中的已知 CVE,这是最常用的能力。
2. 密钥与敏感信息(Secret)
检测源码或层里误提交的 AWS Key、私钥、Token。很多”密钥泄露”事故,根源就是 Dockerfile 里 COPY . . 把 .env 一起打进了镜像。与服务器安全加固里的 Fail2ban 思路一致——防御要分层。
3. 配置合规(Misconfiguration)
按 CIS 等基线检查 Dockerfile、docker-compose、K8s 清单里的不安全写法,比如以 root 运行、特权容器、挂载 docker.sock。
4. 许可证(SBOM)
生成软件物料清单并标记高危许可证(GPL 传染性等),便于合规审计。
| 扫描维度 | 典型对象 | 命令 | 价值 |
|---|---|---|---|
| 漏洞 | OS 包 / 依赖 | trivy image | 堵已知 CVE |
| 密钥 | 源码 / 层文件 | trivy image --scanners secret | 防泄露 |
| 配置 | Dockerfile / K8s | trivy config | 防误配 |
| 许可证 | 依赖清单 | trivy image --scanners license | 合规 |
在 CI/CD 中落地:把漏洞挡在合入前
扫描只有接进流水线才算”左移”。下面这段 GitHub Actions 在每次构建镜像后扫描,只要出现 HIGH/CRITICAL 就让构建失败,从根本上阻止带洞镜像进入仓库:
name: scan
on: [push]
jobs:
trivy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t app:${{ github.sha }} .
- name: Trivy scan (fail on HIGH/CRITICAL)
uses: aquasecurity/trivy-action@master
with:
image-ref: app:${{ github.sha }}
severity: HIGH,CRITICAL
exit-code: '1'
ignore-unfixed: true
注意 ignore-unfixed: true:只拦”已有修复版本”的漏洞,避免被暂时无解的 CVE 卡死发布。结合GitHub Actions 实战的缓存与矩阵技巧,可以把扫描做成几乎零感的常态步骤。
实战:扫描 Dockerfile 与 IaC 配置
除了镜像,源码目录和编排文件也能扫。把这两步并到本地 pre-commit 或 CI 早期,能更早发现问题:
# 扫描整个代码目录:依赖漏洞 + 密钥泄露
trivy fs --scanners vuln,secret .
# 扫描 Dockerfile / docker-compose / K8s 清单的配置风险
trivy config ./Dockerfile
trivy config ./docker-compose.yml
例如 Dockerfile 里写了 USER root 或挂载了宿主 /var/run/docker.sock,Trivy 会按 CIS Docker 基线给出告警。这正是Docker 网络模式与容器运行时加固要配套考虑的一环。
分级处置:严重漏洞怎么修
扫描报告一长串,别慌,按优先级处置:
- 有修复版本且可升:直接升级依赖或基础镜像小版本,CI 会重新通过。
- 有修复版本但升级有风险:先在测试环境验证,再排期,期间用
.trivyignore临时豁免并附理由。 - 无修复版本(unfixed):评估实际可利用性,必要时换组件或加运行时防护(如Nginx 限流防刷那样的边缘防线)。
- 误报:确认后用精准的 ignore 规则排除,避免”忽略一切”掩盖真问题。
进阶:SBOM 与私有仓库
生产环境建议给每个镜像产出 SBOM,并对接私有镜像仓库做准入:
# 生成 SBOM(CycloneDX / SPDX)
trivy image --format cyclonedx --output sbom.json app:latest
# 扫描私有仓库镜像,仅盯严重
trivy image --severity CRITICAL \
registry.internal.example.com/app:latest
把 SBOM 存进制品库,未来出现新 CVE 时可反向检索”哪些镜像受影响”,大幅缩短应急响应时间。在K8s集群里,还可以用 admission webhook 对不带”已扫描”标签的镜像直接拒绝调度。
本地排障:扫描太慢、误报与私有库
实际落地常遇到三类问题。扫描太慢:首次要下载漏洞库,可把 TRIVY_CACHE_DIR 挂进 CI 缓存,或用 trivy image --skip-db-update 复用上次库。误报多:用 .trivyignore 按 CVE 编号或包名精准豁免,并在注释里写清理由与到期日,切忌用全局忽略掩盖问题。私有库拉不到:给 Trivy 配置镜像仓库凭证,或走离线漏洞库(trivy image --offline-scan)满足内网环境。
# 复用漏洞库缓存,跳过在线更新
export TRIVY_CACHE_DIR=$HOME/.cache/trivy
trivy image --skip-db-update app:latest
# 离线扫描内网镜像
trivy image --offline-scan registry.internal/app:latest
小结
Trivy 把”镜像安全”从上线后的救火,变成构建时的硬闸门:一条命令发现 CVE,四维度覆盖漏洞/密钥/配置/许可证,接进 CI 后高危漏洞根本进不了仓库。它免费、单二进制、和 Docker、K8s、GitHub Actions 都能自然咬合。安全不是某个阶段的产物,而是写进流水线的习惯——今天就把 trivy image 加进你的构建脚本吧。另外建议把 SBOM 产出与定期重扫也固化进流水线,基础镜像漂移带来的新 CVE 才能被持续兜住,而不是只在第一次构建时侥幸通过。




