Caloogy 文章
English

CI/CD 流水线安全

日期
2025 年 9 月 13 日
系列
基础设施
阅读
4 分钟
主题
InfraCi cdGithub actionsSupply chain

一句话版本

CI/CD 流水线是一台手握所有钥匙的机器:它能读源代码、拿到部署凭证、推送到生产环境、发布软件包。攻击者如果能在流水线里运行自己的代码,就等于拿到了这些钥匙。 常见的入口有三个:外部贡献者的 PR 在有权限的上下文里运行、第三方 Action 被篡改、长期有效的密钥存在流水线里被读走。对策是:不信任外部代码、把第三方组件锁定到具体提交、用 OIDC 换取短期凭证。

全文分三层:

  1. 通俗理解:自动化工厂的钥匙柜
  2. 实际用处:Codecov、tj-actions、各种 pull_request_target 事故
  3. 原理与工程要点:触发器的权限差异、锁定 Action、OIDC、最小权限

第一层:通俗理解

1.1 自动化工厂的钥匙柜

想象一个全自动工厂:

如果工厂在加工陌生人送来的原料时,钥匙柜是开着的,那陌生人只要在原料里藏一段"把钥匙复印一份寄给我"的指令就行了。

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 的做法:

  1. CI 平台为每次运行签发一个身份令牌,里面写明"这是某仓库、某分支、某工作流的运行";
  2. 云平台验证这个令牌,按预先配置的规则(比如"只信任 main 分支的部署工作流")发放一个几十分钟就过期的临时凭证;
  3. CI 里不存在任何长期密钥。

即使临时凭证被泄露,它也很快失效,而且只能被符合条件的工作流获得。

3.4 最小权限

措施 说明
默认令牌权限设为只读 在仓库或组织设置里,把工作流令牌的默认权限设为只读,需要写权限的工作流单独声明
密钥按环境隔离 生产部署密钥只在"生产环境"里可用,并要求人工批准
分支保护 部署只能从受保护的分支触发;修改工作流文件需要代码审查
公开仓库不用自托管运行器 或者每次运行后销毁运行器
日志脱敏 平台会自动遮盖已注册的密钥,但经过编码(如 Base64)后的密钥可能遮盖不了

3.5 发布物的来源证明

即使流水线本身安全,用户也需要能验证"这个发布包确实是从这个仓库的这个提交构建出来的"。构建来源证明(如 SLSA、Sigstore、软件包仓库的可信发布)把这一点变成可以自动验证的签名,防止"源码干净、发布包有毒"(见《依赖投毒》 里的 xz 事件)。


总结

层次 要点
风险 流水线手握所有钥匙
入口 外部 PR 在高权限上下文运行、第三方 Action 被篡改、长期密钥
关键做法 不在有权限的上下文运行不可信代码;Action 锁定提交哈希;OIDC 短期凭证
补充 默认只读令牌、环境隔离、分支保护、来源证明