## 18.6 容器镜像安全扫描与供应链安全

在 DevOps 流程中，容器镜像安全已经成为不容忽视的关键环节。从开发、构建、存储到部署，镜像的整个生命周期都需要安全防护。本节深入讨论镜像漏洞扫描、软件物料清单（SBOM）、镜像签名验证等供应链安全实践。

### 18.6.1 容器镜像漏洞扫描工具对比

#### Trivy - 轻量级通用扫描器

Trivy 是由 Aqua Security 开发的开源漏洞扫描器，以其轻量级、快速、准确而闻名，已成为业界标准。

**优点：**
- 零依赖，单个二进制文件
- 扫描速度快（秒级）
- 支持镜像、文件系统、Git 仓库多种扫描源
- 数据库每日自动更新
- 支持多种输出格式（JSON、表格、SBOM 等）

**安装与基本使用：**

```bash
# 安装 Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin

# 扫描本地镜像
trivy image nginx:latest

# 生成 JSON 格式报告
trivy image -f json -o report.json nginx:latest

# 扫描文件系统
trivy fs /path/to/project

# 扫描 Git 仓库
trivy repo https://github.com/aquasecurity/trivy
```

**在 CI/CD 中集成：**

```bash
# 设置严重程度过滤
trivy image --severity HIGH,CRITICAL \
  --exit-code 1 \
  myregistry.com/myapp:v1.0.0
```

#### Grype - 支持多种软件包的扫描器

Grype 由 Anchore 开发，支持更广泛的软件包管理器和语言。

**优点：**
- 支持 Java、Python、Go、Ruby、JavaScript 等多种语言的依赖检测
- 与 Syft（SBOM 生成器）配合效果好
- 可自定义漏洞数据库源
- 支持离线扫描模式

**安装与使用：**

```bash
# 安装 Grype
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin

# 扫描镜像
grype docker:nginx:latest

# 与 Syft 配合生成 SBOM
syft docker:nginx:latest -o json > sbom.json
grype sbom:sbom.json

# 扫描特定目录
grype dir:/path/to/app
```

#### Snyk - 完整的安全平台

Snyk 提供了商业级的安全扫描服务，特别适合企业环境。

**特点：**
- 支持开源漏洞和许可证扫描
- 与多个 Git 平台深度集成（GitHub、GitLab、Bitbucket）
- 提供修复建议和自动化修复 PR
- 支持 Kubernetes 部署后安全监控

**基本使用：**

```bash
# 安装 Snyk CLI
npm install -g snyk

# 认证
snyk auth

# 扫描镜像
snyk container test docker-archive://image.tar

# 监控仓库
snyk monitor --docker
```

**工具对比表：**

| 特性 | Trivy | Grype | Snyk |
|------|-------|-------|------|
| 零依赖 | ✓ | ✗ | ✗ |
| 离线模式 | ✓ | ✓ | ✗ |
| 许可证扫描 | ✗ | ✓ | ✓ |
| 自动修复 | ✗ | ✗ | ✓ |
| 开源免费 | ✓ | ✓ | 部分 |
| IDE 集成 | ✓ | ✓ | ✓ |

### 18.6.2 SBOM（软件物料清单）生成与管理

SBOM（Software Bill of Materials）是一份详细列表，记录了软件中使用的所有组件、依赖库及其版本信息。SBOM 在供应链安全中至关重要，特别是在发现新的安全漏洞时，能快速定位受影响的应用。

#### Syft - SBOM 生成工具

Syft 是 Anchore 推出的专业 SBOM 生成工具。

**安装：**

```bash
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
```

**生成 SBOM：**

```bash
# 从镜像生成 SBOM（多种格式）
syft docker:nginx:latest -o json > sbom.json
syft docker:nginx:latest -o spdx > sbom.spdx
syft docker:nginx:latest -o cyclonedx > sbom.xml

# 从本地文件系统生成
syft dir:/path/to/app -o json > sbom.json

# 从 OCI 镜像档案生成
syft oci-archive:image.tar -o json > sbom.json
```

#### CycloneDX 与 SPDX 格式

两种主流的 SBOM 格式：

**CycloneDX 格式示例：**

```xml
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" version="1">
  <components>
    <component type="library">
      <name>openssl</name>
      <version>1.1.1k</version>
      <purl>pkg:deb/debian/openssl@1.1.1k-1+deb11u5</purl>
    </component>
    <component type="library">
      <name>curl</name>
      <version>7.74.0-1.3+deb11u1</version>
      <purl>pkg:deb/debian/curl@7.74.0-1.3+deb11u1</purl>
    </component>
  </components>
</bom>
```

