Docker 镜像安全扫描:Trivy 集成 CI、CVE 管理与基础镜像策略
系统讲解 Docker 镜像安全扫描体系:Trivy 的本地扫描与 CI 集成(GitLab CI/GitHub Actions 阻断流水线)、扫描结果的 CVE 管理(误报处理、.trivyignore 配置、漏洞追踪)、基础镜像安全选型策略(alpine vs slim vs distroless 安全对比)、Harbor Trivy 集成(推送时自动扫描)、镜像 SCA(软件成分分析)与 SBOM 生成,以及定期漏洞扫描的自动化方案。
“我的应用代码没有安全漏洞”——但基础镜像里的 OpenSSL 有。镜像安全扫描的核心价值是:在漏洞进入生产之前发现它,而不是等到被攻击后才知道。本文讲解以 Trivy 为核心的镜像安全扫描体系。
镜像漏洞的来源
镜像漏洞三个来源:
1. 基础镜像的系统包漏洞(OS 包)
ubuntu:20.04 中的 glibc、openssl、curl 等
→ 解决:更换基础镜像或 apt upgrade
2. 应用依赖的漏洞(语言包)
Python requirements.txt 中的 Django、requests
Node.js package.json 中的 lodash、axios
→ 解决:升级依赖版本
3. 应用代码本身的漏洞(SAST)
SQL 注入、命令注入、路径遍历
→ 超出本文范围,用 SAST 工具(SonarQube 等)
一、Trivy 本地扫描
1.1 安装
# Linux(APT)
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" \
| sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install trivy
# Linux(YUM/DNF)
cat > /etc/yum.repos.d/trivy.repo << 'EOF'
[trivy]
name=Trivy repository
baseurl=https://aquasecurity.github.io/trivy-repo/rpm/releases/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://aquasecurity.github.io/trivy-repo/rpm/public.key
EOF
yum install trivy
# macOS
brew install aquasecurity/trivy/trivy
# 或者直接用 Docker 镜像(无需安装)
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $HOME/.cache/trivy:/root/.cache/trivy \
aquasec/trivy:latest image myapp:v1
1.2 基础扫描命令
# 扫描镜像(最常用)
trivy image nginx:1.27-alpine
# 输出示例:
# nginx:1.27-alpine (alpine 3.19.1)
# ==================================
# Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
# ← alpine 通常漏洞很少
# 扫描 Ubuntu 基础镜像
trivy image ubuntu:20.04
# Total: 24 (LOW: 14, MEDIUM: 8, HIGH: 2, CRITICAL: 0)
# 只显示 HIGH 和 CRITICAL
trivy image --severity HIGH,CRITICAL myapp:v1
# 只显示有修复方案的漏洞(--ignore-unfixed 过滤无修复版本的漏洞)
trivy image --severity HIGH,CRITICAL --ignore-unfixed myapp:v1
# 扫描本地 tar 包(离线环境)
docker save myapp:v1 -o myapp.tar
trivy image --input myapp.tar
# 扫描目录(扫描 Dockerfile 和源码)
trivy fs .
# 扫描 Dockerfile(Misconfiguration 检测)
trivy config Dockerfile
# 检查:USER 是否为 root、是否暴露不必要端口等
# 生成 JSON 报告(机器可读)
trivy image --format json --output report.json myapp:v1
# 生成 SARIF 报告(GitLab/GitHub Security Dashboard)
trivy image --format sarif --output report.sarif myapp:v1
# 生成 SBOM(软件物料清单)
trivy image --format cyclonedx --output sbom.json myapp:v1
二、CI 集成(阻断流水线)
2.1 GitLab CI
# .gitlab-ci.yml
trivy-scan:
stage: scan
image:
name: aquasec/trivy:latest
entrypoint: [""]
variables:
# Trivy 漏洞库缓存(避免每次下载)
TRIVY_CACHE_DIR: .trivy-cache
cache:
key: trivy-db
paths:
- .trivy-cache/
before_script:
# 如果有 artifaacts 里的镜像 tar,先 load
- docker load < image.tar 2>/dev/null || true
script:
# 扫描:严重漏洞阻断流水线
- |
trivy image \
--exit-code 1 \
--severity HIGH,CRITICAL \
--ignore-unfixed \
--format sarif \
--output trivy-results.sarif \
--cache-dir .trivy-cache \
${IMAGE_NAME}:${IMAGE_TAG}
# 生成完整报告(不阻断,用于审查)
- |
trivy image \
--exit-code 0 \
--format json \
--output trivy-full-report.json \
--cache-dir .trivy-cache \
${IMAGE_NAME}:${IMAGE_TAG}
artifacts:
reports:
sast: trivy-results.sarif # GitLab Security Dashboard
paths:
- trivy-full-report.json
when: always
expire_in: 30 days
allow_failure: false
needs:
- job: build
artifacts: true
2.2 GitHub Actions
# .github/workflows/security-scan.yml
name: Security Scan
on:
push:
branches: [main]
pull_request:
jobs:
trivy-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: sarif
output: trivy-results.sarif
severity: HIGH,CRITICAL
ignore-unfixed: true
exit-code: 1
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarif
三、CVE 管理与误报处理
3.1 .trivyignore 文件
# 项目根目录放 .trivyignore,Trivy 自动读取
# 格式:每行一个 CVE ID
# 可以加注释说明为什么忽略
# 示例:.trivyignore
# 误报:此 CVE 的攻击向量不适用于我们的使用场景
CVE-2023-12345
# 已评估:glibc 漏洞,需要 local access 才能利用,我们是容器化应用
CVE-2023-4911
# 上游依赖尚未修复,等待 python:3.11-slim 发布修复版本
# 预计修复时间:2026-Q1
# 跟踪 Issue: https://github.com/xxx/yyy/issues/123
CVE-2024-11234
3.2 结构化 CVE 追踪
# 漏洞追踪脚本:扫描后与历史比较,只报告新增漏洞
#!/bin/bash
# scan-and-diff.sh
IMAGE=$1
REPORT_DIR=/var/lib/trivy-reports
mkdir -p "$REPORT_DIR"
CURRENT_REPORT="${REPORT_DIR}/$(date +%Y%m%d)-$(echo $IMAGE | tr '/:' '_').json"
PREV_REPORT=$(ls -t ${REPORT_DIR}/*.json 2>/dev/null | head -2 | tail -1)
# 扫描
trivy image \
--format json \
--severity HIGH,CRITICAL \
--ignore-unfixed \
--output "${CURRENT_REPORT}" \
"${IMAGE}"
# 提取 CVE 列表
CURRENT_CVES=$(jq -r '.[].Vulnerabilities[]?.VulnerabilityID' "${CURRENT_REPORT}" | sort -u)
if [ -f "${PREV_REPORT}" ]; then
PREV_CVES=$(jq -r '.[].Vulnerabilities[]?.VulnerabilityID' "${PREV_REPORT}" | sort -u)
NEW_CVES=$(comm -23 <(echo "$CURRENT_CVES") <(echo "$PREV_CVES"))
FIXED_CVES=$(comm -13 <(echo "$CURRENT_CVES") <(echo "$PREV_CVES"))
echo "=== 本次扫描结果 ==="
echo "新增漏洞(${NEW_CVES:+需要处理}):"
echo "${NEW_CVES:-(无新增漏洞)}"
echo ""
echo "已修复漏洞:"
echo "${FIXED_CVES:-(无)}"
fi
四、基础镜像安全选型
基础镜像安全性对比:
镜像 漏洞数(HIGH+CRITICAL) 大小
──────────────────────────────────────────────────────
ubuntu:22.04 ~20-50 个 80MB
debian:bookworm ~15-40 个 116MB
python:3.11 ~20-40 个(基于debian) 340MB
python:3.11-slim ~5-15 个 45MB
python:3.11-alpine ~0-5 个 20MB
python:3.11-bookworm ~20-40 个 380MB
gcr.io/distroless/python3 ~0-3 个 52MB(无 shell)
scratch 0 个 0MB(无 OS)
建议选型:
Go / Rust 静态编译 → scratch
Python 应用 → python:3.11-slim 或 distroless
Node.js 应用 → node:20-alpine
Java 应用 → eclipse-temurin:21-jre-alpine 或 distroless/java
需要调试工具 → debian:bookworm-slim(比 ubuntu 更精简)
# Python distroless 示例
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
COPY . .
FROM gcr.io/distroless/python3-debian12
COPY --from=builder /root/.local /root/.local
COPY --from=builder /app /app
WORKDIR /app
USER nonroot
ENV PATH=/root/.local/bin:$PATH
CMD ["app.py"]
# 注意:distroless 无 shell,无法 docker exec -it ... bash
# 调试时需要使用 debug 变体:
# FROM gcr.io/distroless/python3-debian12:debug
五、Harbor 集成 Trivy(推送时自动扫描)
# Harbor 安装时启用 Trivy(已在企业级部署章节讲解)
./install.sh --with-trivy
# 配置扫描策略(Harbor UI)
# 项目 → myproject → 配置 → 漏洞扫描
# ✓ 推送时自动扫描
# ✓ 阻止运行高危漏洞镜像(需要额外配置)
# API 触发扫描
curl -X POST \
-u admin:Harbor12345 \
"https://harbor.company.com/api/v2.0/projects/backend/repositories/api/artifacts/sha256:abc.../scan"
# 查看扫描结果
curl -s \
-u admin:Harbor12345 \
"https://harbor.company.com/api/v2.0/projects/backend/repositories/api/artifacts/sha256:abc...?with_scan_overview=true" \
| jq '.scan_overview'
六、定期扫描自动化
#!/bin/bash
# /usr/local/bin/harbor-scan-all.sh
# 定期扫描 Harbor 中所有镜像,发现新漏洞时告警
HARBOR_URL=https://harbor.company.com
HARBOR_USER=admin
HARBOR_PASS=Harbor12345
DINGTALK_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=xxx"
# 获取所有项目
PROJECTS=$(curl -s -u "${HARBOR_USER}:${HARBOR_PASS}" \
"${HARBOR_URL}/api/v2.0/projects?page_size=100" \
| jq -r '.[].name')
TOTAL_CRITICAL=0
TOTAL_HIGH=0
ALERTS=""
for PROJECT in $PROJECTS; do
# 获取每个项目的所有仓库和 tag
REPOS=$(curl -s -u "${HARBOR_USER}:${HARBOR_PASS}" \
"${HARBOR_URL}/api/v2.0/projects/${PROJECT}/repositories?page_size=100" \
| jq -r '.[].name')
for REPO in $REPOS; do
REPO_NAME=$(basename "$REPO")
# 只扫描最新 tag(production 或 latest)
for TAG in production latest; do
RESULT=$(trivy image \
--severity HIGH,CRITICAL \
--ignore-unfixed \
--format json \
--quiet \
"${HARBOR_URL}/${PROJECT}/${REPO_NAME}:${TAG}" 2>/dev/null)
if [ -n "$RESULT" ]; then
CRITICAL=$(echo "$RESULT" | jq '[.[].Vulnerabilities[]? | select(.Severity=="CRITICAL")] | length')
HIGH=$(echo "$RESULT" | jq '[.[].Vulnerabilities[]? | select(.Severity=="HIGH")] | length')
if [ "${CRITICAL:-0}" -gt 0 ] || [ "${HIGH:-0}" -gt 5 ]; then
ALERTS="${ALERTS}\n ${PROJECT}/${REPO_NAME}:${TAG} - CRITICAL:${CRITICAL} HIGH:${HIGH}"
TOTAL_CRITICAL=$((TOTAL_CRITICAL + CRITICAL))
TOTAL_HIGH=$((TOTAL_HIGH + HIGH))
fi
fi
done
done
done
# 如果有需要处理的漏洞,发送告警
if [ "${TOTAL_CRITICAL}" -gt 0 ] || [ "${TOTAL_HIGH}" -gt 20 ]; then
MSG="[Harbor 漏洞扫描报告] $(date '+%Y-%m-%d')\n"
MSG="${MSG}CRITICAL 漏洞:${TOTAL_CRITICAL} 个\n"
MSG="${MSG}HIGH 漏洞:${TOTAL_HIGH} 个\n"
MSG="${MSG}需要关注的镜像:${ALERTS}"
curl -s -X POST "${DINGTALK_WEBHOOK}" \
-H "Content-Type: application/json" \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"${MSG}\"}}"
fi
# 每周一凌晨 4 点执行
echo "0 4 * * 1 root /usr/local/bin/harbor-scan-all.sh >> /var/log/harbor-scan.log 2>&1" \
> /etc/cron.d/harbor-weekly-scan
小结
镜像安全扫描的实施路径:本地扫描(开发阶段)→ CI 阻断(构建阶段)→ Harbor 推送扫描(入库阶段)→ 定期扫描(存量检查),形成四道防线。
实践中最重要的一点:--ignore-unfixed 过滤无修复方案的漏洞,这能把扫描结果从“几十个红色警报”减少到“真正需要处理的几个”,避免团队对告警疲劳而忽视所有扫描结果。
基础镜像是漏洞的最大来源。从 ubuntu/debian 切换到 alpine 或 distroless,漏洞数通常可以减少 80% 以上,这比修复单个 CVE 效率高得多。
