签名是供应链安全最容易被误解的环节。一个团队在 CI/CD 里配置了 cosign sign,把镜像推送到仓库,屏幕上跳出 "Pushed signature to..." 的绿色日志,任务就宣告完成。但三个月后在生产环境拉取这个镜像时,没有任何自动化流程去验证这条签名是否有效、是否来自正确的构建管道、是否被 Rekor 透明日志记录过。
这就是当前软件供应链安全的真实状态:签名端热热闹闹,验证端冷冷清清。Cosign 和 Sigstore 降低了签名的门槛,但验证的门槛——策略定义、信任根分发、部署门禁集成——几乎没有被同步降低。结果是企业花了工程时间在 CI 里加了一行签名命令,却没有获得对等的供应链可信度提升。
本文的问题是:从 "签完名" 到 "能验证" 之间,工程上存在哪些结构性断层?如何补全?
签名与验证之间的真实距离
2021 年的 SolarWinds 事件和 2024 年的 XZ Utils 后门事件,证明了供应链攻击的核心特征:恶意代码不是从外部渗透进来的,而是从"可信来源"合法进入的。SolarWinds 的 Orion 平台本身有代码签名证书,XZ Utils 的 tarball 也通过了社区的多轮审查——问题在于构建环境和分发管道被污染后,下游消费者没有独立手段验证"这个产物是否真的来自我期望的构建流程"。
代码签名解决的是完整性问题(这个文件从签名后有没有被篡改),而 provenance 解决的是来源问题(这个文件来自哪个构建管道、哪次提交、哪台构建机)。在供应链安全框架里,这两个问题的答案缺一不可:
| 问题类型 | 签名回答 | provenance 回答 | 缺失风险 |
|---|---|---|---|
| 完整性 | 文件自签名后未被修改 | 构建过程未被篡改 | 签名后的文件仍可能来自被劫持的构建机 |
| 来源 | 谁持有私钥 | 哪个 CI/CD 管道、哪次 Git 提交触发了构建 | 私钥泄露后攻击者可伪造合法签名 |
| 时效 | 签名时间点 | 构建开始/结束时间、构建环境快照 | 无法区分正常版本和回滚的旧版本 |
| 可复现 | 无 | 构建参数、依赖版本、环境变量 | 无法审计或复现构建结果 |
Cosign 的 keyless signing 通过 Fulcio 和 Rekor 同时提供了签名和 provenance 的基础设施:Fulcio 颁发的短期证书将 OIDC 身份绑定到签名动作,Rekor 的透明日志记录了签名事件的时间戳和哈希。理论上,一次 cosign sign 同时完成了签名和 provenance 记录。但问题出在验证侧——验证需要知道信任谁、去哪里查、查到什么才算通过。这些决策在大多数团队的 DevOps 流程里处于空白地带。
Sigstore 信任模型:谁为签名背书?
要理解验证为什么会断,先看清楚 Sigstore 的信任模型。Cosign 本身不验证任何身份——它只是客户端工具。真正的信任基础设施由三个组件构成:
Fulcio 是一个免费的、社区运营的 CA,专门颁发短期代码签名证书(默认有效期 10 分钟)。它不验证你是谁,只验证你当前持有的 OIDC 身份(GitHub Actions、Google、Microsoft 等)是否有效。证书里包含你的 OIDC 身份标识(如 https://github.com/myorg/.github/workflows/build.yml@refs/heads/main),以及一个临时公钥。签名完成后,私钥即可丢弃——这就是 "keyless" 的含义:没有长期私钥需要保管。
Rekor 是一个 append-only 的透明日志,记录了每一次签名事件的摘要。它的安全属性类似于 Certificate Transparency:任何人都可以查询 Rekor,确认某个签名事件是否被记录、是否在特定时间之前发生、是否被事后篡改。Rekor 的不可抵赖性是整个 Sigstore 生态的根基——如果没有 Rekor,攻击者可以在事后声称"我没有签过这个文件",或者伪造一个更早的签名时间。
Cosign 是开发者直接交互的 CLI 工具,负责调用 Fulcio 获取证书、向 Rekor 提交记录、以及后续验证时查询这两个服务。Cosign v3(2026 年 6 月发布 v3.1.1)默认使用标准化的 Sigstore bundle 格式,统一支持容器镜像、npm 包、GitHub Artifact Attestations 和 Homebrew provenance 的验证。
这个模型的关键假设是:验证侧必须能访问 Fulcio 和 Rekor,并且信任它们的公钥。在生产环境的 Kubernetes 集群里,这往往不成立。很多集群是离线或网络受限的,无法访问 rekor.sigstore.dev;或者团队根本不知道验证时需要指定 --rekor-url 和 --fulcio-url;或者更常见的情况是,验证步骤从来就没有被写进部署流水线。
验证断层的四个工程表现
1. 验证命令的复杂性与认知成本
签名的命令很简单:
cosign sign --yes $IMAGE_URI@$DIGEST但验证的命令明显更复杂:
cosign verify \
--certificate-identity-regexp="^https://github.com/myorg/" \
--certificate-oidc-issuer-regexp="^https://token.actions.githubusercontent.com" \
$IMAGE_URI@$DIGEST验证需要明确的策略参数:允许哪些身份签名?允许哪些 OIDC issuer?是否要求 Rekor 记录?是否验证 SBOM attestation?这些策略不是技术细节,而是组织安全决策——但大多数团队在没有安全工程师参与的情况下,不会主动定义这些规则。结果就是验证要么不做,要么只做最基础的 "签名是否存在" 检查,无法区分"来自正确管道的签名"和"来自攻击者 fork 仓库的签名"。
2. 部署门禁的缺失
签名发生在 CI 的 "build" 阶段,验证应该发生在 CD 的 "deploy" 阶段。但 Kubernetes 的默认行为是不验证镜像签名。要让集群拒绝未签名镜像,需要额外安装策略引擎:
- Kyverno:通过
verifyImages规则在准入控制阶段验证 Cosign 签名 - OPA Gatekeeper:通过 ConstraintTemplate 定义签名验证策略
- Ratify(微软开源):专门用于验证供应链制品的签名和 attestation
- Sigstore Policy Controller:Sigstore 官方提供的 Kubernetes 准入控制器
这些工具都有配置成本。以 Kyverno 为例,一个基本的镜像签名验证规则需要定义:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signature
spec:
validationFailureAction: Enforce
rules:
- name: check-cosign-signature
match:
resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "ghcr.io/myorg/*"
attestors:
- entries:
- keyless:
issuer: "https://token.actions.githubusercontent.com"
subject: "https://github.com/myorg/.github/workflows/build.yml@refs/heads/main"
rekor:
url: https://rekor.sigstore.dev这个配置里的 subject 和 issuer 必须与 CI 侧的 OIDC 身份严格匹配,任何工作流路径变更、分支重命名或组织迁移都会导致验证失败。很多团队在首次配置后,没有建立持续维护这个对应关系的流程。
3. 镜像仓库的验证支持参差不齐
并非所有镜像仓库都原生支持签名验证。Docker Hub 不原生支持 Cosign 签名(需要额外配置 Notary v2 或 Cosign 独立验证);Harbor v2.9+ 开始支持 Cosign 签名验证和 SBOM 展示;GitHub Container Registry (ghcr.io) 支持 Sigstore 签名和 artifact attestations;AWS ECR 支持通过 Notary v2 验证,但对 Cosign 的支持需要额外配置。
仓库不支持意味着验证必须在拉取镜像的节点上完成,增加了每个节点的计算开销和网络依赖。对于边缘节点或离线环境,这几乎不可行。
4. Rekor 可用性与离线验证的困境
Rekor 的透明日志需要在线查询。如果生产集群无法访问公网 Rekor 实例(rekor.sigstore.dev),验证会失败。解决方案包括:
- 部署私有 Rekor 实例(Sigstore 支持私有化部署)
- 使用 Cosign 的
--offline模式(需要预缓存信任根和签名 bundle) - 通过 Sigstore TUF 根定期同步信任材料
但每种方案都增加了运维负担。私有 Rekor 需要高可用部署和监控;离线模式需要设计信任材料的缓存和轮换策略;TUF 根同步本身也需要信任根分发机制。这些都不是"开箱即用"的能力。
SLSA 分级:签名在哪一级才真正有效?
SLSA(Supply-chain Levels for Software Artifacts)框架把供应链安全成熟度分为四个等级。理解这个分级有助于定位"签名但无法验证"问题的根源:
| 等级 | 核心要求 | provenance 质量 | 验证可能性 | 典型实现 |
|---|---|---|---|---|
| L1 | 存在 provenance | 文本描述,可能无签名 | 低,无法验证真实性 | 手动编写 README 或简单的 build metadata |
| L2 | 托管构建 + 签名 provenance | 构建服务自动生成,有签名 | 中,可验证签名但构建环境不受控 | GitHub Actions + cosign sign |
| L3 | 不可伪造的 provenance | 隔离构建、无持久凭证、自动化生成 | 高,可验证且构建环境可信 | slsa-github-generator + GitHub-hosted runner |
| L4 | 完全可复现的构建 | hermetic build、可复现、双向审计 | 极高,可独立复现验证 | Bazel hermetic builds、隔离构建环境 |
大多数团队目前处于 L1-L2 之间:他们有了 provenance(通常只是一个简单的 SBOM 或构建日志),但这个 provenance 要么没有签名,要么签名后没有验证流程。真正让签名产生安全价值的是 L3——它要求 provenance 的生成过程本身不可伪造。
slsa-github-generator 是实现 L3 的关键工具。它不是让你手动调用 cosign sign,而是用一个受保护的 GitHub Actions reusable workflow 来执行构建和签名。这个 workflow 运行在与调用者隔离的环境中,使用临时的、无持久凭证的身份来签名 provenance。攻击者即使入侵了调用仓库,也无法伪造 provenance,因为 provenance 的生成在另一个不可控的环境中完成。
但 L3 的验证同样需要支持。slsa-verifier 是专门用于验证 SLSA provenance 的工具:
slsa-verifier verify-image \
--source-uri github.com/myorg/myrepo \
--builder-id "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v2.0.0" \
$IMAGE_URI@$DIGEST验证时需要指定 builder-id,即 provenance 是由哪个受信任的 builder 生成的。这引入了一个新的信任模型:你不仅信任签名的身份,还信任生成 provenance 的 builder。如果 builder 被攻破(如 slsa-github-generator 的某个版本存在漏洞),所有通过该 builder 生成的 provenance 都会受到质疑。
这正是 2026 年供应链安全讨论的核心转向:从"信任签名"到"信任构建系统"。
GitHub Artifact Attestations:平台级 provenance 的演进
2024 年 GitHub 推出 Artifact Attestations,标志着供应链 provenance 从"社区工具链"向"平台原生能力"的迁移。它的设计思路是:让 provenance 的生成和验证都成为 GitHub 平台的一等公民,降低团队自建 Sigstore 基础设施的成本。
使用方式很直接。在 workflow 中加入:
- uses: actions/attest-build-provenance@v2
with:
subject-name: ghcr.io/myorg/myapp
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true这个 action 会生成一个 in-toto 格式的 attestation,绑定 artifact 的 digest 与构建来源(仓库、workflow、commit SHA、触发事件),然后用 GitHub 托管的 Sigstore 实例签名。消费者验证时只需:
gh attestation verify $IMAGE_URI@$DIGEST \
--repo myorg/myrepo \
--cert-identity-regex "^https://github.com/myorg/.github/workflows/"相比直接使用 Cosign,GitHub Artifact Attestations 的优势在于:
- 无需管理 OIDC 身份映射:GitHub 自动将 workflow 身份绑定到 attestation,无需手动指定
--certificate-identity-regexp - 无需自行部署 Rekor:验证通过 GitHub CLI 完成,背后由 GitHub 托管的透明日志支持
- 统一的策略表达式:验证时可以按仓库、workflow 路径、分支等维度过滤
但它的边界也很清晰:
- 供应商锁定:attestation 的验证依赖 GitHub CLI 和 GitHub 托管的基础设施。如果 artifact 被分发到非 GitHub 环境(如自建 Harbor、AWS ECR),验证流程需要额外适配
- 适用范围有限:目前主要支持容器镜像和文件产物。对于 helm chart、Terraform module、二进制发布包等格式,支持仍在扩展中
- 信任根问题:GitHub 作为 OIDC issuer 和 Sigstore 运营商,自身成为信任根。如果 GitHub 的 OIDC 服务或私钥管理出现安全问题,所有基于其签名的 attestation 都会受影响
从架构演进的角度看,GitHub Artifact Attestations 代表了"平台托管 provenance"的方向——它降低了入门门槛,但也把信任集中到了平台方。对于安全要求更高的组织,私有化 Sigstore 部署(自己运行 Fulcio、Rekor、CT log)仍然是必要的。
从签名到验证的闭环:工程落地路径
补全验证断层不是加一个命令那么简单,需要从 CI 生成、策略定义、部署门禁、监控审计四个层面构建闭环。
CI 生成侧:标准化 provenance 输出
不要只签名镜像,要同时生成并签名 SBOM 和 provenance attestation。推荐的最小产出集合:
artifact (container image)
├── signature (cosign sign)
├── SBOM attestation (syft + cosign attest)
└── SLSA provenance (slsa-github-generator or GitHub Artifact Attestations)以 GitHub Actions 为例,一个完整的 build-and-attest workflow:
jobs:
build:
runs-on: ubuntu-latest
permissions:
id-token: write # 用于 OIDC 身份获取
contents: read
attestations: write # 用于 artifact attestation
packages: write # 用于推送镜像
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t $IMAGE_URI .
- name: Push image
run: docker push $IMAGE_URI
- name: Sign image
run: cosign sign --yes $IMAGE_URI@${{ steps.build.outputs.digest }}
- name: Generate SBOM
run: syft $IMAGE_URI -o spdx-json > sbom.spdx.json
- name: Attest SBOM
run: cosign attest --predicate sbom.spdx.json --type spdxjson $IMAGE_URI@${{ steps.build.outputs.digest }}
- name: Attest provenance
uses: actions/attest-build-provenance@v2
with:
subject-name: $IMAGE_URI
subject-digest: ${{ steps.build.outputs.digest }}关键点是 permissions 的精细化配置。id-token: write 只给需要 OIDC 认证的步骤,不要给整个 workflow 开放不必要的权限。
策略定义侧:明确信任边界
验证之前必须先回答三个问题:
- 信任哪些 signer? 是只允许本组织的 GitHub Actions workflow,还是也允许上游依赖的官方签名?
- 信任哪些 builder? 如果使用 slsa-github-generator,是否允许所有版本,还是只允许特定 tag?
- 验证失败时如何处理? 是阻止部署、告警但放行、还是降级到人工审批?
这些决策应该写成可版本控制的策略代码。以 OPA 为例:
package sigstore.verify
import future.keywords.if
import future.keywords.in
default allow := false
# 允许来自本组织 GitHub Actions 的签名
allow if {
input.issuer == "https://token.actions.githubusercontent.com"
startswith(input.subject, "https://github.com/myorg/")
input.rekor_url == "https://rekor.sigstore.dev"
}
# 拒绝来自 fork 仓库的签名
deny contains msg if {
startswith(input.subject, "https://github.com/myorg/")
not startswith(input.source_repo, "github.com/myorg/")
msg := "signature subject does not match source repository"
}部署门禁侧:在准入控制阶段拦截
无论验证策略多么完善,如果不在部署时强制执行,就等于没有。Kubernetes 的准入控制器(Admission Controller)是最后的防线。
Kyverno 的 verifyImages 规则在 Pod 创建时验证镜像签名,验证失败则拒绝 Pod 创建。OPA Gatekeeper 通过 Constraint 实现同样的效果。Sigstore Policy Controller 则专门为 Sigstore 签名设计,支持更细粒度的 attestation 验证。
对于非 Kubernetes 环境(如裸机部署、VM 镜像启动),验证需要在部署脚本或配置管理工具中完成。Ansible、Puppet、Terraform 都可以在应用配置前调用 cosign verify 或 gh attestation verify。
监控审计侧:持续追踪验证结果
签名验证不是一次性动作,而是持续过程。需要监控的指标包括:
| 指标 | 含义 | 告警阈值 |
|---|---|---|
| 验证成功率 | 通过验证的部署占比 | < 95% 触发告警 |
| 验证延迟 | 从拉取镜像到验证完成的耗时 | P99 > 5s 触发告警 |
| 未签名镜像占比 | 集群中运行但未签名的镜像比例 | > 0% 触发告警 |
| Rekor 查询失败率 | 无法访问 Rekor 导致的验证失败 | > 1% 触发告警 |
| 策略变更频率 | 验证策略的修改次数 | 异常突增触发告警 |
这些指标应该接入现有的可观测性栈(Prometheus + Grafana + Alertmanager),而不是孤立在供应链安全工具内部。
风险边界与未解决问题
keyless signing 和 SLSA provenance 不是银弹。在工程落地之前,需要清醒认识以下边界和限制:
Fulcio 和 Rekor 的可用性依赖
Sigstore 的公网服务(fulcio.sigstore.dev、rekor.sigstore.dev)是免费提供的,但没有 SLA 保证。2024 年 Rekor 曾因证书轮换问题导致大规模验证失败,影响了多个依赖 Sigstore 的 CI/CD 管道。对于生产环境,必须评估是否需要私有化部署,或者至少设计降级策略(如离线验证模式)。
OIDC 供应商的安全边界
Fulcio 信任 OIDC issuer(GitHub、Google、Microsoft 等)。如果攻击者能够伪造 OIDC token(如通过 GitHub Actions 的漏洞或 OIDC 配置错误),就可以获得合法的 Fulcio 证书并签名恶意产物。2023 年曾出现 researchers 通过 GitHub Actions 的 OIDC 配置漏洞获取跨仓库 token 的案例。缓解措施包括:
- 在验证策略中严格限制
subject和issuer,不使用宽泛的正则匹配 - 定期审计 OIDC 配置和 workflow 权限
- 对关键产物使用多重验证(签名 + SLSA provenance + SBOM 交叉比对)
供应链的"最后一公里"问题
签名和 provenance 只能验证"构建产物"的来源,无法验证产物内部的依赖是否安全。一个通过了 Cosign 签名和 SLSA L3 验证的容器镜像,仍然可能包含存在 CVE 的基础镜像层或恶意 npm 包。SBOM(Software Bill of Materials)是弥补这一 gaps 的工具,但 SBOM 本身也需要签名和验证。
完整的验证链条应该是:
大多数团队目前只做到了 A,甚至 A 都只做了一半(签完名不验证)。
跨生态互操作性
Sigstore 的 bundle 格式正在标准化,但不同生态系统(OCI 容器、npm、Python wheel、Maven artifact)的 provenance 格式和验证工具仍然不统一。Cosign v3 开始支持统一验证,但实际落地仍需要按生态适配。对于多技术栈的组织,这增加了策略引擎的复杂度。
结论
Cosign 降低了签名的技术门槛,但没有降低验证的组织门槛。签名是 CI/CD 里的一行命令,验证是横跨安全、运维、开发三个团队的工程协作。当前大多数团队的供应链安全现状是:签名端完成了 80%,验证端完成了 20%,中间隔着策略定义、信任根分发、部署门禁集成和监控审计四条鸿沟。
从工程角度看,补全这个闭环的优先级应该是:
- 先定义策略:明确信任哪些 signer、哪些 builder、验证失败时的处理逻辑
- 再建部署门禁:在 Kubernetes 准入控制或部署脚本中强制执行验证
- 后补 provenance:从 L1/L2 逐步升级到 L3,不要一开始就追求完美的 hermetic build
- 持续监控:把验证成功率、未签名镜像占比等指标接入现有可观测性栈
供应链安全的价值不在"签了名",而在"能验证、敢拦截、可追溯"。签名的成本是一次性的,验证的成本是持续性的——但正是这个持续性成本,区分了安全检查点和真正的安全能力。
