接到“把 Trivy 接进 CI”这个需求,我一般不会先写 pipeline 脚本,而是先确认两个环境点:镜像仓库的凭证注入方式,以及 CI 平台的缓存目录会不会被多个任务共享。这两个点不解决,后面跑出来的扫描结果很难直接当作合并请求的门禁。
在 CI 中集成 Trivy,常见做法是先安装二进制或使用官方 Docker 镜像,然后执行 trivy image --exit-code 1 --severity HIGH,CRITICAL $IMAGE_NAME。这里 --exit-code 1 的作用是让扫描器在发现高严重性漏洞时返回非零退出码,流水线便会停止。为避免重复下载漏洞库,可以挂载一个持久化缓存目录,例如使用 Docker 运行镜像时加上 -v trivy-cache:/root/.cache/。若需要把扫描结果作为附件上传,可以追加 --format json --output result.json。注意扫描前先拉取镜像,否则某些仓库会因认证信息缺失而失败。
先确认仓库和缓存环境
Trivy 最常用的场景是直接在 CI 流水线中对构建产物执行扫描,它通过特征库比对镜像层的已知漏洞。判断是否适合自己的团队,可以先看镜像仓库是否支持私有仓库凭证注入,再看 CI 平台的缓存机制是否容易暴露。如果流水线本身处于内网环境,离线扫描需要额外导入漏洞库,这时要预先准备好更新的包,否则扫描会因缺少数据库而失败。对于大多数使用 Docker 构建的团队,Trivy 的静态扫描方式比动态运行时扫描更简单,也更容易在合并请求阶段就卡住有风险的镜像。
所以我的第一步,经常会先在 CI 里创建一个最小任务,只对公开镜像执行一次 trivy image,确认漏洞库能正常下载。能通过之后,再把私有仓库凭证加进去,测试带认证的拉取链路。
在 CI 里执行扫描的基础命令
环境确认后,再谈具体命令。常见做法是先安装二进制或使用官方 Docker 镜像,然后执行 trivy image --exit-code 1 --severity HIGH,CRITICAL $IMAGE_NAME。这里 --exit-code 1 的作用是让扫描器在发现高严重性漏洞时返回非零退出码,流水线便会停止。为避免重复下载漏洞库,可以挂载一个持久化缓存目录,例如使用 Docker 运行镜像时加上 -v trivy-cache:/root/.cache/。若需要把扫描结果作为附件上传,可以追加 --format json --output result.json。注意扫描前先拉取镜像,否则某些仓库会因认证信息缺失而失败。
如果直接在流水线里用 Docker 镜像执行,我会写一个相对完整的命令:
docker run --rm -v trivy-cache:/root/.cache/ -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:latest image --exit-code 1 --severity HIGH,CRITICAL --format json --output result.json registry.example.com/app/backend:latest这里的 $IMAGE_NAME 建议写全镜像地址,避免因短名称导致拉取到错误仓库。私有仓库要先完成 docker login,或把凭证注入到 CI 的运行环境里,否则日志里会出现认证错误。
确认扫描对象是镜像 digest,而不是旧标签
任务变绿不代表扫描对了镜像。镜像构建时如果对同一标签覆盖推入,Trivy 扫到的是最新标签对应的层,而 Kubernetes 部署时可能引用的是之前的 digest。所以我会在构建步骤后记录 IMAGE_DIGEST,并把它作为 IMAGE@sha256:... 传进去。同时在 Trivy 报告里检查操作系统和包管理器,例如 debian 或 alpine,如果显示 unknown,就要考虑镜像是否为 distroless 或精简到无可识别内容。对于 Kubernetes 部署文件,还需要另外跑 config 扫描,镜像漏洞扫描只能覆盖容器本身。
门禁设置与风险边界
门禁策略需要想清楚,不能把 Trivy 的退出码直接当成唯一审判。Trivy 扫描的是镜像文件系统里的已知漏洞数据库,并不保证覆盖所有运行期风险。比如已修复漏洞的镜像层可能有残留清单,或者运行时会从外部挂载不受扫描的二进制。因此,把 Trivy 的 exit-code 当作唯一门禁时,要留意扫描结果可能包含误报,也可能漏报。建议只对 HIGH、CRITICAL 级别设置硬性失败,而把 MEDIUM 和 LOW 放在报表中人工处理。如果镜像中使用了私有基础镜像,需要显式配置仓库凭证,否则 Trivy 会因无法拉取镜像而直接报错,不会进入扫描阶段。另外,Kubernetes 集群自身的漏洞不是 Trivy 镜像命令能覆盖的,需要另找专门工具。
实际操作时,我会保留 JSON 输出,在脚本里用 jq 按包名和严重级别做一次聚合,再把结果发给负责维护基础镜像的同事。这样即使门禁没有拦下 MEDIUM 级别,也能在通知里留下跟踪记录。
缓存隔离与 distroless 镜像的干扰
缓存是这里最容易翻车的点。多个流水线共享同一个挂载卷时,Trivy 的漏洞库写操作可能互相覆盖,表现为扫描过程偶发失败。我习惯把缓存目录改成 CI 工作空间下的局部路径,或者让任务在开始时先执行 trivy --download-db-only 把数据库准备好。另一个问题是 distroless 镜像缺少系统包管理器,扫描结果里的 os 包信息会与基础镜像不一致,漏洞数看起来偏低。此时可以在报告备注中说明扫描范围,避免有人误以为镜像绝对安全。
--ignore-unfixed 这个参数需要权衡。加上之后,没有官方修复方案的漏洞会被过滤掉,容易造成漏洞数量很低的假象;不加,又会看到很多暂时处理不了的条目。我的做法是默认不加,让清单完整暴露,再通过人工看哪些属于无法修复的例外。
推荐的处理顺序与后续维护
整体来看,我建议把扫描放在镜像构建之后、推送仓库之前。这样发现高危漏洞可以直接阻断本次构建,避免不安全镜像进入远端仓库。如果镜像构建时间较长,可以先用基础层缓存扫描,但需要清楚新加入的应用层不会被覆盖。对于 GitLab CI 和 GitHub Actions 上的现成包装器,使用时要注意版本与 Trivy CLI 的兼容性,参数不识别是常见问题。自建 Jenkins 环境则可以直接用官方 Docker 镜像执行,减少宿主机上的额外依赖。
每次升级 Trivy 版本后,最好用一个带有已知漏洞的样例镜像执行一次扫描,确认退出码和 JSON 格式仍然符合预期。同时定期检查漏洞库的更新频率,离线环境尤其重要,否则扫描结果会偏离实际风险。
处理顺序归纳起来就是:先确认仓库和缓存,再用最小任务跑通扫描,然后绑定 digest 并人工核对报告,最后才设置硬性门禁和通知。每一步都留出人工复核的位置,镜像漏洞扫描提供的是风险证据,不是自动化的最终判决。