Sooua
登录
返回文章列表
供应链安全··13 分钟阅读

把构建来源当成准入条件:npm 与 GitHub Actions Provenance 的验证边界

从 npm Trusted Publishing、GitHub Artifact Attestations、SLSA Provenance 与 Sigstore 验证链路出发,拆解如何把构建来源从审计材料变成发布和部署准入条件。

把构建来源当成准入条件:npm 与 GitHub Actions Provenance 的验证边界

主题:供应链安全。本文讨论发布物来源证明、验证策略和准入控制,不讨论攻击投递或真实入侵步骤。

结论先说清楚

Provenance(来源证明)不是“这个包安全”的证书。它最多回答三个更窄的问题:这个发布物声称由哪个仓库、哪个工作流、哪个提交和哪组构建指令产生;这个声明是否被可信签名链保护;当前消费者是否愿意接受这条构建路径。

因此,企业落地 npm Trusted Publishing、GitHub Artifact Attestations 或 SLSA Provenance 时,不能把目标写成“开启供应链安全”。更准确的目标是:把“构建来源”从事后审计材料变成发布、入库、部署前的准入条件。

这会改变治理重心:

  • 不是只问“有没有签名”,而是问“签名身份是否绑定到预期仓库和工作流”;
  • 不是只问“有没有 SLSA Provenance”,而是问“predicate 中的 builder、subject、commit、workflow 是否满足策略”;
  • 不是只问“包管理器能不能展示 provenance 徽章”,而是问“CI、制品库、部署入口是否会拒绝不符合策略的制品”。

资料和本地验证边界

本次资料检索覆盖 GitHub Artifact Attestations、actions/attest、npm Trusted Publishing、npm provenance、SLSA provenance、Sigstore/cosign。为了避免把文档理解写成空泛结论,我在本地做了最小可复现检查:

## Tool/environment checks
Tue Jun  9 21:01:29 UTC 2026
v22.22.2
10.9.7
/usr/bin/bash: line 9: gh: command not found
/usr/bin/bash: line 10: jq: command not found
/usr/bin/bash: line 11: cosign: command not found
 
## npm package metadata and attestation API ([email protected] sample)
{
  "dist.tarball": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
  "dist.integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
  "repository.url": "git+https://github.com/npm/node-semver.git"
}

继续用 Python 直接访问 npm registry 和 GitHub API,得到一组更可用的边界证据:

## Python registry/API checks
[email protected] attestation_count= 2
predicate_types= ['https://github.com/npm/attestation/tree/main/specs/publish/v0.1', 'https://slsa.dev/provenance/v1']
bundle_keys= ['mediaType', 'verificationMaterial', 'dsseEnvelope']
bundle_keys= ['mediaType', 'verificationMaterial', 'dsseEnvelope']
semver-7.6.3.tgz bytes= 27678 sha256= 376d2ca2c941fc5a37e9ac3ec65302e5e421e2cc1ee3dee57a854d2bd9bee125
repo= actions/attest default_branch= main stars= 120 pushed_at= 2026-06-09T16:31:17Z latest_release= v4.1.0
repo= sigstore/cosign default_branch= main stars= 6025 pushed_at= 2026-06-09T15:45:31Z latest_release= v3.1.1
repo= slsa-framework/slsa-github-generator default_branch= main stars= 575 pushed_at= 2026-03-29T18:57:33Z latest_release= v2.1.0

这段输出只能证明:当前环境可访问 npm registry 和 GitHub API;[email protected] 的 npm attestation API 返回了两类 predicate;本机没有安装 ghjqcosign,所以本文没有伪装成完成了本地 cryptographic verification。真正的密码学验证需要在安装相应工具、固定可信根和策略参数后执行。

Provenance 解决的是“来源可验证”,不是“内容无风险”

SLSA 对 provenance 的定义非常克制:它是可验证信息,用来把一个 artifact 追溯到复杂供应链中“它从哪里来、何时产生、如何产生”。SLSA 还区分 build provenance 和 source provenance:前者追踪构建输出回到源代码和构建过程,后者追踪源码修订及变更管理过程。

