镜像签名与供应链验证:Cosign/Sigstore 与准入控制深度实践

深入讲解镜像签名与供应链验证:镜像篡改与混淆攻击威胁模型、Sigstore 与 Cosign 的 keyless 签名机制(Fulcio/Rekor/OIDC)、OCI 1.1 签名与 Referrers 规范、签名验证策略、Kyverno/Connaisseur 准入控制、SBOM 与 SLSA provenance 联动,以及密钥管理、轮换与离线等生产落地问题。

前置阅读:建议先阅读 Docker 镜像供应链安全:SBOM、签名、扫描与 SLSA 合规 与 Harbor 私有镜像仓库深度实践。前一篇从全景角度介绍供应链,本篇聚焦签名机制与验证落地。

关键概念:镜像签名解决的是**“这个镜像是不是我信任的团队构建并推送的”问题。签名本身不等于安全——必须配合验证策略**(谁签了才可信)与准入控制(不通过验证就不让拉取/运行)。现代签名的核心是 Sigstore:用短期证书(Fulcio)做 keyless 签名,把签名记录上链(Rekor)供审计。


1. 为什么镜像需要签名

1.1 镜像篡改的威胁模型

镜像在"构建 → 推送 → 拉取 → 运行"全链路都可能被篡改:

攻击面:
  - 中间人篡改:HTTPS 到仓库连接被劫持(公网仓库、弱 CA)
  - 仓库失陷:tag 被替换为带后门版本;依赖混淆:冒充官方包名
  - 构建链投毒:CI 缓存/基础镜像被污染后重新发布
无签名的后果:拉取到"看起来同名"但 digest 不同的镜像,无人察觉

1.2 签名能防什么、不能防什么

威胁签名是否有效说明
镜像内容被篡改有效digest 变化,签名校验失败
tag 被替换到其他镜像有效签名不匹配该 digest
构建过程被投毒部分需配合 SLSA provenance 记录构建输入
签名密钥泄露失效攻击者可用真密钥签名,须有密钥保护/轮换
镜像本身有漏洞无效签名只证"是谁发的",不证"没有漏洞"

一句话:签名回答"是谁"(来源),扫描回答"有没有漏洞"(内容),两者互补不可替代。


2. Sigstore 与 Cosign

2.1 Sigstore 三大组件

组件角色作用
Fulcio证书签发为 OIDC 身份签发短期证书(证书里绑定邮箱/身份)
Rekor透明日志记录签名条目,提供可审计的签名日志(防抵赖)
Cosign签名/验证 CLI生成签名、附加到镜像、校验签名
keyless 流程:
  OIDC 身份(GitHub/Google 等)→ Fulcio 签发短期证书
  → Cosign 签名 → 签名写入镜像仓库(tag 或 Referrers)
  → 公钥哈希写入 Rekor → 验证时向 Rekor 查询

2.2 为什么 keyless 优于"一把私钥"

传统 PGP/私钥签名:私钥长期有效,泄露即全盘失效;分发轮换是负担
Sigstore keyless:
  - 证书有效期短(约 10 分钟),身份来自 OIDC(绑定 CI/开发者)
  - 无需管理长期私钥,泄露风险窗口极小

一句话:Sigstore 把"长期密钥管理"换成"短期证书 + OIDC 身份 + 透明日志",签名这件事变得可自动化且可审计。


3. OCI 镜像签名机制

3.1 签名存放在哪

签名是附加在镜像上的对象,有三种承载方式:

① 标签式(cosign 早期默认):ghcr.io/user/app:sha256-<digest>.sig
② OCI 1.1 Referrers(推荐):Index 通过 subject 字段关联,
   用 ORAS/referrers API 读取
③ Notation/Notary v2(CNCF 生态):基于 OCI 1.1,x509 证书签名

3.2 Cosign 签名对象格式

# 拉镜像并用私钥签名(会产生 .sig 附件)
cosign sign --key cosign.key registry.example.com/app:v1
# keyless 签名(GitHub Actions 环境)
cosign sign --yes registry.example.com/app:v1
# 查看签名附件
cosign tree registry.example.com/app:v1
签名内容(payload):
  - 被签名镜像的 digest(immutable 绑定)
  - 签名者证书(含 OIDC 身份声明)+ 时间戳
  - 可选自定义声明(如构建材料哈希)

3.3 Referrers 与 OCI 规范

# ORAS 推送任意 artifact(如 SBOM)关联到镜像 subject
oras attach --artifact-type application/spdx+json \
  registry.example.com/app:v1 sbom.spdx.json

