10.5 容器镜像签名——Cosign 与 Notation 实战

预计阅读时间:14 分钟

📖 目录

容器镜像在 CI/CD 流水线中被频繁拉取、分发,一旦镜像被篡改或注入恶意代码,攻击面将直接扩散到生产集群。镜像签名(Image Signing)是构建软件供应链安全的关键一环——它让部署方能够验证镜像的来源可信完整性未被破坏

学习目标

  • 理解镜像签名与镜像加密的区别及其在供应链安全中的价值
  • 掌握 Cosign 的密钥签名、KMS 签名与 keyless 无密钥签名模式
  • 能用 cosign verify 与自定义注解校验镜像来源和完整性
  • 了解 Notation 的 PKI 签名流程与 OCI Artifact 规范
  • 能用 Kyverno ClusterPolicy 在 K8s 准入阶段强制校验镜像签名
  • 理解"构建时签名 → 分发时验证 → 部署时准入"的完整信任链设计

前置知识

  • 3.1:Docker 容器入门 Docker 容器入门——镜像构建、tag 与 registry 分发
  • cicd01 CI/CD 生产实践——流水线中的镜像构建与推送
  • container_sec01 容器安全——Trivy 镜像漏洞扫描
  • policy_opa01 策略引擎——Kyverno/OPA 准入控制机制

为什么需要镜像签名

传统的镜像拉取仅依赖 registry 的认证机制,但无法回答以下问题:

  • 这个镜像是谁构建的?
  • 构建过程中是否被篡改?
  • 从 A 仓库推到 B 仓库后还能证明出处吗?

供应链攻击(如 SolarWinds、Codecov)已充分说明:仅靠 registry 凭证远远不够。镜像签名通过加密手段为镜像附加可验证的数字签名,配合准入控制器在 K8s 集群入口处拦截未签名镜像,形成"构建时签名 → 分发时验证 → 部署时准入"的完整信任链。

核心概念:镜像签名 ≠ 镜像加密。签名证明的是"谁在什么时候签了什么",不隐藏镜像内容。如需保密请配合镜像加密(如 Skopeo + AES)。

Sigstore Cosign 安装与使用

Cosign 是 Sigstore 项目的核心工具,也是目前最流行的容器镜像签名方案。它支持密钥签名和无密钥(keyless)签名两种模式。

安装 Cosign

# 二进制安装(Linux amd64)
COSIGN_VERSION="v2.4.3"
curl -sSfL "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64" -o cosign
chmod +x cosign
sudo mv cosign /usr/local/bin/

# 验证安装
cosign version

也可通过 Go 安装:

go install github.com/sigstore/cosign/v2/cmd/cosign@latest

生成密钥对

# 交互式生成,会提示输入密码
cosign generate-key-pair

# 输出:
# - cosign.key   (私钥,需妥善保管)
# - cosign.pub   (公钥,可公开分发)
私钥安全:私钥一旦泄露,攻击者可以伪造签名。生产环境建议使用 KMS(HashiCorp Vault、AWS KMS、GCP KMS 等)管理私钥,避免将私钥文件落盘到 CI runner。

签名镜像

# 使用本地私钥签名
cosign sign --key cosign.key myregistry.example.com/myapp:v1.2.3

# 使用 KMS 签名(以 AWS KMS 为例)
cosign sign --key awskms:///alias/my-cosign-key myregistry.example.com/myapp:v1.2.3

# 附加自定义签名注解
cosign sign --key cosign.key \
  -a "build_id=abc123" \
  -a "pipeline_url=https://ci.example.com/build/abc123" \
  myregistry.example.com/myapp:v1.2.3

签名后,Cosign 会将签名 payload 存储在 OCI registry 的 tag 附属位置(如 myapp:v1.2.3.sig),无需额外搭建签名服务器。

验证签名

# 使用公钥验证
cosign verify --key cosign.pub myregistry.example.com/myapp:v1.2.3

