现代应用运行的大部分代码都由外部人员编写,构建系统把它们拉进来,并用手头现成的凭据执行。两起公开事件能说明风险的范围。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的准入策略,拒绝未签名镜像。先用审计模式找出例外,再强制执行。
本周可以做的事
- 搜索工作流中按标签或分支引用的Action,先把能接触密钥的锁定到SHA。
- 列出CI平台中保存的所有长期凭据及其权限范围,把权限最大的换成短期联合身份。
- 在一条生产流水线中加入SBOM生成。
- 演练一下:随机挑一个库,看看找出所有使用它的服务要多久。
最后这个时间,最能说明你的真实水平。