技术博客

Docker 镜像供应链安全:Cosign 签名验证、SBOM 生成与内容信任

系统讲解 Docker 镜像供应链安全的完整方案:Cosign 对镜像进行密钥签名与验证(集成到 CI/CD 确保只有签名镜像才能部署)、SBOM(软件物料清单)的生成与存储(CycloneDX/SPDX 格式)、OCI Artifact 标准的 attestation 附加到镜像、Docker Content Trust(Notary v1)的配置与局限性、Sigstore 无密钥签名方案(keyless signing),以及供应链攻击的常见场景与防御策略。

Docker安全CosignSBOM供应链安全Sigstore内容信任DevSecOps

SolarWinds 供应链攻击、Log4Shell 漏洞——近年来最严重的安全事件都发生在软件供应链环节,而不是应用代码本身。Docker 镜像是软件供应链的核心节点:一个被污染的基础镜像或被替换的镜像,可以影响下游所有使用者。本文讲解镜像供应链安全的核心实践。

供应链攻击场景

常见供应链攻击向量:

1. 镜像仓库攻击
   攻击者替换 Harbor 中的镜像(相同 tag,不同内容)
   防御:Cosign 签名验证,确保内容未被篡改

2. 基础镜像投毒
   拉取了含恶意代码的 python:3.11-slim
   防御:验证官方镜像签名 + Trivy 扫描

3. 依赖包劫持
   pip install requests 安装了同名恶意包
   防御:SBOM 追踪所有依赖 + 包完整性校验

4. CI/CD 管道攻击
   攻击者修改 CI 脚本,在构建时注入恶意代码
   防御:签名策略(只有受信任的 CI 才能产生有效签名)

一、Cosign 镜像签名

Cosign 是 Sigstore 项目的一部分,专为容器镜像签名设计,支持传统密钥签名和无密钥(keyless)签名。

1.1 安装 Cosign