# 输出示例:
# Verification for myregistry.example.com/myapp:v1.2.3 --
# The following checks were performed:
#   - The cosign signature was verified
#   - The signatures were verified against the specified public key
# [{"critical":{"identity":{"docker-reference":"myregistry..."},"image":{"docker-manifest-digest":"sha256:..."},"type":"cosign container image signature"},"optional":{...}}]

# 验证特定注解(如 build_id)
cosign verify --key cosign.pub \
  -a "build_id=abc123" \
  myregistry.example.com/myapp:v1.2.3

密钥管理策略

方案适用场景优势注意事项
本地密钥文件开发/测试环境简单直接密钥需人工保管,泄露风险高
HashiCorp Vault企业内网 CI/CD集中管控,审计日志需维护 Vault 集群
AWS KMS / GCP KMS云原生环境免运维,自动轮换依赖云厂商 SLA
Keyless(Fulcio + Rekor)开源项目 / 临时环境无需管理密钥,基于 OIDC 身份依赖 Sigstore 基础设施

无密钥签名(Keyless)

# 使用 Sigstore 的 OIDC 流程签名(无需管理密钥)
cosign sign myregistry.example.com/myapp:v1.2.3

# 验证时不需要指定公钥,但需要信任 Fulcio 的 CA 证书链
cosign verify \
  --certificate-identity=user@example.com \
  --certificate-oidc-issuer=https://accounts.google.com \
  myregistry.example.com/myapp:v1.2.3

Keyless 模式下,签名证书由 Fulcio CA 根据 OIDC 身份(如 GitHub Actions 的 OIDC token)自动签发,签名记录写入 Rekor 透明日志。非常适合 GitHub Actions 等 CI 环境。

Keyless 实战:GitHub Actions 完整流程

下面是一份可直接落地的签名流水线:构建镜像 → keyless 签名 → 用证书身份重新验证。核心是给 workflow 授予 id-token: write 权限,让 Actions 能向 GitHub 换取 OIDC token。

# .github/workflows/sign.yml —— 构建 + 签名 + 验证
name: build-and-sign
permissions:
  id-token: write   # 关键:允许获取 OIDC token
  contents: read
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/youraccount/myapp:${{ github.sha }}
      - uses: sigstore/cosign-installer@v3.6.0
      - name: Keyless sign
        run: |
          cosign sign --yes ghcr.io/youraccount/myapp:${{ github.sha }}
      - name: Verify with certificate identity
        run: |
          cosign verify --yes ghcr.io/youraccount/myapp:${{ github.sha }} \
            --certificate-identity "https://github.com/youraccount/myapp/.github/workflows/sign.yml@refs/heads/main" \
            --certificate-oidc-issuer "https://token.actions.githubusercontent.com"
身份验证关键点--certificate-identity 必须精确匹配签发时 workflow 的路径与分支引用(GitHub OIDC 声明格式为 repository + workflow ref)。验证方凭这两个字段即可确定"这张证书只可能来自你自己的仓库 CI",任何人克隆仓库都签不出相同身份。

Notation(CNCF 标准)简介

Notation 是 CNCF 的签名项目(Notary v2),与 Cosign 定位相似但设计哲学不同。Notation 更贴近传统 PKI 体系,支持 X.509 证书链,与 OCI registry 的集成遵循 OCI Artifact 规范。

安装 Notation

# 下载并安装
NOTATION_VERSION="1.3.1"
curl -sSfL "https://github.com/notaryproject/notation/releases/download/v${NOTATION_VERSION}/notation_${NOTATION_VERSION}_linux_amd64.tar.gz" -o notation.tar.gz
tar xzf notation.tar.gz
sudo mv notation /usr/local/bin/
sudo mv notation-plugin-notation /usr/local/bin/notation-plugin-notation  # registry 插件

notation version

生成密钥与签名

# 生成自签名密钥(测试用)
notation cert generate-test --name "my-test-key"

# 列出信任存储
notation cert ls

# 签名镜像
notation sign --key "my-test-key" myregistry.example.com/myapp:v1.2.3

# 验证签名
notation verify myregistry.example.com/myapp:v1.2.3
Cosign vs Notation
  • Cosign:生态更成熟,keyless 模式完善,K8s 集成方案多,社区活跃度高
  • Notation:CNCF 官方标准,原生 OCI Artifact 支持,与 Azure ACR 集成紧密
  • 两者可共存,同一镜像可同时用两种方式签名

