一句话版本
CI/CD 流水线是一台手握所有钥匙的机器:它能读源代码、拿到部署凭证、推送到生产环境、发布软件包。攻击者如果能在流水线里运行自己的代码,就等于拿到了这些钥匙。 常见的入口有三个:外部贡献者的 PR 在有权限的上下文里运行、第三方 Action 被篡改、长期有效的密钥存在流水线里被读走。对策是:不信任外部代码、把第三方组件锁定到具体提交、用 OIDC 换取短期凭证。
全文分三层:
- 通俗理解:自动化工厂的钥匙柜
- 实际用处:Codecov、tj-actions、各种
pull_request_target事故 - 原理与工程要点:触发器的权限差异、锁定 Action、OIDC、最小权限
第一层:通俗理解
1.1 自动化工厂的钥匙柜
想象一个全自动工厂:
- 任何人都可以往"原料入口"放东西(提交 PR);
- 工厂会自动加工(构建、测试);
- 加工完成后,工厂用自己的钥匙把产品送进仓库、发到商店(部署、发布)。
如果工厂在加工陌生人送来的原料时,钥匙柜是开着的,那陌生人只要在原料里藏一段"把钥匙复印一份寄给我"的指令就行了。
1.2 流水线手里有什么
| 资产 | 被拿到的后果 |
|---|---|
| 云部署凭证 | 控制生产环境 |
| 软件包发布令牌 | 发布恶意版本,影响所有下游用户(见《依赖投毒》) |
| 代码仓库写权限 | 修改代码、植入后门 |
| 签名密钥 | 给恶意软件签上"官方"签名 |
第二层:实际用处
| 事件 | 经过 |
|---|---|
| Codecov(2021) | 攻击者篡改了 Codecov 提供的上传脚本,大量客户在 CI 里运行这个脚本时,环境变量(包含各种凭证)被发送给攻击者 |
| tj-actions/changed-files(2025,CVE-2025-30066) | 一个被两万多个仓库使用的 GitHub Action 被篡改,攻击者把它的所有版本标签都指向了恶意提交。恶意代码会把流水线中的密钥打印到构建日志里,而公开仓库的构建日志任何人都能看到 |
pull_request_target 误用(多起) |
很多项目为了让外部 PR 也能评论、打标签,使用了这个高权限触发器,却又在其中检出并运行了 PR 里的代码,导致外部贡献者能读取仓库密钥 |
| 自托管运行器被持久化 | 公开仓库使用自托管运行器时,外部 PR 的代码可以在运行器上留下后门,影响之后所有的构建 |
第三层:原理与工程要点
3.1 触发器的权限差异(以 GitHub Actions 为例)
| 触发器 | 运行哪份代码 | 能拿到密钥吗 | 风险 |
|---|---|---|---|
pull_request(来自 fork) |
PR 的代码 | 不能,令牌只读 | 低 |
pull_request_target |
目标分支的工作流定义 | 能 | 如果工作流里检出并运行了 PR 的代码,就等于把密钥交给了外部贡献者 |
workflow_run |
前一个工作流完成后触发 | 能 | 如果处理前一个工作流的产物(可能来自 PR),同样危险 |
原则:在有权限的上下文里,绝不执行不可信代码。如果必须处理外部 PR 的结果,就把"运行不可信代码"和"使用密钥"拆成两个独立的工作流,中间只传递经过验证的数据。
3.2 锁定第三方 Action
# 危险:标签可以被改指向别的提交
- uses: some-org/some-action@v4
# 安全:锁定到完整的提交哈希
- uses: some-org/some-action@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v4.1.0(示例哈希)
tj-actions 事件里,攻击者正是把版本标签改指向了恶意提交。锁定到提交哈希,这种攻击就无效了。配合依赖更新工具(如 Dependabot、Renovate)来更新哈希,更新时审查变更内容。
3.3 OIDC:用短期凭证代替长期密钥
传统做法是把云平台的长期访问密钥存成 CI 密钥。泄露一次,攻击者可以长期使用。
OIDC 的做法:
- CI 平台为每次运行签发一个身份令牌,里面写明"这是某仓库、某分支、某工作流的运行";
- 云平台验证这个令牌,按预先配置的规则(比如"只信任 main 分支的部署工作流")发放一个几十分钟就过期的临时凭证;
- CI 里不存在任何长期密钥。
即使临时凭证被泄露,它也很快失效,而且只能被符合条件的工作流获得。
3.4 最小权限
| 措施 | 说明 |
|---|---|
| 默认令牌权限设为只读 | 在仓库或组织设置里,把工作流令牌的默认权限设为只读,需要写权限的工作流单独声明 |
| 密钥按环境隔离 | 生产部署密钥只在"生产环境"里可用,并要求人工批准 |
| 分支保护 | 部署只能从受保护的分支触发;修改工作流文件需要代码审查 |
| 公开仓库不用自托管运行器 | 或者每次运行后销毁运行器 |
| 日志脱敏 | 平台会自动遮盖已注册的密钥,但经过编码(如 Base64)后的密钥可能遮盖不了 |
3.5 发布物的来源证明
即使流水线本身安全,用户也需要能验证"这个发布包确实是从这个仓库的这个提交构建出来的"。构建来源证明(如 SLSA、Sigstore、软件包仓库的可信发布)把这一点变成可以自动验证的签名,防止"源码干净、发布包有毒"(见《依赖投毒》 里的 xz 事件)。
总结
| 层次 | 要点 |
|---|---|
| 风险 | 流水线手握所有钥匙 |
| 入口 | 外部 PR 在高权限上下文运行、第三方 Action 被篡改、长期密钥 |
| 关键做法 | 不在有权限的上下文运行不可信代码;Action 锁定提交哈希;OIDC 短期凭证 |
| 补充 | 默认只读令牌、环境隔离、分支保护、来源证明 |