# Linux(二进制)
COSIGN_VERSION=$(curl -s https://api.github.com/repos/sigstore/cosign/releases/latest | jq -r '.tag_name')
curl -L "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64" \
    -o /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign

# 验证安装
cosign version

1.2 密钥对签名(企业内网环境)

# 生成密钥对(私钥用于签名,公钥用于验证)
cosign generate-key-pair
# Enter password for private key:
# Enter password for private key again:
# Private key written to cosign.key
# Public key written to cosign.pub

# 安全存储私钥
# ⚠️ 私钥不能提交 Git!存储到 CI 的 Secret 变量中

# 签名镜像(推送后签名)
IMAGE=harbor.company.com/backend/api:v1.2.3
cosign sign \
    --key cosign.key \
    --yes \
    ${IMAGE}
# 会在 Harbor 中创建一个 .sig artifact,与镜像关联

# 验证签名
cosign verify \
    --key cosign.pub \
    ${IMAGE}
# Verification for harbor.company.com/backend/api:v1.2.3 --
# The following checks were performed on each of these signatures:
#   - The cosign claims were validated
#   - The signatures were verified against the specified public key
# {"critical":{"identity":{"docker-reference":"harbor.company.com/backend/api"},...}}

1.3 无密钥签名(Keyless Signing)

# Keyless 方案:通过 OIDC 身份(GitHub/GitLab/Google 账户)签名
# 不需要管理长期密钥
# 签名记录写入 Sigstore 的公共透明日志(Rekor)

# GitHub Actions 中的 Keyless 签名
# .github/workflows/sign.yml
- name: Sign image with Keyless
  env:
    COSIGN_EXPERIMENTAL: 1
  run: |
    cosign sign \
      --yes \
      harbor.company.com/backend/api:${{ github.sha }}
    # 自动使用 OIDC token(github.token)作为身份证明
    # 签名包含:GitHub Actions 运行的 workflow、repo、SHA

# 验证 Keyless 签名
cosign verify \
    --certificate-identity "https://github.com/company/api/.github/workflows/release.yml@refs/heads/main" \
    --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
    harbor.company.com/backend/api:v1.2.3

1.4 在 CI 中集成签名(GitLab CI)

# .gitlab-ci.yml
sign-image:
  stage: sign
  image: cgr.dev/chainguard/cosign:latest
  
  variables:
    COSIGN_YES: "true"
  
  script:
    # 使用存储在 CI 变量中的私钥签名
    - echo "${COSIGN_PRIVATE_KEY}" > /tmp/cosign.key
    - |
      cosign sign \
        --key /tmp/cosign.key \
        ${REGISTRY}/backend/api:${CI_COMMIT_SHORT_SHA}
    - rm /tmp/cosign.key
  
  needs:
    - push
  only:
    - main
    - tags

# 部署时验证签名
verify-and-deploy:
  stage: deploy
  script:
    # 部署前验证签名(确保镜像未被篡改)
    - |
      cosign verify \
        --key ${COSIGN_PUBLIC_KEY} \
        ${REGISTRY}/backend/api:${CI_COMMIT_SHORT_SHA}
      if [ $? -ne 0 ]; then
        echo "❌ 镜像签名验证失败,拒绝部署"
        exit 1
      fi
      echo "✅ 签名验证通过,开始部署"
    
    # 继续部署流程...

二、SBOM(软件物料清单)

2.1 什么是 SBOM

SBOM(Software Bill of Materials)= 软件组成的完整清单

就像食品的"成分表",SBOM 列出镜像中所有:
  - OS 软件包(debian: curl 7.81, openssl 3.0.2...)
  - 应用依赖(python: django 4.2.1, requests 2.31.0...)
  - 许可证信息(MIT, Apache-2.0, GPL-2.0...)

用途:
  1. CVE 快速定位:新漏洞发布时,查 SBOM 确认是否受影响
  2. 许可证合规:确认开源许可证不违反内部政策
  3. 供应链追踪:追溯每个组件的来源
  4. 监管合规:美国 EO-14028 要求软件供应商提供 SBOM

2.2 生成 SBOM

# 方案1:Trivy 生成 SBOM(CycloneDX 格式)
trivy image \
    --format cyclonedx \
    --output sbom-cyclonedx.json \
    harbor.company.com/backend/api:v1.2.3

# 方案2:Trivy 生成 SBOM(SPDX 格式)
trivy image \
    --format spdx-json \
    --output sbom-spdx.json \
    harbor.company.com/backend/api:v1.2.3

# 方案3:Syft(专业 SBOM 工具)
# 安装 Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh

# 生成 SBOM
syft harbor.company.com/backend/api:v1.2.3 \
    -o cyclonedx-json \
    > sbom.json

# 包含更多细节(文件级别)
syft harbor.company.com/backend/api:v1.2.3 \
    -o syft-json \
    > sbom-detailed.json

2.3 将 SBOM 附加到镜像(OCI Attestation)

# 使用 Cosign 将 SBOM 作为 attestation 附加到镜像
# 好处:SBOM 与镜像存储在同一仓库,不需要额外管理

IMAGE=harbor.company.com/backend/api:v1.2.3

# 生成 SBOM
syft ${IMAGE} -o cyclonedx-json > sbom.json

# 附加到镜像(作为 attestation)
cosign attest \
    --key cosign.key \
    --type cyclonedx \
    --predicate sbom.json \
    ${IMAGE}

# 验证并查看 attestation
cosign verify-attestation \
    --key cosign.pub \
    --type cyclonedx \
    ${IMAGE} \
    | jq '.payload | @base64d | fromjson | .predicate'

三、Docker Content Trust(Notary v1)

# Docker Content Trust(DCT)是早期的镜像签名方案
# 基于 Notary v1,现在逐渐被 Cosign 替代
# 但仍然在某些企业环境中使用

# 开启 DCT
export DOCKER_CONTENT_TRUST=1
export DOCKER_CONTENT_TRUST_SERVER=https://notary.company.com

# 开启后,pull/push 都会验证/生成签名
docker pull ubuntu:22.04    # 验证 Ubuntu 官方签名
docker push harbor.company.com/backend/api:v1.0.0   # 签名并推送

# 初始化 delegation key(首次设置)
docker trust key generate admin-key
docker trust signer add --key admin-key.pub admin harbor.company.com/backend/api

# 为镜像签名
docker trust sign harbor.company.com/backend/api:v1.0.0

# 查看签名状态
docker trust inspect harbor.company.com/backend/api:v1.0.0

# DCT 的局限性
# 只支持 Notary v1 协议(已有替代品)
# 密钥管理复杂(root key 需要离线保存)
# 不支持多架构镜像签名
# → 推荐新项目直接用 Cosign

四、供应链安全策略配置

4.1 Harbor 内容信任策略

# Harbor 可以强制要求只接受已签名的镜像
# 管理界面:项目 → 配置 → 内容信任 → ✓ 只允许已签名内容

# API 配置
curl -X PUT \
    -u admin:Harbor12345 \
    -H "Content-Type: application/json" \
    "https://harbor.company.com/api/v2.0/projects/backend" \
    -d '{
        "metadata": {
            "enable_content_trust": "true",
            "enable_content_trust_cosign": "true"   
        }
    }'

