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