Notation 与 Cosign 对比

维度CosignNotation
信任模型公钥 / keyless(Fulcio CA + Rekor)X.509 证书链(传统 PKI)
透明日志Rekor,默认写入可配置,默认不写
密钥管理密钥文件 / KMS / OIDC 身份证书 + 私钥,支持 KMS 存储
签名产物OCI artifact(.sig tag)OCI artifact(按 OCI Artifact 规范)
准入集成Kyverno / Ratify / ConnaisseurKyverno(证书方式)/ Ratify(官方推荐)
云厂商生态通用,GitHub/GitLab CI 友好Azure ACR 深度集成

配置信任策略(trust policy)

Notation 的验证由两层配置驱动:trust store 存放可信的根证书,trust policy 声明"什么证书身份可以验证哪些镜像"。默认策略拒绝一切未显式信任的签名:

# ~/.config/notation/trustpolicy.json —— 信任策略
{
  "version": "1.0",
  "trustPolicies": [
    {
      "name": "my-registry-policy",
      "registryScopes": [ "myregistry.example.com/myapp" ],
      "signatureVerification": {
        "level": "strict"
      },
      "trustStores": [ "ca:my-ca-store" ],
      "trustedIdentities": [
        "x509.subject: CN=my-signer,O=IOHOW"
      ]
    }
  ]
}

# 将自建 CA 证书导入 trust store
notation cert add --type ca --store my-ca-store root-ca.crt

# 此时 verify 才真正按策略校验(信任链 + 身份白名单)
notation verify myregistry.example.com/myapp:v1.2.3
# Successfully verified signature for myregistry.example.com/myapp:v1.2.3

K8s 准入控制集成(Kyverno 验证签名)

签名只是第一步——在 K8s 集群中真正发挥价值,需要准入控制器在 Pod 创建时强制校验签名。Kyverno 是 Kubernetes 原生的策略引擎,支持直接验证 Cosign 和 Notation 签名。

安装 Kyverno

# 通过 Helm 安装
helm repo add kyverno https://kyverno.github.io/kyverno/
helm install kyverno kyverno/kyverno -n kyverno --create-namespace

配置 Cosign 签名验证策略

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: Enforce   # Enforce=强制拒绝 | Audit=仅审计
  background: false
  rules:
    - name: check-cosign-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "myregistry.example.com/*"
          attestors:
            - entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
                      -----END PUBLIC KEY-----
                  rekor:
                    url: https://rekor.sigstore.dev

配置 Notation 签名验证策略

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-notation-signatures
spec:
  validationFailureAction: Enforce
  background: false
  rules:
    - name: check-notation-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "myregistry.example.com/*"
          attestors:
            - entries:
                - certificates: |-
                    -----BEGIN CERTIFICATE-----
                    MIIDazCCAlOgAwIBAgIUJ...
                    -----END CERTIFICATE-----
                  subject: "CN=my-signer"
                  issuer: "CN=my-root-ca"

测试准入控制

# 部署已签名镜像 —— 应该通过
kubectl run signed-app --image=myregistry.example.com/myapp:v1.2.3
# pod/signed-app created

# 部署未签名镜像 —— 应该被拒绝
kubectl run unsigned-app --image=myregistry.example.com/untrusted:latest
# Error from server (Forbidden): admission webhook "kyverno-policy-failures..." denied the request:
# policy verify-image-signatures/verify-cosign-signature failed:
# ... image verification failed
生产环境建议
  • 先用 Audit 模式观察一段时间,确认所有工作负载镜像都已签名后再切换到 Enforce
  • 为系统组件(如 kube-proxy、coredns)预先配置签名豁免或使用独立策略
  • 配合 Kyverno 的 ClusterImagePolicy 支持多个签名者和备选公钥,避免单点故障

生产级完整策略:签名验证 + 摘要固定(Digest Pinning)

