容器镜像安全扫描实战——Trivy 在 CI/CD 流水线中的完整接入
容器镜像安全扫描实战——Trivy 在 CI/CD 流水线中的完整接入
适读人群:DevOps 工程师、安全工程师、容器化项目开发者 | 阅读时长:约 14 分钟 | 核心价值:用 Trivy 扫描 Docker 镜像、文件系统和 IaC 配置,在 CI 中构建容器供应链安全防线
一年前,我认识的一个做云原生的朋友老霍,他们公司把所有服务都容器化部署了。有一次做等保测评,安全评估人员问他:"你们的容器基础镜像有没有扫描漏洞?"
老霍说:"我们用的是 ubuntu:22.04,应该没问题吧。"
评估人员在他面前跑了一下 Trivy,扫出来 73 个漏洞,其中 CRITICAL 级别 5 个,都是基础镜像里的系统库。
老霍当场懵了,说:"这些漏洞不是我写的代码引入的,是镜像里系统的,这也算?"
我后来告诉他:在容器安全里,镜像基础层的漏洞和代码层的漏洞一样危险——攻击者不会区分这是你写的代码还是你用的系统库。Trivy 就是专门解决这类问题的工具。
1. Trivy 是什么?
Trivy 是 Aqua Security 开源的全面安全扫描工具,支持扫描:
- 容器镜像:检测 OS 包(apt、yum)、应用依赖(npm、pip、maven、go)的 CVE
- 文件系统:扫描本地代码仓库
- Git 仓库:远程扫描
- Kubernetes 集群:扫描 K8s 资源配置
- IaC:Terraform、CloudFormation、Kubernetes YAML
- SBOM:生成软件物料清单
核心优势:快速(golang 实现)、准确率高、数据库自动更新、零依赖运行。
2. 安装与基础使用
# macOS
brew install trivy
# Linux
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin
# Docker 方式
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest image nginx:latest2.1 扫描 Docker 镜像
# 基础扫描
trivy image nginx:latest
# 只显示 CRITICAL 和 HIGH
trivy image --severity CRITICAL,HIGH nginx:latest
# 输出 JSON 格式
trivy image --format json --output report.json nginx:latest
# 扫描本地镜像(不 pull)
trivy image --input my-image.tar
# 失败阈值(发现 CRITICAL 漏洞时 exit code 非 0)
trivy image --exit-code 1 --severity CRITICAL nginx:latest2.2 扫描文件系统(代码仓库)
# 扫描当前目录(依赖文件 + Dockerfile + IaC)
trivy fs .
# 扫描指定目录
trivy fs --security-checks vuln,config ./src2.3 扫描 IaC 配置
# 扫描 Kubernetes YAML
trivy config ./k8s/
# 扫描 Dockerfile
trivy config Dockerfile
# 输出示例
# Dockerfile (dockerfile)
#
# Tests: 23 (SUCCESSES: 21, FAILURES: 2, EXCEPTIONS: 0)
# Failures: 2 (HIGH: 2, MEDIUM: 0, LOW: 0)
#
# HIGH: Specify at least 1 USER command in Dockerfile
# HIGH: Run as a high-privileged user3. Dockerfile 安全加固
Trivy 会扫描 Dockerfile 的配置安全问题,以下是修复常见问题的最佳实践:
# 安全问题多的 Dockerfile
FROM ubuntu:22.04
RUN apt-get install -y curl wget
COPY . /app
RUN pip install -r /app/requirements.txt
CMD ["python", "/app/main.py"]
# 安全加固版
FROM python:3.12-slim AS builder
WORKDIR /app
# 只复制依赖文件,利用 Docker 缓存
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
FROM python:3.12-slim
# 不以 root 运行
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
# 只复制运行所需文件
COPY --from=builder /root/.local /home/appuser/.local
COPY --chown=appuser:appuser src/ .
USER appuser
# 明确 expose 端口
EXPOSE 8080
# 使用 ENTRYPOINT + CMD 分离
ENTRYPOINT ["python"]
CMD ["main.py"]加固要点:
- 使用 slim/distroless 基础镜像,减少攻击面
- 多阶段构建,只保留运行时必需文件
- 不以 root 运行(USER 指令)
- 使用特定版本标签,不用
latest - 不在镜像里包含密钥和配置
4. 生成 SBOM(软件物料清单)
SBOM(Software Bill of Materials)是镜像里所有软件组件的清单,越来越多的合规要求需要它:
# 生成 SBOM(CycloneDX 格式)
trivy image --format cyclonedx --output sbom.json nginx:latest
# 生成 SBOM(SPDX 格式)
trivy image --format spdx-json --output sbom.spdx.json nginx:latest
# 用已有 SBOM 进行漏洞扫描
trivy sbom sbom.json5. GitHub Actions 完整集成
# .github/workflows/container-security.yml
name: Container Security Scan
on:
push:
branches: [main]
pull_request:
jobs:
build-and-scan:
name: Build & Security Scan
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # 用于上传 SARIF 结果到 GitHub Security
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
outputs: type=docker,dest=/tmp/myapp.tar
# === Trivy 扫描 ===
- name: Scan image with Trivy
uses: aquasecurity/trivy-action@master
with:
input: /tmp/myapp.tar
format: sarif
output: trivy-results.sarif
severity: 'CRITICAL,HIGH'
exit-code: '0' # 先不失败,先收集结果
# 上传到 GitHub Security 标签页(需要 Code scanning)
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarif
# 以 table 格式再次扫描,用于 CI 失败判断
- name: Trivy scan with failure gate
uses: aquasecurity/trivy-action@master
with:
input: /tmp/myapp.tar
format: table
severity: 'CRITICAL'
exit-code: '1' # CRITICAL 漏洞时 CI 失败
# 扫描 Dockerfile 配置
- name: Trivy config scan
uses: aquasecurity/trivy-action@master
with:
scan-type: config
scan-ref: .
format: table
exit-code: '1'
severity: 'HIGH,CRITICAL'
# 生成 SBOM
- name: Generate SBOM
uses: aquasecurity/trivy-action@master
with:
input: /tmp/myapp.tar
format: cyclonedx
output: sbom.json
- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.json6. 镜像漏洞处理策略
当 Trivy 扫描出漏洞时,按以下优先级处理:
# 查看漏洞详情和修复版本
trivy image --severity CRITICAL --format json nginx:latest | \
jq '.Results[].Vulnerabilities[] | {PkgName, InstalledVersion, FixedVersion, VulnerabilityID}'处理策略:
- 升级基础镜像:使用最新版本或 distroless 镜像
- 升级漏洞包:在 Dockerfile 里显式更新受影响的包:
RUN apt-get update && apt-get upgrade -y libssl-dev && rm -rf /var/lib/apt/lists/* - 接受风险(有文档):对于无修复版本的漏洞,通过
.trivyignore或 trivy policy 声明接受 - 切换基础镜像:从 ubuntu 换到 distroless 能消除大量系统包漏洞
.trivyignore 文件(类似 .gitignore):
# CVE-2024-xxxxx:经安全团队评估,此漏洞在我们的使用场景下不可触发
# 评估人:张工,日期:2024-03-01,复审日期:2024-09-01
CVE-2024-xxxxx
# Golang stdlib 漏洞,已在下一版本 Go 升级中修复
GO-2024-12347. 踩坑实录
踩坑记录 1:扫描 latest 镜像结果每天不同
如果你扫描 nginx:latest,今天和明天的结果可能不同(镜像更新了)。CI 里要固定镜像 SHA 或版本:
# 不推荐(会变)
FROM nginx:latest
# 推荐(固定版本)
FROM nginx:1.25.4-alpine
# 最严格(固定 SHA)
FROM nginx@sha256:abc123...踩坑记录 2:Trivy DB 更新失败导致 CI 卡住
Trivy 首次运行需要下载漏洞数据库(约 200MB),下载失败时会卡住或报错。解决方案:在 CI 里缓存 Trivy 数据库:
- name: Cache Trivy vulnerability database
uses: actions/cache@v4
with:
path: ~/.cache/trivy
key: trivy-db-${{ github.run_id }}
restore-keys: trivy-db-踩坑记录 3:误将 CI 里的扫描结果混淆了 PR 和 main 的策略
PR 阶段建议宽松(发现 CRITICAL 告警但不失败),给开发者时间处理;main 分支合并后严格(发现 CRITICAL 直接失败阻止部署)。两个阶段的策略要分开设置。
8. 容器镜像最小化实践
减少镜像攻击面,最直接的手段是减少镜像里的内容。
8.1 distroless 镜像
Google 的 distroless 镜像是目前最极致的最小化方案——它只包含应用运行所需的最少文件,没有 shell、没有包管理器、没有大多数系统工具:
# Go 应用使用 distroless
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags="-s -w" -o app ./cmd/app
# 最终镜像:只有 Go 运行时 + 应用二进制
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]distroless 的优点:没有 shell,攻击者即使突破容器也无法执行命令;没有包管理器,无法安装额外工具;镜像体积极小(几 MB 到几十 MB)。
缺点:调试困难,没有 bash/sh 无法直接进入容器排查问题。可以使用 kubectl debug 或者 distroless 的 debug 变体(gcr.io/distroless/static-debian12:debug)来调试。
8.2 镜像版本固定策略
# 不推荐:tag 可以被覆盖
FROM golang:1.22-alpine
# 推荐:固定 digest,完全不可变
FROM golang@sha256:1a2b3c4d...
# 中间方案:固定小版本
FROM golang:1.22.3-alpine3.20在 CI 里定期运行 docker pull 获取最新 digest,通过 Renovate Bot 自动更新 Dockerfile 里的 digest。既保持了安全更新,又保证了可重复构建。
8.3 运行时安全配置
Trivy 扫镜像漏洞是静态分析,运行时还需要安全配置来限制容器权限:
# Kubernetes Pod 安全上下文
securityContext:
runAsNonRoot: true
runAsUser: 10001
readOnlyRootFilesystem: true # 根文件系统只读
allowPrivilegeEscalation: false # 禁止提权
capabilities:
drop:
- ALL # 删除所有 Linux capabilities
add:
- NET_BIND_SERVICE # 只添加需要的Trivy 的 config 扫描(trivy config k8s/)能检查这些安全配置是否符合最佳实践。
9. 容器安全的成熟度路径
容器安全是一个有层次的工程领域,建议按照以下路径循序渐进:
Level 1(基础):
- Trivy 扫描所有镜像,CRITICAL 漏洞阻塞部署
- 使用非 root 用户运行容器
- 使用固定版本的基础镜像
Level 2(中级):
- 多阶段构建,最小化最终镜像
- 切换到 distroless 或 scratch 基础镜像
- 生成并存档 SBOM
- Kubernetes 安全上下文配置
Level 3(高级):
- 镜像签名(Sigstore/cosign)
- 运行时威胁检测(Falco)
- 镜像准入控制(OPA Gatekeeper)
- 定期自动扫描生产环境在运行的镜像
大多数团队做到 Level 2 就能覆盖绝大多数容器安全风险。Level 3 更多是金融、医疗等高合规要求行业的做法。
10. 容器安全的完整防御体系:从构建到运行时
Trivy 解决的是"构建时静态扫描"的问题,但容器安全需要覆盖整个生命周期:构建时、推送时、部署时、运行时。这一节描述一个完整的容器安全防御体系。
构建时(Build Time):在 Dockerfile 里用最小化基础镜像(distroless/scratch),不以 root 运行,多阶段构建减少最终镜像体积。在 CI 里用 Trivy 扫描,发现 CRITICAL 漏洞时阻断流水线。
推送时(Push Time):镜像推送到仓库前,验证 Trivy 扫描通过。用 Sigstore/cosign 对镜像进行签名,建立"这个镜像是经过验证的构建产出"的信任链:
# 签名镜像(需要 OIDC 认证,GitHub Actions 里直接可用)
cosign sign --oidc-issuer https://token.actions.githubusercontent.com ghcr.io/my-org/myapp:v1.2.3@sha256:abc123...
# 验证签名(部署时验证)
cosign verify ghcr.io/my-org/myapp:v1.2.3 --certificate-identity-regexp="^https://github.com/my-org/.*" --certificate-oidc-issuer="https://token.actions.githubusercontent.com"部署时(Deploy Time):Kubernetes 的 Admission Controller(如 OPA Gatekeeper 或 Kyverno)可以在 Pod 创建时验证镜像是否经过签名、是否有运行时安全配置(非 root、只读根文件系统、没有特权模式)。未满足条件的 Pod 被拒绝创建,防止未经审查的镜像进入生产集群。
运行时(Runtime):Falco 是一个运行时安全检测工具,监控容器里的系统调用,发现"容器里执行了 bash"、"写了不应该写的文件"、"建立了异常的网络连接"等行为,实时告警。这是静态扫描之外的最后一道防线。
11. 把容器安全融入工程文化
老霍那次等保测评事件,在他们公司引发了一场认真的讨论:"我们一直觉得用了主流发行版的镜像就是安全的,原来这个假设是错的。"
这种认知上的转变,比任何技术工具都重要。容器安全被忽视,通常不是因为工程师不重视安全,而是因为"安全"这个概念太模糊、太遥远——直到等保评估或者真实的漏洞事件把它具象化。
把 Trivy 集成进 CI,做的不只是技术层面的事,更重要的是建立一个即时反馈机制:工程师每次推代码,都能看到"这次引入了 2 个 HIGH 级漏洞,请在合并前处理"。这种即时、具体的反馈,比半年一次的安全培训效果好得多。
对于团队 Leader,推行容器安全的最有效方式不是发安全规定,而是先把 Trivy 跑起来,生成一份当前镜像的漏洞报告,让大家看到"我们现在有多少漏洞",然后一起制定修复计划。有了具体数据,讨论就不再是"安全很重要"这种空话,而是"这个 CRITICAL 漏洞影响了哪个服务,谁来处理,什么时候完成"。
容器安全不是一次性任务,而是持续的工程运营。漏洞数据库每天都在更新,今天安全的镜像,三个月后可能出现新的漏洞。定期扫描、及时更新基础镜像、建立漏洞修复的 SLA,这些是维持安全状态的工程基础设施。
12. 从合规驱动到工程驱动:容器安全的正确姿态
老霍那次经历,是典型的"合规驱动安全"——不是主动关注安全,而是被外部审计倒逼。这种状态下,安全措施往往是临时的、补救性的,而不是系统性的。
更成熟的安全实践,是从合规驱动转向工程驱动:不是为了通过审计而安全,而是因为理解了安全风险、认可了安全的工程价值而主动做安全。两者的区别,在于心态和动力的来源不同,长期效果差异很大。
工程驱动的容器安全实践,始于一个问题:如果明天有攻击者扫描我们的容器镜像,最容易被利用的漏洞是什么?Trivy 给你一份清单。然后是另一个问题:这些漏洞的修复成本和不修复的风险,哪个更大?大多数情况下,升级一个依赖包或者换一个更小的基础镜像,成本远低于处理一次安全事件的成本。
把这个成本-收益的思考带入工程文化,安全就不再是"合规要求",而是"工程常识"。每个工程师都能理解:一个已知漏洞的生产镜像,是一个已知的风险暴露点;修复它,不是为了通过审计,而是为了不在凌晨三点被紧急电话叫醒。
容器安全不是一个可以"完成"的工作,而是需要持续维护的工程状态。每周新增的 CVE、每次基础镜像更新、每次业务代码的依赖升级,都可能改变镜像的安全状态。把 Trivy 扫描集成进 CI,建立定期的基础镜像更新机制,是维持安全状态的工程基础设施,而不是一次性的安全加固动作。
在云原生时代,容器镜像就是你的软件交付单元,镜像安全就是软件安全。Trivy 把"镜像安全评估"这个以前需要专业安全工程师才能做的事,变成了每个开发者在 CI 里就能完成的例行检查。这种安全民主化,是容器技术生态最重要的进步之一。
容器安全不需要大量的专业安全知识就能开始实践——Trivy 的安装和使用是 10 分钟的事,把它集成进 CI 是半小时的事。这种低门槛高价值的安全实践,不需要等到"有专职安全工程师时再做",现在就可以开始,从今天的 PR 开始扫描。
每一个选择使用安全基础镜像、非 root 用户运行、固定版本标签的工程决策,都是在为用户和团队减少一点潜在的风险暴露。这些决策单独看很小,但积累起来,构成了容器安全的工程基础。Trivy 帮你发现已知的漏洞,良好的工程习惯帮你减少漏洞引入的机会,两者配合,才是完整的容器安全实践。
容器安全的完整实践,从 Trivy 开始,延伸到镜像签名、运行时安全策略、准入控制,是一个可以持续深化的工程旅程。不需要一步到位,但需要开始走。每一步深化,都在减少系统的攻击面,增加安全事件发生的成本。这种持续改善的心态,才是容器安全工程实践的正确状态。
容器化改变了软件分发的方式,也改变了安全的边界——你负责的不只是你写的代码,还包括你选择打包进镜像的所有软件。Trivy 帮你看清这个边界内的风险,而了解风险,是管理风险的第一步。从今天开始,让 Trivy 成为你的镜像安全守门人。
每次 Trivy 扫描都是对容器安全状态的一次诚实评估。
写在最后
容器化改变了软件交付方式,也带来了新的安全挑战。镜像里的漏洞来自三个层次:基础 OS 层、运行时层(JVM/Python 解释器)、应用依赖层——Trivy 能同时扫描这三层。
把 Trivy 集成进 CI,和把单测写进 CI 一样重要。在你的镜像还没被攻击者扫到之前,先让 Trivy 扫一遍。
下一篇我们聊测试环境的统一管理——Docker Compose + Testcontainers 如何解决"本地跑得好,CI 跑不起来"的问题。
