现代应用运行的大部分代码都由外部人员编写,构建系统把它们拉进来,并用手头现成的凭据执行。两起公开事件能说明风险的范围。2024年,广泛使用的开源压缩库xz被发现存在后门(CVE-2024-3094),后门就藏在发布制品里。2025年,GitHub Action tj-actions/changed-files遭到入侵(CVE-2025-30066),导致CI密钥暴露在构建日志中。

这些代码你不可能全部审查,所以现实的目标要窄一些:确切知道运行了什么,限制问题组件能碰到的范围,并能证明交付了什么。四个习惯就能覆盖大部分工作。

锁定构建所用的一切

版本范围意味着同一次提交在不同日子可能构建出不同的软件。锁定版本让构建可重复,也让上游变化成为可审查的差异。

  • 提交锁文件,并使用遵循锁文件的安装方式,如npm ci;Python可考虑pip install --require-hashes。
  • 容器基础镜像按摘要(digest)引用,因为标签可以移动。
  • CI的Action和可复用工作流锁定到完整的提交SHA。GitHub的安全指南指出,这样才能把Action当作不可变版本使用。标签可以被改指向,tj-actions事件正是如此。
  • 条件允许时通过内部代理或制品库安装,上游包被删除也不影响构建,而且有了统一执行策略的地方。

锁定也有代价。从不更新的版本会过时并积累已知漏洞,所以要配合自动更新的合并请求和可信的测试。可以考虑让新版本放几天再自动采用,给社区留出发现恶意版本的时间;安全修复则走单独的快速通道。

生成SBOM,并真正用起来

软件物料清单(SBOM)是一份正式的机器可读清单,列出软件包含的组件,以及版本和供应关系。常见格式是CycloneDX和SPDX。

SBOM本身拦不住任何攻击。它的价值体现在新的严重漏洞公布的那个早上:“我们哪里在用这个组件?”可以靠一条查询回答,而不用发一周邮件。

  • 每次发布都在流水线中生成,尽量基于构建产物,这样才能反映实际交付的内容。
  • 与制品一起存放,按版本或摘要索引。
  • 导入可检索的系统,并接入漏洞匹配,不用问就能知道。
  • 向关键软件供应商索要他们的SBOM,他们的依赖也会传给你。

把CI当生产环境对待

构建系统通常既能发布软件,也能部署到生产环境。攻击者很清楚这一点,所以它的凭据至少要像生产管理员权限一样谨慎对待。

  • 通过CI平台与云服务商之间的OIDC联合,用短期令牌取代长期云密钥。
  • 每个令牌限定到一个代码库,最好再限定到一个受保护的分支或环境。
  • CI令牌默认只读,按作业单独授予更多权限。
  • 不要把密钥暴露给由外部fork的合并请求触发的工作流。
  • 把构建和部署拆成独立作业、使用不同凭据,被攻陷的测试依赖就碰不到生产部署密钥。
  • 优先使用用完即弃的运行器,避免被攻陷的作业给下一个作业留下东西。

签名,并且检查签名

签名能证明制品来自你的流水线,且之后没有被改动。Sigstore让这件事容易了很多,比如Cosign工具和绑定CI工作负载身份的无密钥签名。来源证明(provenance)更进一步,记录制品如何由什么源码构建,SLSA框架提供了可以逐级推进的目标。

很多团队跳过的一步是验证。没人检查的签名只是摆设。把检查放进部署路径,比如Kubernetes的准入策略,拒绝未签名镜像。先用审计模式找出例外,再强制执行。

本周可以做的事

  1. 搜索工作流中按标签或分支引用的Action,先把能接触密钥的锁定到SHA。
  2. 列出CI平台中保存的所有长期凭据及其权限范围,把权限最大的换成短期联合身份。
  3. 在一条生产流水线中加入SBOM生成。
  4. 演练一下:随机挑一个库,看看找出所有使用它的服务要多久。

最后这个时间,最能说明你的真实水平。