技术博客

Docker 镜像安全扫描:Trivy 集成 CI、CVE 管理与基础镜像策略

系统讲解 Docker 镜像安全扫描体系:Trivy 的本地扫描与 CI 集成(GitLab CI/GitHub Actions 阻断流水线)、扫描结果的 CVE 管理(误报处理、.trivyignore 配置、漏洞追踪)、基础镜像安全选型策略(alpine vs slim vs distroless 安全对比)、Harbor Trivy 集成(推送时自动扫描)、镜像 SCA(软件成分分析)与 SBOM 生成,以及定期漏洞扫描的自动化方案。

Docker安全TrivyCVE镜像扫描SBOM供应链安全DevSecOps

“我的应用代码没有安全漏洞”——但基础镜像里的 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 效率高得多。