site logo

Marico's space

Trivy vs Amazon Inspector:详解 AWS 上的容器镜像扫描

服务器技术 2026-11-10 14:49:30 5

最近折腾容器镜像安全扫描,踩了几个坑:Trivy 能卡住构建,却不会自动盯着已经上线的镜像;Amazon Inspector 能持续复扫,但成本和功能边界又跟 Trivy 不一样。这篇把 AWS 上的容器镜像扫描讲清楚,再给出一套能直接落地的 GitHub Actions 方案。

你把容器镜像推到 Amazon ECR(Amazon Elastic Container Registry,亚马逊弹性容器镜像仓库)之后,它真的可以安全运行吗?

在 AWS(Amazon Web Services,亚马逊云服务)上,常见答案有两类:一类是使用 AWS 自带的扫描能力,也就是 ECR basic scanning 或 Amazon Inspector;另一类是在 CI(Continuous Integration,持续集成)流程中运行 Trivy 这样的开源扫描器。本文会从零介绍两者,横向比较差异,并在 GitHub Actions 中搭一套实用的 Trivy 方案。这套方案还顺手解决了一个经常被忽略的问题:那些已经运行中的镜像。

不需要提前掌握 Trivy。

Trivy 是什么?

Trivy 是 Aqua Security 推出的开源安全扫描器。它只有一个可执行文件,你只要指定一个目标(target),它就会把问题扫出来。

它支持的扫描目标包括:

  • 容器镜像(trivy image)
  • 本地目录或 Git 仓库(trivy fs、trivy repo)
  • 基础设施即代码:Terraform、CloudFormation、Dockerfile、Kubernetes 清单(trivy config)
  • Kubernetes 集群(trivy k8s)
  • 已有的 SBOM(Software Bill of Materials,软件物料清单)(trivy sbom)

它会查找以下几类问题,对应的扫描器称为 scanners:

  • vuln:操作系统包(apk、apt、rpm 等)和语言包(pip、npm、Go modules、Maven、Cargo 等)里的已知 CVE(Common Vulnerabilities and Exposures,通用漏洞披露)
  • misconfig:不安全配置,例如安全组对 0.0.0.0/0 开放,或者容器以 root 身份运行
  • secret:遗留在文件或镜像层里的 API(应用程序接口)密钥和密码
  • license:依赖项使用的许可证

它是怎么工作的

Trivy 会维护一份由公开数据源构建的漏洞数据库,包括 NVD(National Vulnerability Database,美国国家漏洞数据库)、GitHub Advisory Database、各发行版安全公告等。这个数据库每天会更新多次。每次运行时,Trivy 会:

  1. 下载最新数据库,或者直接使用本地缓存;
  2. 盘点目标里有什么,包括操作系统、包名和版本;
  3. 将盘点结果与漏洞数据库进行匹配。

扫描过程中不会把任何内容上传出去,镜像也不会离开本机或 CI Runner。

第一次扫描

# macOS
brew install trivy # Scan a public image
trivy image python:3.12-slim
Enter fullscreen mode Exit fullscreen mode

输出结果会按层类型拆成表格:先列操作系统包,再列语言包。每一行都会展示软件包、已安装版本、CVE 编号、严重程度和修复版本。修复版本是最值得看的字段,因为它能直接告诉你升级有没有用。

下面几个参数基本每天都会用到:

# Only what matters, and only what you can actually fix today
trivy image --severity HIGH,CRITICAL --ignore-unfixed python:3.12-slim # Fail the command (exit code 1) if anything is found: this is your CI gate
trivy image --severity CRITICAL --exit-code 1 myapp:latest # Machine-readable output
trivy image --format json --output result.json myapp:latest
trivy image --format sarif --output result.sarif myapp:latest # Generate an SBOM (list of everything inside the image)
trivy image --format cyclonedx --output sbom.json myapp:latest
Enter fullscreen mode Exit fullscreen mode

如果团队明确接受某项风险,可以在执行 Trivy 的目录旁放一个 .trivyignore 文件,把对应 CVE 列进去:

# Not reachable in our usage; revisit 2027-01-01
CVE-2024-12345
Enter fullscreen mode Exit fullscreen mode

扫描私有 ECR 镜像时,直接使用正常的 AWS 凭证即可。Trivy 会用这些凭证向 ECR 完成认证:

aws sso login --profile dev
AWS_PROFILE=dev trivy image 123456789012.dkr.ecr.eu-central-1.amazonaws.com/myapp:1.4.0
Enter fullscreen mode Exit fullscreen mode