# 查看镜像的 referrers(签名、SBOM、漏洞报告)
oras discover registry.example.com/app:v1

一句话:OCI 1.1 的 Referrers 让"签名、SBOM、扫描报告"都挂在镜像 subject 上,成为可发现、可遍历的附件集合。


4. 签名工作流

4.1 本地与 CI 签名

# 生成密钥对(长期密钥方案,适合测试/私有)
cosign generate-key-pair
# 生产建议把公钥推送到 KMS/HashiCorp Vault

# GitHub Actions 内 keyless 签名(无需密钥)
cosign sign --yes $IMAGE
# GitHub Actions:构建 + 签名
jobs:
  build:
    permissions:
      id-token: write   # 关键:允许请求 OIDC token
    steps:
      - uses: sigstore/cosign-installer@v3
      - run: docker build -t $IMAGE .
      - run: docker push $IMAGE
      - run: cosign sign --yes $IMAGE

4.2 签名 SLSA provenance

# 同时生成构建出处(provenance),防"构建输入被投毒"
steps:
  - uses: slsa-framework/slsa-github-generator@v2
  - run: cosign attest --predicate predicate.json --type slsaprovenance $IMAGE

4.3 签名注意事项

□ 只对 immutable digest 签名(v1 → sha256:...),tag 会漂移
□ 每次重建必须重新签名;签名与推送原子化(同一 CI job)
□ 多架构镜像:manifest list 需分别签每个平台清单

一句话:签名要"贴在 digest 上"而不是"贴在 tag 上",并在 CI 里构建/推送/签名一气呵成。


5. 签名验证策略

5.1 用 Cosign 验证

# 用公开公钥验证
cosign verify --key cosign.pub registry.example.com/app:v1

# keyless 验证(依赖 Rekor + 信任根)
cosign verify registry.example.com/app:v1 \
  --certificate-identity-regexp ".*@example.com" \
  --certificate-oidc-issuer-regexp ".*github.com"

# 验证并同时校验 SBOM attestation
cosign verify-attestation --type slsaprovenance registry.example.com/app:v1

5.2 验证策略文件

验证策略回答三个问题:谁签的、用什么签的、是否过期:

# cosign policy(示例:只信任指定签发身份)
registry.example.com:
  images:
    app:
      - verified-by: example-corp
        certificate-identity: "release-bot@example.com"
        certificate-oidc-issuer: "https://github.com/login/oauth"

5.3 信任根与 CA

keyless 验证依赖公共信任根(Sigstore TUF 根):
  - 离线/内网环境需要自建 Sigstore(fulcio+rekor+tuf 自托管)
  - 或退回"私钥 + cosign.pub"方案,把公钥作为信任锚点
验证失败的表现:
  - 签名不存在 / digest 不匹配 / 身份不符 / 证书已过期

一句话:验证策略是签名的"白名单"——没有策略约束的签名只是"有人签过",有策略约束才是"可信的人签过"。


6. 准入控制:让集群只跑可信镜像

6.1 Kyverno 校验镜像签名

# Kyverno ClusterPolicy:镜像必须通过 cosign 校验
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
spec:
  validationFailureAction: Enforce
  rules:
    - name: verify-cosign
      verifyImages:
        - image: "registry.example.com/*"
          key: |-
            -----BEGIN PUBLIC KEY-----
            ...(信任锚点公钥)...
            -----END PUBLIC KEY-----

6.2 Connaisseur 准入控制器

Connaisseur(基于 OCI 签名的准入控制器):
  - 部署为 Validating Webhook,拦 Pod 创建
  - 策略:require-all(所有镜像必须有有效签名)
  - 支持 cosign 与 notaryv1 签名格式
  - 验证通过才允许 Pod 调度(否则拒绝)

6.3 Docker 侧:Content Trust

# Docker Engine 开启内容信任(DCT)
export DOCKER_CONTENT_TRUST=1
docker pull registry.example.com/app:v1
# 无签名的镜像会被拒绝拉取
方案控制点强度
DOCKER_CONTENT_TRUST拉取端(客户端)中(绕过容易)
Kyverno verifyImages集群准入(kube-apiserver)高(强制)
Connaisseur集群准入 Webhook高(强制)
Registry 策略仓库侧(推送/拉取)中(与签名存储联动)

一句话:验证最有效的着力点是集群准入——让不通过签名的镜像根本进不了 Pod,而不是依赖每个开发者的拉取习惯。


7. 与 SBOM / SLSA 联动