# 效果:所有推送到 backend 项目的镜像
# 如果没有有效的 Cosign 签名,任何人都无法 pull

4.2 完整供应链流水线

# .gitlab-ci.yml — 完整的供应链安全流水线

stages:
  - test
  - build
  - scan
  - sign          # 新增:签名
  - attest        # 新增:附加 SBOM attestation
  - deploy

# 构建后签名
sign-and-attest:
  stage: sign
  image: cgr.dev/chainguard/cosign:latest
  
  script:
    # 1. 签名镜像
    - |
      echo "${COSIGN_PRIVATE_KEY}" | cosign sign \
        --key /dev/stdin \
        --yes \
        ${IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}
    
    # 2. 生成 SBOM
    - |
      syft ${IMAGE_NAME}:${CI_COMMIT_SHORT_SHA} \
        -o cyclonedx-json > sbom.json
    
    # 3. 附加 SBOM attestation
    - |
      echo "${COSIGN_PRIVATE_KEY}" | cosign attest \
        --key /dev/stdin \
        --type cyclonedx \
        --predicate sbom.json \
        --yes \
        ${IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}
    
    # 4. 将 SBOM 作为 artifact 保存(供审计)
    - mv sbom.json sbom-${CI_COMMIT_SHORT_SHA}.json
  
  artifacts:
    paths:
      - sbom-*.json
    expire_in: 90 days
  
  needs:
    - trivy-scan   # 必须通过安全扫描才签名

# 部署时验证
verify-before-deploy:
  stage: deploy
  script:
    # 验证签名(确认由受信任的 CI 签名)
    - |
      cosign verify \
        --key ${COSIGN_PUBLIC_KEY} \
        ${IMAGE_NAME}:${CI_COMMIT_SHORT_SHA} \
        || (echo "❌ 签名验证失败,拒绝部署"; exit 1)
    
    # 验证 SBOM 存在(确认经过 SBOM 生成步骤)
    - |
      cosign verify-attestation \
        --key ${COSIGN_PUBLIC_KEY} \
        --type cyclonedx \
        ${IMAGE_NAME}:${CI_COMMIT_SHORT_SHA} \
        > /dev/null \
        || (echo "❌ SBOM 验证失败,拒绝部署"; exit 1)
    
    echo "✅ 供应链验证通过"

五、基础镜像的供应链验证

# 验证官方镜像的 Cosign 签名(Chainguard 镜像全部签名)
cosign verify \
    --certificate-identity "https://github.com/chainguard-images/images/.github/workflows/release.yml@refs/heads/main" \
    --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
    cgr.dev/chainguard/nginx:latest

# 验证 Docker 官方镜像(通过 Docker Hub DCT)
export DOCKER_CONTENT_TRUST=1
docker pull nginx:1.27-alpine
# 自动验证 Docker 官方签名

# 在 Dockerfile 中固定基础镜像的 digest(防止 tag 被替换)
# 错误:
FROM python:3.11-slim    # tag 可能被替换

# 正确:用 digest 锁定(不可变)
FROM python:3.11-slim@sha256:abc1234def5678...
# 即使 python:3.11-slim 被替换,也不影响已固定 digest 的构建

# 查找镜像的 digest
docker pull python:3.11-slim
docker inspect python:3.11-slim --format '{{.RepoDigests}}'
# [python@sha256:abc1234...]

小结

镜像供应链安全的核心是建立信任链:从代码提交到生产部署的每一步都有密码学证明。

实际落地最有效的三步:第一,用 Cosign 对 CI 产生的每个镜像签名,部署时验证签名(阻断未经 CI 的镜像进入生产);第二,用 Trivy 或 Syft 生成 SBOM 并附加到镜像,方便漏洞快速定位;第三,在 Dockerfile 中用 digest 固定基础镜像(FROM python:3.11@sha256:xxx),防止 docker pull 时拉到被替换的镜像。

Keyless 签名是值得关注的未来方向:不需要管理密钥,通过 OIDC 身份(GitHub/GitLab token)证明签名者是受信任的 CI 系统,简化了密钥生命周期管理问题。