这一区分非常关键。一个 npm 包具备 build provenance,只能说明 tarball 与某次构建声明之间存在可验证关系;它不能自动证明:

  • 源码没有恶意逻辑;
  • 依赖树没有已知漏洞;
  • 维护者账号没有被接管;
  • workflow 本身没有执行过高风险脚本;
  • 构建环境没有从网络拉取未固定的二进制工具;
  • 发布物符合企业内部许可证、隐私和数据流要求。

GitHub Docs 也明确提示,artifact attestations 不是 artifact 安全性的保证,而是把消费者链接到产生该 artifact 的源码和构建指令。安全收益来自后续策略判断,而不是 attestation 文件本身。

可以把它抽象成四层:

如果企业只做到 D 层,把 bundle 生成出来但不在 E/F 层执行策略,这套机制就停留在“可审计但不拦截”。

npm Trusted Publishing:减少长效 token,不等于减少所有发布风险

npm Trusted Publishing 的核心价值是用 OIDC 让 CI/CD 工作流获得短期发布能力,替代长期 npm token。GitHub 2025 年 7 月的变更说明中提到,npm trusted publishing with OIDC 已经 GA;使用 trusted publishing 时,npm CLI 会自动生成并发布 provenance attestations,不再需要额外加 --provenance 参数。

这解决了一个非常具体的痛点:长期 token 放在 CI secrets 中,泄露后可能被复用;OIDC 则把发布身份绑定到特定 CI 运行上下文,令 registry 根据短期断言发放临时能力。

但它不会自动解决这些问题:

风险点Trusted Publishing 能改善什么仍需额外控制什么
长期 npm token 泄露减少或移除发布 token 存储仍要限制能触发 release workflow 的人和分支
发布来源不透明自动生成 provenance,便于追溯仓库和 workflow消费方必须验证 predicate 与预期策略匹配
Workflow 被篡改OIDC 能说明“哪个 workflow 运行了”需要分支保护、CODEOWNERS、复用工作流和审计
构建中拉取未固定依赖provenance 可记录构建上下文仍要 lockfile、固定 digest、离线/镜像策略
包内容恶意或漏洞不直接解决需要 SCA、代码审计、行为分析、权限沙箱

npm provenance 的一个实际边界可以从 [email protected] 样本看到:registry attestation API 返回的不只是 SLSA predicate,还有 npm publish predicate。也就是说,消费者不能简单地“拿到 attestations 数组就通过”,而要明确选择 predicateType == https://slsa.dev/provenance/v1 或符合内部策略的其他 predicate。

predicate_types= [
  'https://github.com/npm/attestation/tree/main/specs/publish/v0.1',
  'https://slsa.dev/provenance/v1'
]

这类细节很适合进入制品准入策略:没有 SLSA provenance 不通过;有 provenance 但 subject digest 不匹配不通过;issuer、repository、workflow 不在允许列表不通过。

GitHub Artifact Attestations:生成只是第一步,验证入口更重要

GitHub Artifact Attestations 允许在 GitHub Actions 中为二进制、容器镜像或 SBOM 生成签名 attestation。官方文档给出的二进制最小配置大致是:

permissions:
  id-token: write
  contents: read
  attestations: write
 
steps:
  - name: Generate artifact attestation
    uses: actions/attest@v4
    with:
      subject-path: 'PATH/TO/ARTIFACT'

如果是容器镜像,则需要 subject-namesubject-digest,并且文档明确提醒 subject-name 应该是 fully-qualified image name,不应包含 tag;digest 必须是 sha256:HEX_DIGEST 形式。这个要求背后的安全逻辑很简单:tag 是可移动指针,digest 才是内容地址。

actions/attest 的 README 还补充了几个实现边界:

  • attestation 使用 in-toto 格式,把 subject 与 predicate 绑定;
  • 签名使用短期 Sigstore 证书;
  • public repository 使用 Sigstore public good instance;
  • private/internal repository 使用 GitHub private Sigstore instance;
  • 私有仓库使用 artifact attestations 需要 GitHub Enterprise Cloud;GitHub Enterprise Server 不支持。

这意味着验证策略不能只写一套“公有 Sigstore + Rekor”的固定逻辑。组织内部至少要区分三种场景:

场景可信根/透明日志边界推荐验证入口常见误判
Public GitHub repo artifactSigstore public good instance,公共 transparency loggh attestation verify 或 cosign bundle 验证以为有公共日志就代表内容可信
Private GitHub repo artifactGitHub private Sigstore instance,无公共 RekorGitHub CLI + 组织权限上下文用 public Rekor 逻辑硬套私有制品
npm package provenancenpm registry attestation API + Sigstore bundlenpm CLI / cosign / 自定义策略不区分 publish predicate 与 SLSA predicate

一个可落地的准入策略应该检查什么

真正有用的 provenance 验证不是“能不能 verify OK”,而是“verify OK 之后是否满足组织策略”。建议把策略拆成六组条件。

策略维度检查对象通过条件示例失败处理
Artifact identity文件 hash、package name、image digestsubject digest 与下载内容一致;镜像只接受 digest,不接受 tag隔离制品,重新拉取或拒绝部署
IssuerOIDC issuer / Sigstore trusted rootGitHub Actions issuer 或组织批准的 CI issuer拒绝;检查是否来自未知 CI
Sourcerepository、owner、commit SHA仓库在 allowlist;commit 属于受保护分支或 release tag进入人工审查或要求重建
Workflowworkflow path、reusable workflow、trigger使用固定 release workflow;禁止临时 workflow 发布拒绝或降级为不可发布
PredicateSLSA predicate type、builder id、materialspredicateType 符合策略;materials 可追踪记录证据,触发供应链审计
Time and revocation签名时间、证书有效期、透明日志/时间戳构建时间在 release 窗口内;证书链可验证拒绝或要求安全团队复核

这组检查可以落到三个入口:

  1. 发布入口:release workflow 结束前生成 attestation,并把 artifact、SBOM、attestation 作为一组不可拆的发布材料;
  2. 制品库入口:自建 registry/proxy 只接受通过策略验证的包或镜像;
  3. 部署入口:Kubernetes admission、CD pipeline 或 IaC plan 阶段验证 digest 与 attestation,不允许“手动替换镜像 tag”。

最小工程实现:先做“证据闭环”,再谈高等级 SLSA

很多团队会一上来讨论 SLSA Build Level 3 或更复杂的 isolated builder,但第一阶段更应该把证据闭环做扎实。一个可执行的落地路径如下。

第一步:发布物必须有不可变身份

npm 包要记录 tarball URL、integrity、sha256;容器镜像要记录 digest;二进制要记录 checksums 文件。没有不可变 subject,attestation 就没有稳定锚点。

可复现实验可以从只读检查开始:

npm view <package>@<version> dist.tarball dist.integrity repository.url --json
python3 - <<'PY'
import hashlib, urllib.request
url='https://registry.npmjs.org/semver/-/semver-7.6.3.tgz'
data=urllib.request.urlopen(url, timeout=30).read()
print(len(data), hashlib.sha256(data).hexdigest())
PY

本文环境对 [email protected] 得到的 SHA256 是:

376d2ca2c941fc5a37e9ac3ec65302e5e421e2cc1ee3dee57a854d2bd9bee125

它只代表本次下载内容的 hash,不代表该包安全,也不代表未来网络路径一定返回同一内容;生产系统应结合 registry metadata、lockfile 和 attestation subject 做一致性校验。

第二步:发布 workflow 要收敛

不要让每个仓库都随手写一份 release YAML。更稳妥的是使用组织级 reusable workflow,业务仓库只传入 package path、artifact name、release tag 等有限参数。这样做的收益有两个:

  • 审计范围从 N 份 release workflow 收敛到少量受控模板;
  • provenance 中的 workflow identity 更容易写入 allowlist。

GitHub Docs 提到,artifact attestations 本身提供 SLSA v1.0 Build Level 2;如果构建发生在跨组织共享且经过审查的 reusable workflow 中,可以进一步接近 Build Level 3 的隔离和受控构建指令目标。这里的重点不是追逐等级数字,而是让“构建指令可审计、可复用、可限制”。

第三步:验证策略要版本化

验证命令不应该散落在 wiki 里,而应该进入代码库,例如:

policy/
  provenance-policy.yaml
  allowed-builders.yaml
  allowed-workflows.yaml
  verify-artifact.sh

策略至少要能回答:

  • 允许哪些 package scope / image repository;
  • 允许哪些 GitHub owner/repo;
  • 允许哪些 workflow path 或 reusable workflow;
  • 是否要求 SLSA provenance;
  • 是否要求 SBOM attestation;
  • 验证失败是 hard fail、quarantine,还是只记录 warning。

第四步:失败要可排错

Provenance 准入失败不能只输出 “verification failed”。对工程团队有用的失败模式应该拆成具体原因:

失败模式可能原因排查证据
digest mismatch下载内容变化、subject 指错、tag 被移动本地 hash、registry metadata、attestation subject
unknown issuer来自未批准 CI、验证工具可信根配置错OIDC issuer、certificate chain、trusted root
workflow mismatchrelease workflow 路径变化、临时 workflow 发布workflow path、run id、commit diff
predicate missing没有生成 SLSA provenance、只生成 publish predicatepredicateType 列表、registry attestation API
private attestation verify failed使用了 public Sigstore 验证逻辑GitHub private Sigstore trusted root、企业权限
policy expired旧仓库迁移、builder id 变更policy 版本、例外审批记录

这张表比“开启 provenance”更接近生产运维:它让开发、平台和安全团队知道应该去哪里找证据。

不要把 cosign / gh / npm CLI 当成同一种验证器

Sigstore Blog 展示了 cosign v2.4.0 之后可以验证 npm provenance、GitHub Artifact Attestations、Homebrew provenance 使用的 bundle 格式。它的示例模式是:拿到 artifact,拿到 bundle,再用 --certificate-oidc-issuer--certificate-identity-regexp 指定期望来源。

这说明 cosign 可以作为底层验证工具,但工程上仍要注意三点:

  1. 工具入口不同:npm CLI、GitHub CLI、cosign 的默认拉取位置、可信根、输出结构并不完全相同;
  2. 公私有边界不同:public GitHub repo 与 private GitHub repo 的 Sigstore 实例和透明日志语义不同;
  3. verify OK 不是最终策略:还要解析 predicate,执行组织自己的 allowlist、denylist、例外和审计规则。

本文本地环境没有安装 cosignghjq,所以没有给出“已实测 verify OK”的结论。更严谨的生产验证应在固定工具版本后记录:工具版本、artifact hash、bundle 来源、trusted root、验证命令、输出摘要和策略版本。

安全收益与剩余风险

Provenance 的主要收益是减少“来源不可知”的供应链风险,并让事后调查更快定位构建路径。但剩余风险仍然很现实:

剩余风险Provenance 的帮助仍需的补偿控制
维护者提交恶意源码可追踪到 commit 和 workflow代码审查、分支保护、行为检测
构建脚本下载动态依赖可暴露材料和构建指令线索依赖锁定、网络 egress 控制、内部镜像
CI runner 被污染可记录 builder 身份但不必然证明 runner 干净ephemeral runner、隔离构建池、runner hardening
策略例外泛滥可产生拒绝证据例外过期、审批留痕、定期回收
消费方不验证没有实际拦截收益制品库/部署准入强制验证

所以最准确的定位是:Provenance 是供应链控制面的“证据层”和“准入输入”,不是漏洞扫描器、不是沙箱、不是代码审计替代品。

结论

把构建来源当成准入条件,核心是把『信任』从人转移到可验证的自动化流程。从资产盘点到发布收敛,从身份绑定到 attestation 生成,从验证策略到准入执行,每一步都需要明确的验收证据。最危险的场景不是攻击者伪造了 artifact,而是团队根本不知道自己依赖了哪些来源。

最后判断

如果团队只是为了让 npm 页面多一个 provenance 标识,收益有限;如果把 provenance 作为制品准入条件,它会迫使发布链路回答一组原本经常被忽略的问题:谁构建的、用什么 workflow 构建的、构建了什么 digest、消费者为什么信任这条路径。

这才是 provenance 的工程价值:不是承诺“软件一定安全”,而是把不可见的构建来源变成可验证、可拒绝、可复盘的控制面输入。

分享

评论

登录 后参与讨论。

加载中…