工作流安全加固与供应链防御

深度解析 GitHub Actions 工作流的安全加固实践,涵盖 GITHUB_TOKEN 细粒度权限最小化、第三方 Action 审计与 commit SHA 固定、fork PR 的 pull_request_target 风险、Secrets 保护与告警、工作流注入攻击(script injection)防御、SLSA/Sigstore 供应链签名,以及 Runners 隔离策略。

CI/CD 管道已成为供应链攻击的第一目标:攻击者通过投毒第三方 Action、利用 fork PR 的宽松上下文、或操纵 issue 标题触发注入,最终窃取 Secrets 或篡改产物。本文聚焦工作流自身的加固:从 GITHUB_TOKEN 的细粒度权限、第三方 Action 的 SHA 固定与审计,到注入防御、SLSA/Sigstore 供应链证明,以及 Runner 隔离策略,构建纵深防御。


一、GITHUB_TOKEN 权限最小化

1.1 默认权限的危险

GITHUB_TOKEN 是 workflow 自动注入的临时令牌,其默认权限范围由仓库/组织设置决定。若不显式声明,token 可能拥有仓库内容的写权限——一次注入即可让攻击者修改代码、创建 Release。

1.2 顶层与 job 级双重收窄

name: Secure Workflow
on: [push]

# 顶层:全局默认最小权限
permissions:
  contents: read
  issues: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

  create-release:
    runs-on: ubuntu-latest
    # job 级:按需放大,仅此 job 生效
    permissions:
      contents: write
    steps:
      - run: gh release create v1.0.0

  no-permission-job:
    runs-on: ubuntu-latest
    # 显式关闭全部权限
    permissions: {}
    steps:
      - run: echo "此 job 不需要任何 token 权限"
权限值效果
contents: read只读代码,最常用默认
contents: write可提交、打 tag、发 Release
id-token: write允许获取 OIDC token(云认证必需)
actions: read读取/下载 Artifact 等
packages: write推送 GitHub Packages
permissions: {}清空一切权限

1.3 组织级默认策略

# Organization → Settings → Actions → General
# Default workflow permissions: Read repository contents and packages permissions
# 并勾选:Allow GitHub Actions to create and approve pull requests(按需)

一句话:显式声明 permissions 是工作流安全的起点——默认 contents: read,按 job 需求放大,永不依赖「不写就自动有权限」。


二、第三方 Action 审计与 SHA 固定

2.1 供应链风险模型

第三方 Action 在 Runner 上以你的身份执行代码——一个被投毒的 Action 可以读取所有 Secrets、写仓库、甚至接管云凭证。

引用方式风险说明
@main / @v4(tag)高tag 可被维护者/攻击者移动重写
@v4.0.0中若维护者被攻破仍可篡改
@<40位完整SHA>低提交不可变,精确固定

2.2 SHA 固定写法

steps:
  # ✅ 固定到完整 commit SHA
  - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
  - uses: actions/setup-node@1d0a46970b10f4c00a5f49cae0f628a540f6cc2e  # v4.1.0

  # ❌ 反模式:可变 tag
  # - uses: actions/checkout@v4

2.3 用 Dependabot 持续审计

SHA 固定后仍需跟进上游安全修复——github-actions 生态的 Dependabot 会自动打开升级 PR,并给出新旧版本对比:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
    reviewers:
      - "security-team"
    commit-message:
      prefix: "chore(deps)"

2.4 第三方 Action 审计清单

  • 只用 Verified 创建者 / GitHub 官方 / 高星仓库
  • 审查源码后再引入,尤其是处理 Secrets 的 Action
  • 固定到完整 SHA 而非可变 tag
  • Dependabot 开启 github-actions 生态自动更新
  • 关注上游仓库的 security advisories

三、fork PR 的安全问题

3.1 fork PR 的默认隔离

默认 pull_request 事件下,fork PR 的 Secrets 不可见、GITHUB_TOKEN 权限受限——这是「首次贡献者友好」的安全设计。风险往往来自改用 pull_request_target。

# ⚠️ pull_request_target:在目标分支上下文 + 完整 Secrets 下执行
on:
  pull_request_target:
    types: [opened, labeled]

pull_request_target 让工作流拿到完整 Secrets,但它 checkout 的是目标分支代码——如果 checkout 并执行了 PR 中的代码,攻击者即可在其代码中窃取 Secrets。