AWS 原生提供什么

ECR basic scanning

这项能力免费,已经集成在 ECR 中。你只需开启 scan on push,或者手动触发扫描,扫描结果就会出现在 ECR 控制台的镜像旁边。它的限制也很直接:

  • 只覆盖操作系统包,不会检查 Python 或 Node.js 依赖。
  • 镜像只会在推送时扫描一次,除非你再次手动触发。

ECR enhanced scanning(Amazon Inspector)

把 ECR 切换到 enhanced scanning 后,Amazon Inspector 会接管扫描,并补上这些能力:

  • 在操作系统包之外,增加语言包扫描;
  • 持续扫描:新 CVE 发布后,Inspector 会重新评估已经扫描过的镜像。镜像最后一次推送或拉取后需要持续监控多久,可以自行设置;
  • 结果会进入 Inspector 控制台、Security Hub 和 EventBridge,因此可以继续分发到 Slack、Jira 等系统。

Amazon Inspector 不只扫容器

Inspector 还能扫描 EC2 实例,既可以通过 SSM 部署代理,也可以通过 EBS 快照无代理扫描;它还支持扫描 Lambda 函数。

Inspector 会给出一个 Inspector score:它会结合网络可达性等上下文修正 CVSS(Common Vulnerability Scoring System,通用漏洞评分系统)分数。例如,同一个严重 CVE,如果只存在于无法从公网访问的实例上,排序会低于位于公网负载均衡器后侧的同类漏洞。

Inspector 按资源计费。ECR 的费用包括镜像首次扫描的按次收费,以及每次复扫的较低费用;EC2 则按实例和月份收费。具体价格应以所在区域的 Inspector 定价页面为准。还有个很容易漏掉的细节:使用 Auto Scaling 弹性伸缩组时,只要实例存活一段时间就会计费,账单增长速度可能超出预期。

关键区别:扫描什么时候发生

Trivy 是时点扫描,你什么时候运行,它就什么时候扫描。Inspector 则是持续扫描,外部世界发生变化时,它会重新扫描。

这件事比功能列表更重要。假设实际情况是这样的:

  1. 周一:你构建了 myapp:1.4.0,Trivy 扫描通过,于是将它部署上线。
  2. 周三:该镜像内部使用的某个库被曝出一个严重 CVE。
  3. 由于代码没有任何变化,没有人重新构建 myapp:1.4.0。

Inspector 会在周三标记这个问题,而放在 CI 中运行的 Trivy 再也不会看它一眼。

所以,如果用 Trivy 替代 Inspector,就必须自己安排定期复扫。好消息是,这只需要几行 YAML,稍后直接给出。

横向对比

ECR basic Inspector(ECR enhanced) Trivy
成本 免费 按镜像扫描和复扫收费 免费(开源)
操作系统包 ✅ ✅ ✅
语言包 ❌ ✅(需要确认支持的生态) ✅(支持范围更广)
新 CVE 发布后复扫 ❌ ✅ 自动执行 仅在自行安排任务时执行
运行位置 ECR AWS 托管服务 任意位置:本地、CI、集群
推送前扫描/中断构建 ❌ 可以通过 Inspector 的 CI/CD scanning API 实现 ✅ 原生支持(--exit-code)
结果去向 ECR 控制台 Inspector、Security Hub、EventBridge 由你决定:终端、JSON、SARIF、GitHub
IaC、密钥、许可证 ❌ 部分支持(Lambda 代码扫描) ✅
EC2/Lambda ❌ ✅ ❌(不是托管服务)
上下文感知评分 ❌ ✅ ❌(仅使用基础严重程度)
AWS 之外可用 ❌ ❌ ✅

在 GitHub Actions 中落地 Trivy

下面给出两个工作流:一个在镜像发布前执行扫描,另一个每天重新扫描正在运行的镜像。

1. 在 CI 中扫描,推送到 ECR 之前执行

name: build on: push: branches: [main] permissions: contents: read id-token: write # OIDC to AWS, no access keys jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Build image run: docker build -t myapp:${{ github.sha }} . - name: Scan image uses: aquasecurity/trivy-action@<pinned-sha> # pin to a commit SHA with: image-ref: myapp:${{ github.sha }} severity: CRITICAL,HIGH ignore-unfixed: true exit-code: "1" # fail the job, so the push below never runs - uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: arn:aws:iam::123456789012:role/github-actions-ecr-push aws-region: eu-central-1 - id: ecr uses: aws-actions/amazon-ecr-login@v2 - name: Push run: | IMAGE=${{ steps.ecr.outputs.registry }}/myapp:${{ github.sha }} docker tag myapp:${{ github.sha }} "$IMAGE" docker push "$IMAGE"
Enter fullscreen mode Exit fullscreen mode

