概述
扫描 Dockerfile 或锁文件并不等于扫描镜像。运行时镜像包含基础层的操作系统包,而基础镜像的 CVE 恰恰出现在这里。应当扫描已推送的镜像,并且扫描 digest 而不是 tag,因为 tag 可能在扫描与部署之间被移动。把扫描放在与构建、推送相同的 job 中,位于任何发布步骤之前,失败时直接阻断发布,而不是事后生成一份报告。
步骤
- 扫描镜像,忽略暂无修复的漏洞,高危与严重级别直接失败:
trivy image --ignore-unfixed --severity HIGH,CRITICAL \
--exit-code 1 ghcr.io/acme/api:$GITHUB_SHA
- 缓存漏洞数据库,避免每次 job 都重新下载:
- uses: actions/cache@v4
with:
path: ~/.cache/trivy
key: trivy-db-${{ github.run_id }}
restore-keys: trivy-db-
- 输出 SARIF,便于在仓库安全页集中跟踪:
trivy image --ignore-unfixed --severity HIGH,CRITICAL \
--format sarif -o trivy.sarif ghcr.io/acme/api:$GITHUB_SHA
- 部署前按 digest 再扫描一次实际要运行的镜像:
trivy image --exit-code 1 --severity CRITICAL \
ghcr.io/acme/api@sha256:6f1c2a9d4b7e8f0a1c3d5e7f9b2a4c6d8e0f1a3b5c7d9e1f3a5b7c9d1e3f5a7b
验证
验证分两步。先确认门禁会失败:临时用带已知可修复 CVE 的旧基础镜像构建,流水线应在推送后、发布前停止。再确认门禁会通过:修复后重新构建,扫描退出码为 0,SARIF 结果与镜像 digest 对应。若扫描在数分钟后才报错,说明它被放在了后续工作流中,应移动到构建 job 内。
注意事项
- 镜像扫描无法发现业务逻辑漏洞,也可能漏掉不经系统包管理器安装的语言依赖。
--ignore-unfixed会隐藏真实风险,应把未修复列表放入定期报告单独评审。- 例外必须写明 CVE、原因与到期时间,并在到期后自动失效。
- 即使代码未变,也应定期重建并重扫,因为新披露的 CVE 需要新的扫描才能发现。
- 扫描 tag 会留下时间窗,最终必须以 digest 为准。