3.2 安全使用 pull_request_target 的规则

on:
  pull_request_target:
    types: [opened]

jobs:
  label:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      contents: read
    steps:
      # ✅ 只运行受信任的 github-script,不 checkout PR 代码
      - uses: actions/github-script@v7
        with:
          script: |
            const { data: pr } = await github.rest.pulls.get({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: context.payload.pull_request.number,
            });
            // 仅根据 PR 元数据打标签,绝不执行 PR 代码

3.3 fork PR 安全决策表

场景事件选择是否可运行 PR 代码
普通 CI 测试pull_request✅ 但无 Secrets
需要 Secrets 的检查pull_request_target❌ 严禁 checkout 并运行 PR 代码
打标签/评论pull_request_target + github-script❌ 只用受信脚本

一句话:pull_request_target 的每一行代码都按「PR 代码不可信」假设编写——永远只运行仓库自带的受信逻辑,绝不执行来自 PR 的 checkout 产物。


四、Secrets 保护与告警

4.1 密钥托管与注入规范

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      # ✅ env 注入,避免命令行回显
      - name: Deploy
        env:
          API_KEY: ${{ secrets.API_KEY }}
        run: |
          curl -H "Authorization: Bearer $API_KEY" https://api.example.com

      # ❌ 危险:拼进命令行可能进入日志
      # - run: curl -H "Authorization: Bearer ${{ secrets.API_KEY }}" ...

4.2 启用 Secret Scanning 与 Push Protection

# .github/secret_scanning.yml
secret_scanning:
  enable_push_protection: true   # 阻止含密钥的提交推送到仓库

Push Protection 会在开发者推送包含已知格式密钥的代码时直接拦截,并要求其移除或验证:

GitHub 提示:检测到 AWS Access Key,提交已被阻止。
请移除该密钥或确认其是测试值后使用 --force-with-lease 重新推送。

4.3 告警与轮转

防线工具行为
入库拦截Secret Scanning Push Protection阻止含密钥提交
泄露检测组织级 Secret Scanning扫描历史与新增
依赖审计Dependabot + GitHub Advisories预警已知漏洞
云密钥轮转定期 + 检测到泄露即轮转缩小暴露窗口

4.4 Secrets 轮转建议

Secret 类型轮转周期
云平台长期密钥90 天
代码签名密钥180 天
GITHUB_TOKEN每次 run 自动轮换
OIDC 会话凭证10-15 分钟

五、工作流注入攻击防御

5.1 什么是注入攻击

当不受信输入(PR 标题、issue 内容、分支名)被 ${{ }} 表达式直接拼进 run 脚本时,攻击者可以用特殊字符逃逸出字符串、执行任意命令:

# ❌ 漏洞:PR 标题包含 `"; rm -rf ./ && echo "`
steps:
  - run: echo "PR 标题:${{ github.event.pull_request.title }}"

5.2 正确的防御:环境变量传递

# ✅ 防御:表达式先写入 env,脚本用变量引用
steps:
  - name: Comment with PR title
    env:
      PR_TITLE: ${{ github.event.pull_request.title }}
    run: |
      echo "PR 标题:$PR_TITLE"          # 变量值永不作为代码解析

5.3 注入风险清单

输入源危险表达式防御
PR 标题/正文github.event.pull_request.titleenv 传递
issue 标题github.event.issue.titleenv 传递
分支名/标签github.ref、github.head_refenv 传递 + 白名单校验
评论内容github.event.comment.bodyenv 传递 + 严格解析
手动输入github.event.inputs.*env 传递 + 输入校验

5.4 用 fromJSON 与白名单收敛输入

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      ENV_NAME: ${{ github.event.inputs.environment }}
    steps:
      - name: Validate input
        run: |
          case "$ENV_NAME" in
            staging|production) ;;        # 白名单
            *) echo "非法环境"; exit 1 ;;
          esac

一句话:所有 ${{ }} 表达式都视为「可能被篡改的数据」,只通过 env 传给脚本;脚本内再用白名单、参数化命令做二次校验。


六、供应链 SLSA 与 Sigstore 签名

6.1 SLSA 来源证明

