Trivy 是 Aqua Security 开源的容器镜像漏洞扫描工具,能在 CI 里一键检出操作系统包与语言依赖里的 CVE。本文从安装、扫描到 GitHub Actions 集成,手把手把供应链安全问题拦在合入之前,而不是等上线后被扫出一堆 CRITICAL。
一、为什么要在 CI 里做镜像扫描
容器镜像不是“干净的运行环境”,它里面打包了基础系统的 glibc、openssl,以及你的 Python/Node 依赖。任何一层有已知 CVE,都会随镜像一起发布到生产。典型的教训是 Log4Shell(CVE-2021-44228):一个传递依赖就能让整个集群可被远程执行代码。
把扫描放到 CI 而不是事后人工巡检,核心收益有三:一是可复现,每次构建都按同一套规则检查;二是可阻断,高危漏洞直接让流水线失败,合并不了;三是可审计,SBOM(软件物料清单)能回答“这个镜像到底装了什么”。
扫描的时机也很关键:越早越好(shift-left)。在镜像构建完成后立刻扫,比等到部署前再扫能省下数倍的返工成本——你肯定不希望在凌晨被一条“紧急修复 CVE”的告警叫醒。把扫描作为流水线里一个普通步骤,和单元测试、构建并列,安全就不再是上线前的“临时抱佛脚”。
二、安装 Trivy
Trivy 是单一静态二进制,没有运行时依赖。常见平台一行即可装好:
# macOS
brew install trivy
# Ubuntu / Debian
sudo apt-get install -y wget apt-transport-https gnupg
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb stable main" | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install -y trivy
# 不想装?直接用容器版,效果完全一致
docker run --rm aquasec/trivy:latest --version
装完用 trivy --version 确认能跑。它的漏洞库(trivy-db)首次运行会自动下载并缓存到 ~/.cache/trivy,后续扫描默认走本地缓存,离线也能扫。
三、扫描一个镜像:读懂结果
# 扫描官方 nginx 镜像
trivy image nginx:1.25
# 只看高危以上,避免被海量 LOW 噪音淹没
trivy image --severity HIGH,CRITICAL nginx:1.25
# 指定镜像来源(本地镜像、tar 包、私有仓库都支持)
trivy image --input myapp.tar
trivy image registry.internal.example.com/myapp:1.2.0
输出按漏洞等级分四档:CRITICAL(严重)、HIGH(高)、MEDIUM(中)、LOW(低)。每条都会给出 CVE 编号、受影响包、已修复版本(Fixed Version)和建议。重点关注 CRITICAL/HIGH,且 Fixed Version 非空的项——那是可以直接升级修掉的。
看漏洞不能只看等级。同样是 HIGH,CVSS 评分 7.0 和 9.8 的紧迫度天差地别;更关键的是攻击向量(Attack Vector):标着“本地可达 / 需要认证”的漏洞,实际被利用的概率远低于“网络可达 / 无需认证”的漏洞。Trivy 结果里会标注这些维度,排修复优先级时要把“能否被外部打到”放在等级之前,避免团队被海量 LOW 噪音拖垮。
四、Trivy 能扫什么:扫描类型一览
| 扫描目标 | 命令示例 | 主要用途 |
|---|---|---|
| 容器镜像 | trivy image <img> | 检出 OS 包与语言依赖 CVE |
| 文件系统 | trivy fs ./project | 扫源码目录里的依赖与密钥 |
| 代码仓库 | trivy repo <git-url> | 远程仓库直接拉取扫描 |
| Kubernetes | trivy k8s –report summary | 扫集群工作负载与配置 |
| 配置合规 | trivy config ./deploy | 检查 IaC(Dockerfile/K8s)误配置 |
| 密钥泄露 | trivy secret ./project | 发现硬编码的 token/密码 |
| SBOM 生成 | trivy image –format cyclonedx | 输出软件物料清单 |
日常 CI 用得最多的是 image 和 fs;secret 和 config 则能补上“代码里写了密码”“Dockerfile 用 root 跑”这类非 CVE 类风险。
五、忽略策略与误报处理
不是每个漏洞都能立刻修:有些依赖暂时没有补丁,有些在你的运行场景里根本不可达。Trivy 用 .trivyignore 声明豁免,但务必写清理由,避免变成“一键屏蔽所有”。
# .trivyignore
# CVE-2023-1234 在 Alpine 3.19 的 libwebp 中不可达(仅静态库被链接,无解码路径)
CVE-2023-1234
# 该组件为构建期工具,不进最终镜像,风险可控
CVE-2024-5678
除了忽略文件,还可以用 --ignore-unfixed 跳过“官方还没给修复版本”的漏洞,让 CI 只报真正能修的项,减少噪音:
trivy image --severity HIGH,CRITICAL --ignore-unfixed myapp:latest
六、集成到 GitHub Actions:高危即阻断
这是最关键的落地方式。下面这个 workflow 在构建出镜像后立刻扫描,只要有 HIGH/CRITICAL 漏洞,流水线就非零退出,PR 无法合入。
name: scan
on:
push:
branches: [main]
pull_request:
jobs:
trivy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
severity: HIGH,CRITICAL
exit-code: "1"
ignore-unfixed: true
如果团队暂时不想“硬阻断”,可先把 exit-code 设为 0,让结果以警告形式出现在 PR 评论里,观察一段时间再收紧。关于流水线本身的搭建,可以参考我们的 GitHub Actions CI/CD 实战。
这个 workflow 有几个值得说明的点:pull_request 触发保证每次合并评审都过一遍扫描;exit-code: '1' 是“阻断”开关,设为 0 则只报不拦;ignore-unfixed: true 跳过官方尚未提供补丁的漏洞,避免无效告警刷屏。Trivy Action 会自动处理漏洞库下载与缓存,基本开箱即用,无需自己再写一层封装。
七、与 Docker 构建联动:多阶段 + 扫描
推荐在 CI 里用多阶段构建产出最小运行时镜像,再对最终镜像扫描。这样既能减少攻击面,也能避免把构建期的 dev 依赖扫进风险清单。
# 构建阶段
FROM node:20 AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build
# 运行时阶段:只拿产物,不带构建工具
FROM nginx:1.25-alpine
COPY --from=build /app/dist /usr/share/nginx/html
# 注意:尽量用非 root 用户,避免 config 扫描报“以 root 运行”
USER 1001
构建镜像的姿势,建议沉淀成团队统一的 Docker Compose 一键编排 模板,保证本地和 CI 行为一致。
八、生成 SBOM:合规的入场券
越来越多客户和监管要求在交付时附上 SBOM。Trivy 一条命令就能产出 CycloneDX 或 SPDX 格式清单,可直接对接后续的合规与持续监控平台。
# 生成 CycloneDX 格式 SBOM
trivy image --format cyclonedx --output sbom.cdx.json myapp:latest
# 生成 SPDX 格式
trivy image --format spdx-json --output sbom.spdx.json myapp:latest
九、性能与私有仓库
扫描慢多半是漏洞库没缓存或拉镜像走公网。把缓存目录挂到 CI 缓存里能显著加速;私有仓库则通过环境变量注入凭证:
# 复用漏洞库缓存,避免每次重建下载
export TRIVY_CACHE_DIR=$HOME/.cache/trivy
# 私有仓库认证(推荐用 CI Secret,别写死)
export TRIVY_USERNAME=$REG_USER
export TRIVY_PASSWORD=$REG_PASS
trivy image registry.internal.example.com/myapp:latest
十、常见坑与排错
| 现象 | 根因 | 对策 |
|---|---|---|
| 首次扫描极慢 | 正在下载 trivy-db | 预热缓存,CI 中挂载 ~/.cache/trivy |
| DB 下载报网络错 | 访问 GitHub 受限 | 设 TRIVY_DB_REPOSITORY 走镜像源 |
| 扫描结果“全绿” | 镜像标签被覆盖/拉错 | 用 immutable 的 digest(@sha256:)锁定 |
| 大量 LOW 噪音 | 未过滤严重度 | 加 –severity HIGH,CRITICAL –ignore-unfixed |
| 私有仓库 401 | 凭证未注入 | 配置 TRIVY_USERNAME/PASSWORD 或 docker login |
十一、落地 Checklist
- 本地装好 Trivy,先用
trivy image扫一遍现有镜像,建立基线。 - 在 GitHub Actions 里加扫描步骤,HIGH/CRITICAL 直接
exit-code: 1阻断。 - 用
.trivyignore收敛误报,每条必须带理由。 - 开启
secret与config扫描,补齐非 CVE 风险。 - 每次发布附上 SBOM,喂给合规与监控链路。
- 缓存
~/.cache/trivy,把扫描耗时压到分钟级。
十二、小结
Trivy 的最大价值不是“扫出多少漏洞”,而是把安全卡点前移到 CI,让问题在合入前就被拦下。它单一二进制、规则开箱即用、和 GitHub Actions 几乎零成本对接,是中小团队最划算的供应链安全第一步。配合 VS Code 远程开发 在本地提前验证、再用 chezmoi 管理 dotfiles 统一团队配置,整条研发链路都能更稳。