7.1 签名 SBOM 附件

# 生成 SBOM 并附加、签名
syft registry.example.com/app:v1 -o spdx-json > sbom.spdx.json
cosign attach sbom --sbom sbom.spdx.json registry.example.com/app:v1
cosign sign --key cosign.key registry.example.com/app:v1
cosign verify-attestation registry.example.com/app:v1   # 一并校验 SBOM 真实性

7.2 SLSA 等级与签名

SLSA L3 的核心要求之一就是"生成出处 + 签名出处":
  签名对象 = SLSA provenance(构建器、输入、依赖清单)
  消费端 = 部署准入时验证 provenance 的签名与内容
效果:
  即使镜像本身被重建,出处中的构建输入也能被核对

7.3 完整供应链校验链

镜像拉取前依次校验:
  ① 镜像本身签名(谁构建的)        cosign verify
  ② SBOM 附件签名(物料清单真实)    verify-attestation
  ③ SLSA provenance(构建输入可信)   verify slsaprovenance
  ④ 漏洞扫描报告(内容安全)          Trivy/scan(见安全扫描篇)

一句话:签名、SBOM、SLSA 是供应链的"三道保险"——来源可验、物料可查、出处可溯,串起来才是完整的信任链。


8. 生产落地与常见坑

8.1 密钥管理

长期密钥方案:
  - 私钥进 KMS/Vault,绝不进 CI 变量明文
  - 公钥作为信任锚点发布到受控位置(如 GitHub 仓库 + cosign.pub)
keyless 方案:
  - CI 角色需 OIDC federation 配置(GitHub OIDC Provider)
  - 自建 Sigstore 时管理 Fulcio/Rekor/TUF 根

8.2 离线 / 内网 / 私有仓库

□ 私有仓库需支持 OCI 签名附件(Harbor 2.5+ 支持 cosign 附件)
□ 内网无 Rekor 访问 → 用私钥方案替代 keyless
□ 自建 Sigstore:需部署 fulcio、rekor-server、tuf 根、cosign
□ 离线验证:本地缓存 TUF 根与 Rekor 日志状态

8.3 轮换与撤销

# 证书/密钥轮换:旧签名将无法通过新公钥验证,需重新签名发布
# 撤销策略:
#   - keyless 短证书:过期即失效,天然轮换
#   - 长期密钥:泄露后用 cosign 吊销记录 + 重新签名所有受影响镜像
cosign verify --key old.pub image:v1   # 失败 → 触发重新签名流程

8.4 常见问题速查

症状根因对策
no matching signatures镜像未签名补签并重新推送
验证通过但准入拒绝策略身份不匹配核对 certificate-identity 正则
私有仓库附件丢失仓库不支持 Referrers升级 Harbor/注册 OCI 附件
keyless 验证超时无法访问 Rekor自建 Sigstore 或转私钥方案
digest 变化后旧签名失效重新构建未重签CI 内构建+签名原子化

一句话:签名落地最大的坑不是签名本身,而是"密钥/信任根/仓库兼容"这三个基础设施——先选好方案再上准入。


9. 总结

主题关键结论一句话记忆
威胁模型签名防"谁"不防"漏洞"来源可验、内容靠扫描
SigstoreFulcio 短证书 + Rekor 日志keyless 免密钥管理
签名对象绑定 digest 而非 tag签 digest 不签 tag
承载方式OCI 1.1 Referrers 附件附件挂 subject
验证策略白名单身份 + 信任根策略比签名重要
准入控制Kyverno/Connaisseur 强制进不了 Pod 才算数
SBOM 联动签名 SBOM + SLSA provenance物料与出处可溯
生产落地KMS 管密钥、自建 Sigstore先选方案再上准入

镜像签名不是一次性的"加个命令",而是一套签名 → 存储 → 验证 → 准入 → 轮换的完整体系。落地要点:优先用 Sigstore keyless 在 CI 里对 digest 签名,把签名附件存进支持 OCI 1.1 的仓库,用 Kyverno/Connaisseur 在集群准入层强制验证,并让 SBOM 与 SLSA provenance 一并签名。当"无有效签名镜像无法运行"成为平台默认,供应链才算真正闭合。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「docker」更多文章

  1. 镜像安全扫描与 SBOM:Trivy、Grype、Clair 与 CI 安全门禁
  2. containerd 插件机制与扩展:snapshotter、shim 与 CNI 的深度定制
  3. 边缘容器与轻量运行时:Wasm、gVisor 与 K3s 的资源受限实践