**SPDX 格式示例：**

```json
{
  "SPDXID": "SPDXRef-DOCUMENT",
  "spdxVersion": "SPDX-2.2",
  "creationInfo": {
    "created": "2024-03-01T12:00:00Z",
    "creators": ["Tool: syft"]
  },
  "packages": [
    {
      "SPDXID": "SPDXRef-Package-openssl",
      "name": "openssl",
      "versionInfo": "1.1.1k",
      "downloadLocation": "NOASSERTION"
    }
  ]
}
```

#### SBOM 的应用场景

**漏洞关联：**

当新的 CVE 被发现时，可快速查询受影响的应用：

```bash
# 使用 Grype 针对 SBOM 进行漏洞扫描
grype sbom:sbom.json --add-cpes-if-none
```

**合规性报告：**

将 SBOM 保存为构建产物，用于审计和合规性检查。

**依赖升级决策：**

通过分析 SBOM 中的依赖版本，制定安全升级计划。

### 18.6.3 镜像签名与验证（Cosign/Notary）

镜像签名确保镜像的来源可信且未被篡改。两种主流方案是 Cosign 和 Notary。

#### Cosign - 现代签名解决方案

Cosign 是 Sigstore 项目的核心工具，支持无密钥签名，适合现代 CI/CD 流程。

**安装：**

```bash
wget https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x cosign-linux-amd64
sudo mv cosign-linux-amd64 /usr/local/bin/cosign
```

**生成密钥对（传统方式）：**

```bash
cosign generate-key-pair
# 生成 cosign.key 和 cosign.pub
```

**签名镜像：**

```bash
# 使用私钥签名（推送到仓库前）
cosign sign --key cosign.key myregistry.com/myapp:v1.0.0

# 系统会提示输入私钥密码
```

**验证签名：**

```bash
# 使用公钥验证
cosign verify --key cosign.pub myregistry.com/myapp:v1.0.0

# 输出结果示例
# Verification successful!
# {
#   "critical": {
#     "identity": {...},
#     "image": {...},
#     "type": "cosign container image signature"
#   },
#   "optional": {...}
# }
```

**Keyless 签名（推荐用于 CI/CD）：**

```bash
# 在 GitHub Actions 等 CI 中无需存储密钥
cosign sign --yes myregistry.com/myapp:v1.0.0

# 验证时自动使用 OIDC 令牌验证身份
cosign verify myregistry.com/myapp:v1.0.0 \
  --certificate-identity https://github.com/myorg/myrepo/.github/workflows/build.yml@refs/heads/main \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
```

#### Docker Content Trust（DCT）与 Notary

Docker Content Trust 使用 Notary 实现镜像签名，是 Docker 官方的签名解决方案。

**启用 DCT：**

```bash
# 在环境中启用 DCT
export DOCKER_CONTENT_TRUST=1

# 此后所有 docker push/pull 都需要签名
docker push myregistry.com/myapp:v1.0.0
# 如果镜像未签名，操作会被拒绝

# 禁用 DCT（仅用于特定操作）
docker push --disable-content-trust myregistry.com/myapp:v1.0.0
```

**签名密钥管理：**

```bash
# 首次推送时会提示创建 Delegation Key
# 密钥存储在 ~/.docker/trust/private/root_keys/ 和 ~/.docker/trust/private/tuf_keys/

# 查看签名信息
docker inspect --format='{{.RepoDigests}}' myregistry.com/myapp:v1.0.0
```

### 18.6.4 供应链安全最佳实践

#### 1. 基础镜像安全

```dockerfile
# ❌ 不推荐：使用 latest 标签
FROM ubuntu:latest
RUN apt-get update && apt-get install -y curl

# ✓ 推荐：固定基础镜像版本和摘要
FROM ubuntu:22.04@sha256:a6d2b38300ce017add71440577d5b0a90460d0e6...
RUN apt-get update && apt-get install -y curl=7.68.0-1ubuntu1
```

#### 2. 构建时扫描

在 Dockerfile 中集成安全扫描：

```dockerfile
FROM golang:1.20-alpine AS builder
WORKDIR /app
COPY . .

# 使用 Trivy 扫描源代码
RUN apk add --no-cache curl && \
    curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin && \
    trivy fs . --exit-code 1 --severity HIGH,CRITICAL

RUN go build -o app .

FROM alpine:3.17@sha256:abcd...
COPY --from=builder /app/app /app
```

#### 3. 运行时镜像扫描策略

