容器镜像签名验证是供应链防篡改的最后一道闸门。镜像仓库被投毒、构建产物被替换、tag 被重新覆盖指向恶意镜像,这类事件在近年供应链攻击中反复出现,而仅靠镜像扫描(扫描的是内容而非来源)无法阻止。本文给出 cosign 签名与验证的完整配置:从密钥对生成、签名与验证、keyless 身份签名、SBOM 附着,到 Kubernetes 准入控制强制校验,附可直接执行的命令与策略代码。
适用场景
- CI/CD 流水线构建镜像并推送到 Harbor、GitLab Registry 等私有仓库,需要确保上线镜像与构建产物一致。
- Kubernetes 集群只允许运行已签名镜像,防止运维或攻击者直接
kubectl apply一个未审计的第三方镜像。 - 供应链安全合规要求提供”镜像来源可信、内容未篡改”的可验证证据。
- 基础镜像或第三方组件由外部提供,需要在引入前验证其签名与出处。
先明确边界:镜像签名解决的是来源与完整性问题,即”这个镜像确实由该主体构建、且字节未被改动”;它不负责判断镜像里有没有漏洞。签名验证与镜像扫描、SBOM 分析是互补关系,不是替代关系。
前置条件
- cosign v2 及以上版本,可通过官方二进制或
go install github.com/sigstore/cosign/v2/cmd/cosign@latest安装。 - 镜像仓库支持 OCI artifact 存储:Harbor 2.5 以上、GitLab Container Registry 15 以上或标准 registry v2 均可。
- 本机已
docker login或配置~/.docker/config.json凭据,cosign 复用该凭据访问私有仓库。 - Kubernetes 准入控制方案二选一:Kyverno 1.8 以上,或 sigstore 官方 policy-controller。
- CI 环境如需 keyless 签名,需能访问 Fulcio 与 Rekor,且流水线具备 OIDC 身份(如 GitHub Actions 的 id-token 权限)。
原理说明
理解以下三点,配置会顺畅很多:
- 签名绑定 digest,而不是 tag。tag 是可变的,攻击者可以把
web:1.0重新指向另一个镜像。cosign sign会自动把 tag 解析为 manifest digest,并对 digest 签名。因此验证与部署都应使用@sha256:...形式。 - 签名本身也是一个 OCI 制品。cosign 把签名数据以
sha256-<digest>.sig为 tag 存放在同一仓库中,所以签名随镜像一同分发,无需额外签名服务器,但要求仓库支持 OCI artifact。 - keyless 用身份替代密钥。keyless 模式下,签名者用 OIDC 令牌向 Fulcio 换取短期证书,签名行为写入 Rekor 透明日志。验证时不再依赖公钥,而是校验”证书身份是否匹配预期 + 记录是否在透明日志中”。这消除了私钥保管问题,但引入对 Fulcio/Rekor 的依赖。
攻击面决定验证策略:如果攻击者能修改镜像内容,验证必然失败;如果攻击者能拿到签名私钥,则需要靠密钥轮换与 Rekor 记录审计发现。因此私钥与 CI 变量必须纳入密钥管理范围,不能写进仓库。
操作步骤
步骤一:生成签名密钥对
# 本地生成,交互输入两次口令
cosign generate-key-pair
# 生成 cosign.key(私钥,务必保密)与 cosign.pub(公钥,可公开分发)
# CI 环境用环境变量传入口令,非交互
COSIGN_PASSWORD='强随机口令' cosign generate-key-pair
# 校验密钥与权限
ls -l cosign.key cosign.pub
chmod 600 cosign.key
cosign.key 必须存入密钥管理系统(如 Vault、云 KMS),禁止提交到代码仓库。建议每季度轮换一次,轮换后旧公钥保留一段时间用于验证历史镜像。
步骤二:签名镜像
# 构建并推送镜像
docker build -t registry.example.com/app/web:1.0 .
docker push registry.example.com/app/web:1.0
# 取得不可变的 digest 引用
DIGEST_REF=$(docker inspect --format='{{index .RepoDigests 0}}' \
registry.example.com/app/web:1.0)
echo "$DIGEST_REF"
# 用私钥对 digest 签名(cosign 会自动把 tag 归一为 digest)
cosign sign --key cosign.key "$DIGEST_REF"
签名完成后可在仓库中看到名为 sha256-xxxx.sig 的制品。签名必须针对 digest 执行,直接对 tag 签名的做法在 tag 被覆盖后即失去意义。
步骤三:验证签名
# 用公钥验证
cosign verify --key cosign.pub "$DIGEST_REF"
# 需要看签名载荷与证书时输出 JSON
cosign verify --key cosign.pub "$DIGEST_REF" | jq .
# 未签名或签名不匹配时,命令以非 0 退出码返回,可直接用于流水线门禁
if cosign verify --key cosign.pub "$DIGEST_REF" > /dev/null 2>&1; then
echo "signature OK"
else
echo "signature FAILED" && exit 1
fi
步骤四:配置 keyless 签名(推荐用于开源与 CI 场景)
# GitHub Actions 等具备 OIDC 身份的环境中,直接签名,无需本地私钥
cosign sign --yes "$DIGEST_REF"
# 验证 keyless 签名:必须同时限定身份与 OIDC 签发者
cosign verify "$DIGEST_REF" \
--certificate-identity-regexp '^https://github\.com/your-org/your-repo/.*$' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com'
验证 keyless 签名时,两个参数缺一不可。只校验 issuer 会让任意使用同一 OIDC 提供方的仓库都能通过;只校验 identity 则无法限定签发来源。证书身份必须收紧到具体仓库或工作流。
步骤五:附着并校验 SBOM
# 生成 SBOM(syft 或镜像内构建工具均可)
syft registry.example.com/app/web:1.0 -o spdx-json > sbom.spdx.json
# 附着到镜像
cosign attach sbom --sbom sbom.spdx.json "$DIGEST_REF"
# 同时对 SBOM 做签名,防止 SBOM 被替换
cosign sign --key cosign.key --attachment sbom "$DIGEST_REF"
# 校验 SBOM 证明
cosign verify-attestation --type spdxjson --key cosign.pub "$DIGEST_REF" | jq .
步骤六:在 Kubernetes 准入门强制校验
以 Kyverno 的 verifyImages 为例,未通过验证的镜像会被直接拒绝创建:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signature
spec:
validationFailureAction: Enforce
background: false
rules:
- name: check-cosign-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "registry.example.com/app/*"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
# keyless 场景改用 keyless 条目
# keyless:
# issuer: https://token.actions.githubusercontent.com
# subject: https://github.com/your-org/your-repo/.github/workflows/build.yaml@refs/heads/main
kubectl apply -f verify-image-policy.yaml
kubectl get clusterpolicy verify-image-signature
使用 sigstore policy-controller 时,改为声明 ClusterImagePolicy,通过 policy.sigstore.dev/include: "true" 标签限定生效的命名空间。两种方案都要注意:校验对象必须是 digest,如果工作负载里写的是 tag,准入阶段会把 tag 解析为 digest 后验证,但一旦 tag 被覆盖,滚动更新会拉到新镜像并触发验证失败——这属于预期行为,也是签名机制起作用的表现。
步骤七:接入 CI 流水线门禁
# GitHub Actions 关键片段
# permissions 必须包含 id-token: write 才能 keyless 签名
jobs:
build:
permissions:
contents: read
packages: write
id-token: write
steps:
- uses: sigstore/cosign-installer@v3
- name: Build and push
run: |
docker build -t $IMAGE:${{ github.sha }} .
docker push $IMAGE:${{ github.sha }}
- name: Sign image
run: |
DIGEST_REF=$(docker inspect --format='{{index .RepoDigests 0}}' \
$IMAGE:${{ github.sha }})
cosign sign --yes "$DIGEST_REF"
- name: Verify before deploy
run: |
cosign verify "$DIGEST_REF" \
--certificate-identity-regexp \
"^https://github\.com/${{ github.repository }}/.*$" \
--certificate-oidc-issuer \
"https://token.actions.githubusercontent.com"
配置验证
| 检查项 | 验证命令 | 期望结果 |
| 签名制品已写入仓库 | cosign triangulate "$DIGEST_REF" |
输出 sha256-xxx.sig 引用 |
| 签名验证通过 | cosign verify --key cosign.pub "$DIGEST_REF" |
退出码 0,输出签名信息 |
| 篡改后验证失败 | 对未签名镜像执行 verify | 退出码非 0,提示 no matching signatures |
| keyless 身份已限定 | 带 identity-regexp 与 issuer 的 verify | 身份不匹配时验证失败 |
| SBOM 已附着且签名 | cosign verify-attestation --type spdxjson |
返回 SBOM 内容 |
| 准入门策略生效 | kubectl apply 未签名镜像 |
被拒绝,提示签名校验失败 |
关键回归测试是主动篡改验证:把一个未签名镜像改名为已签名镜像的 tag 并推送,确认 cosign verify 失败、Kubernetes 拒绝创建 Pod。只有这一步通过,才能证明门禁真正生效,而不是配置摆设。
常见问题
FAQ 1:只对 tag 签名行不行?为什么必须绑定 digest?
不行。tag 是可变指针,攻击者或误操作都能把 web:1.0 重新指向另一个镜像。若签名针对 tag 的当前指向,tag 被覆盖后签名与实际镜像内容就不再对应。正确做法是:
# 错误做法:依赖 tag
cosign sign --key cosign.key registry.example.com/app/web:1.0
# 正确做法:先解析为 digest,再签名与部署
DIGEST_REF=$(docker inspect --format='{{index .RepoDigests 0}}' \
registry.example.com/app/web:1.0)
cosign sign --key cosign.key "$DIGEST_REF"
kubectl set image deploy/web web="$DIGEST_REF"
另外应开启仓库的不可变 tag 策略(Harbor 支持 immutable tag),从机制上防止 digest 被覆盖。
FAQ 2:keyless 与私钥签名怎么选?内网离线环境怎么办?
| 维度 | 私钥签名 | keyless 签名 |
| 密钥管理 | 需自建保管与轮换流程 | 无需保管私钥 |
| 外部依赖 | 无,可完全离线 | 依赖 Fulcio 与 Rekor |
| 可追溯性 | 靠内部流程记录 | 透明日志可公开审计 |
| 适用场景 | 内网、专有云、离线交付 | CI/CD、开源、多团队协作 |
纯内网或离线环境选私钥签名,把 cosign.key 存入 Vault 或硬件密码模块。若希望内网也用 keyless,可以自建 Fulcio 与 Rekor 实例并设置 --fulcio-url、--rekor-url 指向内部服务,但维护成本明显高于私钥方案。
FAQ 3:签名存在哪里?私有仓库需要什么前提?
签名与镜像存放在同一个仓库,以 sha256-<digest>.sig 为 tag 的 OCI 制品形式存在,随镜像一起分发。前提是仓库支持 OCI artifact:
- Harbor 需 2.5 以上版本,并在项目中允许 OCI artifact 类型。
- 标准 registry v2 需支持 manifest list 与自定义 artifact type。
- 若仓库不支持,签名推送会报错,此时需要升级仓库或改用支持该能力的托管服务。
另外要注意仓库的垃圾回收策略:清理未被 tag 引用的制品时可能把签名一并删除,需在保留策略中把 .sig 制品排除。
总结
容器镜像签名验证的落地要点可以压缩成四句话:签名永远绑定 digest 而不是 tag;验证必须同时限定签名身份与 OIDC 签发者;准入控制要用 Enforce 模式并做一次主动篡改回归测试;私钥与 SBOM 同样需要纳入密钥管理和签名范围。实施顺序建议为:先在 CI 中生成密钥对并对镜像签名,用 cosign verify 打通流水线门禁,再部署 Kyverno 或 policy-controller 在集群侧强制校验,最后补齐 SBOM 附着与签名,把镜像的来源、完整性、成分三项证据串成完整链条。这样即便镜像仓库或构建环境被入侵,未签名的投毒镜像也无法进入生产集群。