生产环境的完整策略不止"验证签名",还应顺带解决 tag 漂移问题:验证通过后把镜像引用改写为不可变的 digest,并给 Pod 打上审计注解:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-and-pin-image-signature
spec:
  validationFailureAction: Enforce
  background: false
  webhookTimeoutSeconds: 30
  rules:
    - name: require-cosign-signature
      match:
        any:
          - resources:
              kinds: ["Pod"]
              namespaces: ["production", "staging"]
      verifyImages:
        - imageReferences:
            - "myregistry.example.com/*"
          attestors:
            - count: 1
              entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
                      -----END PUBLIC KEY-----
                  rekor:
                    url: https://rekor.sigstore.dev
          mutateDigest: true
      mutate:
        patchStrategicMerge:
          metadata:
            annotations:
              +(image-signatures): "verified-by-kyverno"
    - name: deny-latest-tag
      match:
        any:
          - resources:
              kinds: ["Pod"]
      validate:
        message: "禁止使用 latest 标签,必须引用不可变版本或 digest"
        pattern:
          spec:
            containers:
              - image: "!*:latest"

mutateDigest: true 会在准入时把 myregistry.example.com/myapp:v1.2.3 重写为 myregistry.example.com/myapp@sha256:...。之后即使镜像 tag 被恶意覆盖,集群内所有引用依然指向准入时已验证的不可变内容。

供应链安全全景:SLSA 与 SBOM

镜像签名只是供应链安全的一环。业界用 SLSA(Supply-chain Levels for Software Artifacts)评估构建链路的防篡改等级,用 SBOM(软件物料清单)记录镜像里的每一个组件。签名 + SLSA + SBOM 共同构成"内容可审计、来源可验证、构建可复现"的信任基线。

SLSA 等级对照

等级要求典型实现
SLSA 1构建过程可溯源(版本控制管理)构建脚本入库、固定依赖版本
SLSA 2托管构建 + 生成签名GitHub Actions / GitLab CI 构建,cosign sign
SLSA 3无特权隔离构建 + 不可篡改审计日志BuildKit 无特权构建、签名入 Rekor
SLSA 4双人评审 + 可复现构建两次独立构建 digest 一致,全部参数入证据

生成 SBOM 并签名(syft + cosign attest)

# 1. 安装 syft(Anchore 出品)
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin

# 2. 从镜像生成 SPDX 格式 SBOM
syft ghcr.io/youraccount/myapp:v1.2.3 -o spdx-json > sbom.spdx.json

# 3. 统计组件数量
syft ghcr.io/youraccount/myapp:v1.2.3 | tail -5

# 4. 把 SBOM 作为 attestation 签名并存入 registry
cosign attest --yes --predicate sbom.spdx.json --type spdx \
  ghcr.io/youraccount/myapp:v1.2.3

# 5. 验证 attestation 内容(关键:type 必须匹配)
cosign verify-attestation --yes --type spdx \
  --certificate-identity "https://github.com/youraccount/myapp/.github/workflows/sbom.yml@refs/heads/main" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/youraccount/myapp:v1.2.3

签名撤销与密钥轮换

密钥泄露或签名者身份失效时,必须让旧签名不可信。keyless 模式天然缓解该问题:证书约 10 分钟过期且绑定 OIDC 身份,"撤销"等价于收回 CI 身份的签发能力;传统密钥模式则需要显式的轮换与吊销流程。

密钥轮换流程

# 1. 生成新密钥对
cosign generate-key-pair    # 生成新的 cosign.key / cosign.pub

# 2. 用新私钥重新签名所有在产镜像
cosign sign --key cosign.key myregistry.example.com/myapp:v1.2.3

# 3. 更新验证方:把 Kyverno ClusterPolicy 中的公钥块替换为新公钥;
#    保留一段过渡期时,可在同一 attestors 下并列配置新旧两个 keys 条目,
#    任一公钥验证通过即放行

# 4. 审计追溯:所有签名事件都在 Rekor 中留存
cosign verify --key cosign.pub \
  --certificate-identity "..." \
  --certificate-oidc-issuer "..." myregistry.example.com/myapp:v1.2.3

吊销的实际手段