只要发现已有修复版本的 HIGH 或 CRITICAL CVE,任务就会失败,镜像也就无法进入 ECR。扫描结果表会直接打印在任务日志里,开发者不用再跳转到别的地方找。

ignore-unfixed 能让这道门禁更合理:如果某个 CVE 暂时没有修复版本,它会卡住每一次构建,却不给团队提供任何可执行的修复方案。

这里故意没有创建 Issue。提交代码的人会立刻看到任务失败,存在漏洞的镜像也不会发布。Issue 应该留给已经运行在生产环境中的问题,这正是下一个工作流要处理的。

卡构建还是只报告? 这个示例采用了最严格的方案:发现漏洞就阻止推送。很多团队并不想这么做。公共基础镜像一旦曝出新 CVE,所有构建可能在同一天集体变红,最后大家只会把漏洞全塞进 .trivyignore,先把镜像发出去再说。

更温和的做法是先推送镜像,扫描失败不中断构建,再为发现的问题创建 Issue。实现时,在扫描步骤中加入 continue-on-error: true,并复用下一个工作流中的 Issue 步骤,同时添加 if: steps.scan.outcome == 'failure'。还可以取个中间值:只对 CRITICAL 卡构建,其余级别统一用 Issue 跟踪。具体选哪种,取决于团队的风险偏好和交付节奏。

2. 每天扫描 ECS 中真正运行的镜像

这部分用来替代 Inspector 的持续扫描。不要重新扫描 ECR 中的全部镜像,因为其中大多数都是没人再运行的旧构建。更省事的做法是让 ECS 告诉我们当前到底运行着哪些镜像,然后只扫这些。

name: scan-running-images on: schedule: - cron: "0 6 * * *" # every day workflow_dispatch: permissions: contents: read id-token: write issues: write # open an issue when a running image is vulnerable jobs: list: runs-on: ubuntu-latest outputs: images: ${{ steps.list.outputs.images }} steps: - uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: arn:aws:iam::123456789012:role/github-actions-ecs-read aws-region: eu-central-1 - id: list name: List images of running ECS tasks run: | for cluster in $(aws ecs list-clusters --query 'clusterArns[]' --output text); do tasks=$(aws ecs list-tasks --cluster "$cluster" --desired-status RUNNING \ --query 'taskArns[]' --output text) [ -n "$tasks" ] || continue # Unquoted on purpose: one argument per task ARN (max 100 per call) # shellcheck disable=SC2086 aws ecs describe-tasks --cluster "$cluster" --tasks $tasks \ --query 'tasks[].containers[].image' --output text done | tr '\t' '\n' | { grep '\.dkr\.ecr\.' || true; } | sort -u > images.txt echo "images=$(jq -Rsc 'split("\n") | map(select(length > 0))' images.txt)" >> "$GITHUB_OUTPUT" cat images.txt scan: needs: list if: needs.list.outputs.images != '[]' runs-on: ubuntu-latest strategy: fail-fast: false matrix: image: ${{ fromJson(needs.list.outputs.images) }} steps: - uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: arn:aws:iam::123456789012:role/github-actions-ecs-read aws-region: eu-central-1 - uses: aws-actions/amazon-ecr-login@v2 - name: Scan id: scan uses: aquasecurity/trivy-action@<pinned-sha> with: image-ref: ${{ matrix.image }} severity: CRITICAL,HIGH ignore-unfixed: true exit-code: "1" - name: Open an issue (once per image) if: failure() && steps.scan.outcome == 'failure' env: GH_TOKEN: ${{ github.token }} IMAGE: ${{ matrix.image }} run: | export title="Vulnerable image running in production: $IMAGE" # search narrows the list; the jq filter keeps exact title matches only open=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \ --search "\"$title\" in:title" --json title \ --jq 'map(select(.title == env.title)) | length') if [ "$open" -eq 0 ]; then gh issue create --repo "$GITHUB_REPOSITORY" --title "$title" \ --body "The daily Trivy scan found fixable HIGH/CRITICAL CVEs. Details: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" fi
Enter fullscreen mode