SLSA(Supply-chain Levels for Software Artifacts)为构建产物提供「来源证明」——谁、在哪个工作流、用什么代码构建的。slsa-github-generator 自动生成可验证的 provenance:

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    outputs:
      artifacts-download: ${{ steps.build.outputs.artifacts-download }}
    steps:
      - uses: actions/checkout@v4
      - run: mkdir -p dist && echo "build" > dist/app
      - uses: actions/upload-artifact@v4
        with:
          name: artifacts
          path: dist/**

  provenance:
    needs: build
    permissions:
      actions: read
      id-token: write
      contents: write
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
    with:
      base64-subjects: "${{ needs.build.outputs.artifacts-download }}"

6.2 Sigstore/cosign 对产物签名

steps:
  - uses: sigstore/cosign-installer@v3
  - name: Sign artifact
    run: |
      cosign sign-blob --yes dist/app.tar.gz \
        --output-signature dist/app.tar.gz.sig \
        --output-certificate dist/app.tar.gz.crt

验证侧:

cosign verify-blob \
  --signature dist/app.tar.gz.sig \
  --certificate dist/app.tar.gz.crt \
  --certificate-identity-regexp="^https://github.com/my-org/my-repo/.github/workflows/.*" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  dist/app.tar.gz

6.3 供应链加固对比

手段防御目标落地成本
SHA 固定 Action上游 Action 投毒低
Dependabot 自动更新已知漏洞低
SLSA provenance构建来源不可抵赖中
Sigstore 签名产物被篡改中
依赖锁定(lockfile)依赖版本漂移低

七、Runners 隔离策略

7.1 GitHub 托管 vs 自托管

Runner 类型隔离性Secrets 暴露适用
GitHub-hosted每次全新 VM仅当次 job默认首选
组织自托管共享宿主机可被任意 job 读取需严格隔离
仓库自托管仓库级高风险不推荐

核心规则:绝不让公共仓库 / fork PR 使用自托管 Runner——恶意 PR 可以在你的服务器上执行任意代码。

7.2 自托管 Runner 的隔离策略

# 用标签限制 Runner 用途,并与敏感 job 隔离
jobs:
  deploy:
    runs-on: [self-hosted, production-safe]   # 专用标签组
    environment: production
    steps:
      - run: ./deploy.sh
隔离层级方法强度
标签隔离runs-on 标签区分用途弱
独立用户每个 runner 专用系统用户中
容器隔离runner 运行在容器中中
VM 隔离每次 job 全新 VM强
ARC Pod 隔离每 job 独立 Pod强

7.3 审计日志监控

# 定期拉取组织 audit log,监控高风险操作
curl -H "Authorization: token $TOKEN" \
  "https://api.github.com/orgs/my-org/audit-log?phrase=action:workflows"

八、安全加固检查清单

  • 所有 workflow 显式声明 permissions(默认只读)
  • 组织级默认权限设为只读
  • 第三方 Action 固定到完整 commit SHA
  • Dependabot 开启 github-actions 生态更新
  • pull_request_target 绝不执行 PR 代码
  • 所有 ${{ }} 输入经 env 传递,避免脚本注入
  • Secrets 仅 env 注入,开启 Push Protection
  • 生产产物启用 SLSA + Sigstore 签名
  • 公共仓库禁用自托管 Runner
  • 自托管 Runner 启用标签/容器/VM 隔离
  • 定期轮转长期 Secrets
  • 接入审计日志与异常告警

总结

工作流安全是「纵深防御」的工程实践,从令牌权限到供应链证明层层设防。

防线关键手段阻断的攻击
令牌权限显式 permissions 最小化越权读写、密钥窃取
Action 供应链SHA 固定 + Dependabot上游 Action 投毒
fork PR正确使用 pull_request_target恶意 PR 代码执行
Secretsenv 注入 + Push Protection密钥泄露
注入防御表达式仅经 env 传递脚本注入 RCE
产物可信SLSA + Sigstore产物篡改、来源不明
Runner 隔离禁用公共仓库自托管宿主机沦陷

一句话:把每个 ${{ }}、每个第三方 Action、每个 Secrets 都当成攻击面来对待——最小化、固定化、隔离化、可验证,四件事做到位,工作流才称得上加固。


延伸阅读:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「github-actions」更多文章

  1. 移动端 CI/CD:Flutter/iOS/Android 构建与签名
  2. 环境保护与部署门禁:环境规则、审批与 CD 流程
  3. 基础设施即代码自动化:Terraform/CloudFormation 与 Atlantis