```bash
# 镜像构建完成后立即扫描
trivy image --severity HIGH,CRITICAL \
  --exit-code 1 \
  --timeout 30m \
  $IMAGE_NAME:$IMAGE_TAG

# 定期扫描已部署的镜像
trivy image --scanners vuln,misconfig registry:5000/myapp:latest
```

#### 4. 镜像仓库安全配置

**Harbor（私有镜像仓库）的安全扫描：**

```yaml
# harbor.yml 配置示例
trivy:
  enabled: true
  # 启用镜像扫描
  image_source: "Official"

# 默认扫描配置
scan_on_push: true  # 推送时自动扫描
scan_all: true      # 扫描仓库中的所有镜像
```

#### 5. 政策执行（Admission Controller）

在 Kubernetes 环境中使用 Admission Webhook 强制镜像签名和扫描：

```yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: image-security-policy
webhooks:
- name: image-security.example.com
  clientConfig:
    service:
      name: image-security-webhook
      namespace: security
      path: "/validate"
  rules:
  - operations: ["CREATE", "UPDATE"]
    apiGroups: [""]
    apiVersions: ["v1"]
    resources: ["pods"]
  admissionReviewVersions: ["v1"]
  sideEffects: None
```

### 18.6.5 CI/CD 中集成安全扫描

#### GitHub Actions 工作流示例

```yaml
name: Build and Scan Image

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      security-events: write

    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2

    - name: Build Docker image
      uses: docker/build-push-action@v4
      with:
        context: .
        push: false
        load: true
        tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

    - name: Run Trivy vulnerability scan
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
        format: 'sarif'
        output: 'trivy-results.sarif'
        severity: 'HIGH,CRITICAL'

    - name: Upload Trivy results to GitHub Security tab
      uses: github/codeql-action/upload-sarif@v2
      with:
        sarif_file: 'trivy-results.sarif'

    - name: Generate SBOM
      uses: anchore/sbom-action@v0
      with:
        image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
        format: cyclonedx-json
        output-file: sbom-cyclonedx.json

    - name: Upload SBOM
      uses: actions/upload-artifact@v3
      with:
        name: sbom
        path: sbom-cyclonedx.json

    - name: Sign image with Cosign
      if: github.event_name == 'push'
      env:
        COSIGN_EXPERIMENTAL: 1
      run: |
        cosign sign --yes ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

    - name: Login to Registry and Push
      if: github.event_name == 'push'
      uses: docker/login-action@v2
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Push image
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
```

#### GitLab CI 工作流示例

```yaml
stages:
  - build
  - scan
  - sign
  - push

variables:
  REGISTRY: registry.gitlab.com
  IMAGE_NAME: $REGISTRY/$CI_PROJECT_PATH

build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t $IMAGE_NAME:$CI_COMMIT_SHA .
    - docker save $IMAGE_NAME:$CI_COMMIT_SHA > image.tar

scan:trivy:
  stage: scan
  image: aquasec/trivy:latest
  script:
    - trivy image --severity HIGH,CRITICAL --exit-code 1 docker-archive://image.tar
  allow_failure: false

scan:grype:
  stage: scan
  image: anchore/grype:latest
  script:
    - grype docker-archive://image.tar

generate:sbom:
  stage: scan
  image: anchore/syft:latest
  script:
    - syft docker-archive://image.tar -o cyclonedx > sbom.xml
  artifacts:
    reports:
      sbom: sbom.xml

sign:
  stage: sign
  image: gcr.io/projectsigstore/cosign:latest
  script:
    - cosign sign --key $COSIGN_KEY $IMAGE_NAME:$CI_COMMIT_SHA
  only:
    - main

push:
  stage: push
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker load < image.tar
    - docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD $REGISTRY
    - docker push $IMAGE_NAME:$CI_COMMIT_SHA
  only:
    - main
```

### 18.6.6 常见问题与最佳实践

**Q: 扫描报告中有过时的 CVE，如何处理？**

A: 某些 CVE 可能已经被修复但数据库未更新，可以：
- 手动验证安全补丁是否已应用
- 使用工具的忽略列表功能（如 Trivy 的 `.trivyignore`）
- 定期更新扫描工具和漏洞数据库

**Q: 如何平衡镜像大小和安全性？**

A:
- 使用多阶段构建减少最终镜像大小
- 使用精简基础镜像（Alpine、Distroless）
- 定期更新依赖而不是一味求小
- 优先安全性，体积次之

**Q: 如何管理和轮换签名密钥？**

A:
- 在密钥管理系统（如 HashiCorp Vault）中存储密钥
- 定期轮换密钥（建议每 90 天）
- 使用 Keyless 签名消除密钥管理复杂性
- 保留密钥轮换的审计日志
