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.title | env 传递 |
| issue 标题 | github.event.issue.title | env 传递 |
| 分支名/标签 | github.ref、github.head_ref | env 传递 + 白名单校验 |
| 评论内容 | github.event.comment.body | env 传递 + 严格解析 |
| 手动输入 | 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 代码执行 |
| Secrets | env 注入 + Push Protection | 密钥泄露 |
| 注入防御 | 表达式仅经 env 传递 | 脚本注入 RCE |
| 产物可信 | SLSA + Sigstore | 产物篡改、来源不明 |
| Runner 隔离 | 禁用公共仓库自托管 | 宿主机沦陷 |
一句话:把每个 ${{ }}、每个第三方 Action、每个 Secrets 都当成攻击面来对待——最小化、固定化、隔离化、可验证,四件事做到位,工作流才称得上加固。
延伸阅读:
- GitHub Actions 安全与 Secret 管理 — 权限、OIDC、Secrets 全景
- GitHub Actions 自托管 Runner — Runner 隔离与 ARC
- GitHub Actions OIDC 云认证 — 免长期密钥
- GitHub Actions 测试报告与覆盖率集成 — 质量门禁联动
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。