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 集群入口处拦截未签名镜像,形成"构建时签名 → 分发时验证 → 部署时准入"的完整信任链。
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 (公钥,可公开分发)
签名镜像
# 使用本地私钥签名
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:生态更成熟,keyless 模式完善,K8s 集成方案多,社区活跃度高
- Notation:CNCF 官方标准,原生 OCI Artifact 支持,与 Azure ACR 集成紧密
- 两者可共存,同一镜像可同时用两种方式签名
Notation 与 Cosign 对比
| 维度 | Cosign | Notation |
|---|---|---|
| 信任模型 | 公钥 / keyless(Fulcio CA + Rekor) | X.509 证书链(传统 PKI) |
| 透明日志 | Rekor,默认写入 | 可配置,默认不写 |
| 密钥管理 | 密钥文件 / KMS / OIDC 身份 | 证书 + 私钥,支持 KMS 存储 |
| 签名产物 | OCI artifact(.sig tag) | OCI artifact(按 OCI Artifact 规范) |
| 准入集成 | Kyverno / Ratify / Connaisseur | Kyverno(证书方式)/ 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 | 立即 | 临时禁用某镜像的签名(可被重新签名绕过) |
常见错误
| 错误现象 | 可能原因 | 排查方法 |
|---|---|---|
cosign verify 报 signature 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 是否存在 |
最佳实践
- 优先使用 Keyless 模式:在 GitHub Actions、GitLab CI 等 OIDC 环境中,避免管理私钥文件,利用 Fulcio + Rekor 实现零密钥签名
- 签名与准入控制分阶段上线:先在
Audit模式下验证所有工作负载镜像是否已签名,再切换到Enforce强制拦截 - 为系统组件预留豁免:kube-proxy、coredns 等集群基础设施镜像通常来自上游,需提前配置签名豁免或独立策略,避免集群启动被阻断
- 将签名集成到 CI 流水线:在构建完成后自动执行
cosign sign,确保每一份发布的镜像都带有签名 - 定期轮换签名密钥:即使是 KMS 管理的密钥也应制定轮换计划,配合 Rekor 审计日志追踪签名历史
练习题
- 在本地用 Cosign 对一个测试镜像完成签名,并用
cosign verify验证签名有效性。然后篡改镜像的 config digest,再次验证并观察错误信息。 - 编写一条 Kyverno
ClusterPolicy,要求仅允许来自myregistry.example.com且带有合法 Cosign 签名的镜像部署到production命名空间。 - 在 GitHub Actions 中配置 Keyless 签名流水线:使用 OIDC token 签名构建产物,并在另一个 Job 中验证签名,记录证书身份信息。
学习检查点
学完本章后,请检验自己是否掌握以下内容:
| 检查项 | 自测问题 | 验证方法 |
|---|---|---|
| 概念理解 | 能用自己的话解释容器镜像签名的原理和信任链 | 尝试向他人讲解 |
| 命令操作 | 能不查文档完成 Cosign 安装、镜像签名和验证 | 在终端实际执行 |
| 原理掌握 | 能说出镜像签名的密钥管理和签名验证流程 | 画出流程图 |
| 故障排查 | 能独立排查镜像签名验证失败的问题 | 模拟故障并修复 |
| 最佳实践 | 能说明为什么需要为生产镜像配置签名验证策略 | 对比不同方案 |
本章总结
镜像签名把"信任谁"从 registry 凭证扩展为可验证的加密证据,Cosign 的 keyless 模式借助 OIDC 身份与 Rekor 透明日志,让签名融入 CI 流水线而无密钥管理负担。签名本身不产生安全收益,必须结合 Kyverno 等准入控制在集群入口强制执行,并遵循"先 Audit 后 Enforce、为系统组件预留豁免"的分阶段上线策略,才能形成闭环的供应链信任链。