手段生效时机适用场景
Rekor 透明日志检索即时(日志不可删除)审计追溯:谁在何时签过什么
证书吊销(CRL/OCSP)证书有效期内传统长周期证书依赖吊销;Fulcio 短证书约 10 分钟即过期,CRL/OCSP 实际意义有限,靠证书过期+OIDC 身份回收即可
信任策略更新策略部署时从信任库移除根证书 / 替换公钥(最常用)
删除 registry 中的 .sig tag立即临时禁用某镜像的签名(可被重新签名绕过)
不要依赖"删除签名"做撤销:任何持有旧私钥的人都能重新签名。真正的撤销必须发生在信任根层面——更新验证策略、吊销证书或回收 CI 身份。

常见错误

错误现象可能原因排查方法
cosign verifysignature verification failed 公钥与签名密钥不匹配,或镜像在签名后被重新推送 确认使用的公钥与签名时的私钥配对;检查镜像 digest 是否与签名时一致
签名时提示 Error: key signing requires --key 未指定私钥路径 使用 --key 参数指定私钥文件或 KMS URI
Kyverno 策略已部署但未拦截未签名镜像 策略模式为 Audit 而非 Enforce,或 imageReferences 未匹配该镜像 检查 validationFailureAction 字段;确认通配符覆盖目标 registry
Keyless 签名验证报 certificate identity mismatch --certificate-identity--certificate-oidc-issuer 与签发时不一致 查看 Rekor 透明日志中实际记录的 OIDC 身份信息
OCI registry 返回 MANIFEST_UNKNOWN 签名 payload tag 不存在或 registry 不支持 OCI Artifact 确认 registry 版本支持 OCI Artifact 存储;检查 .sig tag 是否存在

最佳实践

  1. 优先使用 Keyless 模式:在 GitHub Actions、GitLab CI 等 OIDC 环境中,避免管理私钥文件,利用 Fulcio + Rekor 实现零密钥签名
  2. 签名与准入控制分阶段上线:先在 Audit 模式下验证所有工作负载镜像是否已签名,再切换到 Enforce 强制拦截
  3. 为系统组件预留豁免:kube-proxy、coredns 等集群基础设施镜像通常来自上游,需提前配置签名豁免或独立策略,避免集群启动被阻断
  4. 将签名集成到 CI 流水线:在构建完成后自动执行 cosign sign,确保每一份发布的镜像都带有签名
  5. 定期轮换签名密钥:即使是 KMS 管理的密钥也应制定轮换计划,配合 Rekor 审计日志追踪签名历史

练习题

  1. 在本地用 Cosign 对一个测试镜像完成签名,并用 cosign verify 验证签名有效性。然后篡改镜像的 config digest,再次验证并观察错误信息。
  2. 编写一条 Kyverno ClusterPolicy,要求仅允许来自 myregistry.example.com 且带有合法 Cosign 签名的镜像部署到 production 命名空间。
  3. 在 GitHub Actions 中配置 Keyless 签名流水线:使用 OIDC token 签名构建产物,并在另一个 Job 中验证签名,记录证书身份信息。

学习检查点

学完本章后,请检验自己是否掌握以下内容:

检查项自测问题验证方法
概念理解能用自己的话解释容器镜像签名的原理和信任链尝试向他人讲解
命令操作能不查文档完成 Cosign 安装、镜像签名和验证在终端实际执行
原理掌握能说出镜像签名的密钥管理和签名验证流程画出流程图
故障排查能独立排查镜像签名验证失败的问题模拟故障并修复
最佳实践能说明为什么需要为生产镜像配置签名验证策略对比不同方案

本章总结

镜像签名把"信任谁"从 registry 凭证扩展为可验证的加密证据,Cosign 的 keyless 模式借助 OIDC 身份与 Rekor 透明日志,让签名融入 CI 流水线而无密钥管理负担。签名本身不产生安全收益,必须结合 Kyverno 等准入控制在集群入口强制执行,并遵循"先 Audit 后 Enforce、为系统组件预留豁免"的分阶段上线策略,才能形成闭环的供应链信任链。

延伸阅读

↑ 回到顶部