痛点
容器化部署已成为标配,但一个关键问题常被忽视:你怎么确认运行的镜像确实是 CI/CD 构建出来的那个,而不是被篡改过的?
2024 年的多起供应链攻击事件再次敲响警钟 —— 攻击者通过入侵镜像仓库、CI 流水线或依赖库,注入恶意代码到最终镜像中。传统的 tag 机制(如 latest、v1.2.3)是可变的,任何有仓库写权限的人都能覆盖同一个 tag 指向不同的 digest。
核心需求: 在镜像从构建到部署的全链路中,建立不可篡改的信任锚点 —— 即镜像签名与验证。
方案
Sigstore 是 Linux Foundation 孵化的开源项目,提供免费的代码/容器签名基础设施。其核心组件 Cosign 专门用于容器镜像的签名、验证和存储。
为什么选 Sigstore/Cosign 而非传统 GPG 签名?
| 维度 | GPG 签名 | Cosign (Sigstore) |
|---|---|---|
| 密钥管理 | 手动分发/轮换公钥 | 支持 Keyless(OIDC 身份绑定) |
| 透明度 | 无公开审计 | Rekor 透明日志,不可篡改 |
| 集成难度 | 需自建验证链路 | 原生支持 OCI Registry、K8s Policy |
| CI/CD 集成 | 复杂 | GitHub Actions/GitLab CI 原生支持 |
核心架构:
CI Build → Cosign Sign(私钥/Keyless)→ OCI Registry(镜像+签名)
↓
Kubernetes Admission → Policy Controller 验证签名 → 允许/拒绝部署
实操步骤
第 1 步:安装 Cosign
# Linux amd64
curl -sSL -o cosign https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x cosign && sudo mv cosign /usr/local/bin/
# 验证安装
cosign version
第 2 步:生成密钥对并签名镜像
方式一:本地密钥对(适合私有环境)
# 生成密钥对(会提示输入密码)
cosign generate-key-pair
# 签名镜像(以 digest 引用,避免 tag 歧义)
IMAGE="registry.example.com/myapp@sha256:abc123..."
cosign sign --key cosign.key $IMAGE
签名会作为 OCI artifact 存储在同一个 Registry 中,无需额外存储。
方式二:Keyless 签名(推荐用于 CI/CD)
# 无需管理密钥,通过 OIDC 身份(GitHub Actions/Google/Microsoft)签名
# 在 GitHub Actions 中:
cosign sign --yes $IMAGE
Keyless 模式下,Sigstore 的 Fulcio CA 会颁发短期证书,绑定 CI 的 OIDC 身份(如 https://github.com/myorg/myrepo/.github/workflows/build.yml@refs/heads/main),签名记录写入 Rekor 透明日志。
第 3 步:验证镜像签名
# 使用公钥验证
cosign verify --key cosign.pub $IMAGE
# Keyless 验证(指定预期的签名者身份)
cosign verify \
--certificate-identity "https://github.com/myorg/myrepo/.github/workflows/build.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
$IMAGE
验证通过输出签名 payload(JSON 格式),包含签名时间、身份信息等元数据。
第 4 步:Kubernetes 集群中强制验证(Policy Controller)
安装 Sigstore Policy Controller(基于 Kubernetes Admission Webhook):
# 使用 Helm 安装
helm repo add sigstore https://sigstore.github.io/helm-charts
helm repo update
helm install policy-controller sigstore/policy-controller \
-n cosign-system --create-namespace \
--set webhook.failOpen=false
创建 ClusterImagePolicy 策略,强制所有镜像必须经过签名:
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: require-signed-images
spec:
images:
- glob: "registry.example.com/**"
authorities:
- key:
data: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
对命名空间启用策略:
kubectl label namespace production policy.sigstore.dev/include=true
此后,任何未签名或签名验证失败的镜像都将被 Admission Webhook 拒绝部署。
第 5 步:CI/CD 集成示例(GitHub Actions)
name: Build, Sign and Push
on:
push:
branches: [main]
permissions:
contents: read
id-token: write # 必须:用于 Keyless 签名的 OIDC token
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3
- name: Build and Push
run: |
docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Sign Image (Keyless)
env:
DIGEST: ${{ steps.push.outputs.digest }}
run: |
cosign sign --yes ghcr.io/${{ github.repository }}@${DIGEST}
避坑
1. 签名用 digest 而非 tag
# ❌ 错误:tag 可能被覆盖,签名就失效了
cosign sign --key cosign.key registry.example.com/myapp:v1.0
# ✅ 正确:始终用 sha256 digest
cosign sign --key cosign.key registry.example.com/myapp@sha256:abc123...
2. Policy Controller 的 failOpen 配置
生产环境建议 failOpen=false(严格模式),但首次部署务必先在 staging 环境验证,避免因策略配置错误导致所有 Pod 无法启动。可以先用 warn 模式观察:
spec:
mode: warn # 只告警不阻断,观察一周后再切 enforce
3. Keyless 签名的 OIDC Issuer 变更
如果你的 CI 平台更换了 OIDC Issuer URL(如从 GitHub.com 迁移到 GHES),验证策略中的 certificate-oidc-issuer 必须同步更新,否则所有旧签名验证都会失败。建议在策略中同时保留新旧 issuer 过渡期。
总结
| 环节 | 工具 | 作用 |
|---|---|---|
| 签名 | Cosign | 对镜像 digest 进行密码学签名 |
| 证书 | Fulcio | Keyless 模式颁发短期签名证书 |
| 透明日志 | Rekor | 签名记录不可篡改,支持审计 |
| 准入控制 | Policy Controller | K8s 集群层面强制验证 |
落地建议:
- 起步阶段:CI 中加入 Cosign 签名步骤,成本几乎为零(多 5 秒构建时间)
- 观察阶段:部署 Policy Controller,用
warn模式运行 1-2 周 - 强制阶段:切换为
enforce,拒绝一切未签名镜像进入生产 - 进阶:结合 SBOM(
cosign attach sbom)和漏洞扫描结果,构建完整的供应链信任链
镜像签名不是银弹,但它是软件供应链安全中 ROI 最高的一环 —— 实施成本低、效果显著、合规审计友好。对于任何运行容器化工作负载的团队,这都应该是 Day 2 的必做项。