开源项目 CI/CD 安全:谁能在你的流水线里跑代码?

2026-06-04 28 预计阅读时间:1 分钟
来源:cncf.io AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:9 分钟

过去一年,开源供应链几乎成了攻击者的游乐场。Axios 在 npm 上被劫持,正常版本里混进了远程访问木马;LiteLLM 的 PyPI 包同样遭篡改——这些都不是理论风险,而是已经发生的事。攻击路径往往指向同一个薄弱环节:CI/CD 流水线的执行权限。谁能触发 workflow?谁能往流水线里注入步骤?谁能拿到签名密钥?这些问题如果没想清楚,项目再火也只是给攻击者递了一把更锋利的刀。

攻击者怎么利用你的 CI/CD

开源项目的 CI/CD 天然比私有项目更"开放"——任何人都能提交 PR、fork 仓库、触发部分 workflow。这种开放性正是攻击面:

  • 恶意 PR 触发 workflow:攻击者提交一个看似正常的 PR,workflow 运行时执行了 PR 中注入的脚本,窃取仓库 secrets。
  • Fork 仓库的 pull_request_target 陷阱pull_request_target 事件允许来自 fork 的 PR 访问主仓库 secrets,如果 workflow 代码本身被 PR 修改并合并执行,密钥就泄露了。
  • 标签/发布流程劫持:攻击者通过社会工程或权限漏洞推送恶意 tag,触发 release workflow,在发布产物中植入后门。

Axios 和 LiteLLM 事件的共同点:攻击者拿到了发布流程的控制权,然后用这个权限把恶意代码塞进了用户信任的包里。

GitHub Actions 里最危险的三个关键字

先看一段典型的"危险 workflow":

# ⚠️ 危险示例——不要直接用
name: CI
on:
  pull_request_target:  # 允许 fork 的 PR 触发,且能访问 secrets
  push:
    tags: ['v*']

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # checkout PR 代码
      - run: ./run-tests.sh  # 执行 PR 中可能被修改的脚本
      - run: echo "${{ secrets.RELEASE_TOKEN }}" | curl ...  # 泄露密钥

三个问题叠加在一起:

  1. pull_request_target:让 fork PR 能跑 workflow,同时赋予 secrets 访问权。
  2. checkout PR head SHA:把 fork 的代码拉到能访问 secrets 的环境里。
  3. 执行 PR 中的脚本或引用 PR 修改的 action:攻击者控制了执行内容。

单独看每一步都有合理用途,组合起来就是密钥泄露的完整路径。

锁住流水线:分层防御策略

第一层:限制谁能触发

最直接的办法——把敏感 workflow 的触发条件收紧:

name: Release
on:
  push:
    tags: ['v*']
    # 只允许特定分支的 tag 触发
  workflow_dispatch:
    # 手动触发,但加上权限限制

permissions:
  contents: write
  id-token: write

jobs:
  release:
    runs-on: ubuntu-latest
    # 关键:限制执行者身份
    if: github.actor == 'maintainer-handle' || github.actor == 'another-maintainer'
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Build and publish
        run: |
          npm run build
          npm publish --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

if 条件直接过滤执行者。不是 maintainer?workflow 跳过。这比事后审计快得多。

第二层:用环境保护规则做门禁

GitHub Environments 不只是部署目标,它是一套审批机制:

name: Publish to PyPI
on:
  push:
    tags: ['v*']

jobs:
  publish:
    runs-on: ubuntu-latest
    environment: pypi-release  # 关键行
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - name: Build package
        run: |
          pip install build
          python -m build
      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        # secrets 在 environment 层面配置,需要审批才能访问

在 GitHub 仓库设置里配置 pypi-release 环境:

  • Required reviewers:指定 1-2 个 maintainer 必须手动审批,workflow 才能继续。
  • Wait timer:加一个 5 分钟等待窗口,给自动化攻击增加时间成本。
  • Branch protection:只允许 main 分支的 tag 触发这个环境。

这样即使攻击者推了一个恶意 tag,workflow 也会停在审批环节,maintainer 看到异常可以直接拒绝。

第三层:PR workflow 的安全写法

来自 fork 的 PR 仍然需要跑测试,但绝不能让 PR 代码接触 secrets:

name: PR Tests
on:
  pull_request:  # 注意:用 pull_request,不是 pull_request_target

permissions:
  contents: read  # 只给读权限,不给写权限

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        # 默认 checkout base 分支代码,不 checkout PR head
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - name: Install dependencies
        run: pip install -e ".[test]"
      - name: Run tests
        run: pytest tests/
        # 这里不使用任何 secrets

关键选择:

  • pull_request 而不是 pull_request_target——前者不授予 secrets 访问权。
  • permissions 显式声明为 contents: read——最小权限原则。
  • 不 checkout PR 的 SHA,不执行 PR 中的自定义脚本。

如果确实需要 fork PR 访问某些 secrets(比如需要 API key 跑集成测试),用 pull_request_target绝不 checkout PR head SHA,也绝不 run PR 中可能被修改的文件。把测试逻辑完全放在 base 分支的代码里。

密钥管理:别把钥匙放在门旁边

CI/CD 里的 secrets 是最终目标。几个实操原则:

# 检查你的仓库 secrets 列表——定期审计
gh secret list -R your-org/your-project

# 删除不再使用的 secrets
gh secret delete DEPRECATED_KEY -R your-org/your-project

# 用 environment-level secrets 替代 repository-level secrets
gh secret set NPM_TOKEN --env npm-release -R your-org/your-project
  • Repository secrets 对所有 workflow 可见——风险最高。
  • Environment secrets 只在指定环境的 job 里可用——配合审批机制,安全性大幅提升。
  • OIDC tokenid-token: write)比固定密钥更安全:PyPI、npm、AWS 都支持 OIDC 联合认证,不需要在 GitHub 里存任何长期密钥。

OIDC 发布到 PyPI 的配置:

- name: Publish to PyPI via OIDC
  uses: pypa/gh-action-pypi-publish@release/v1
  with:
    # 不需要 PyPI API token,OIDC 自动获取短期凭证
  permissions:
    id-token: write  # 必须声明

去 PyPI 项目设置里配置 Trusted Publisher,把 GitHub repository 和 environment 绑定好。之后发布完全不需要存储任何 token。

自检清单

把下面这些项逐条过一遍,你的项目 CI/CD 安全水位就会明显提升:

检查项 状态
所有 release/publish workflow 是否限制了触发者身份(if 条件或 environment 审批)
PR workflow 是否用 pull_request 而非 pull_request_target
使用 pull_request_target 时是否绝不 checkout PR head SHA
每个 workflow 是否显式声明了最小 permissions
Repository-level secrets 是否已迁移到 environment-level
发布流程是否已迁移到 OIDC 联合认证(PyPI/npm/AWS)
是否定期用 gh secret list 审计并清理废弃密钥
Fork PR 是否禁止执行 PR 中自定义脚本或 action

开源项目的 CI/CD 安全不是一次性配置,而是持续维护。每次加新 workflow、新 secret、新发布目标时,问自己一个问题:如果攻击者控制了这个触发点,他们最多能拿到什么? 答案如果包含发布密钥或用户信任的产物,就需要加一层门禁。


相关推荐