读书笔记 · 约 12,848 字 · 预计推送 2 部分

GitHub Actions 安全实践:权限最小化、OIDC、Fork PR、Secrets 与依赖钉扎(主题精读) 未开始

目录

题记与说明

来源清单(本次实际精读)

  1. GitHub Docs — Security hardening for GitHub Actions(官方加固总纲)

https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions

  1. GitHub Docs — Events that trigger workflows(pull_request / pull_request_target / workflow_run 权限语义)

https://docs.github.com/en/actions/reference/events-that-trigger-workflows

  1. GitHub Docs — About security hardening with OpenID Connect(OIDC 官方说明)

https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect

  1. GitHub Security Lab — Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests(作者 jarlob,2021-08-03 发布;示例代码 2026-06-17 更新)※ 本文为全文精读

https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/

  1. StepSecurity — Harden-Runner detection: tj-actions/changed-files action is compromised(2025-03 供应链事件复盘,CVE-2025-30066)

https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised

提取方式说明:来源 4 通过 webReader 读到全文原文;来源 1/2/3/5 通过 WebFetch 对页面内容做定向提取(其中标注为引述的英文句子为提取结果中的原句)。GitHub 文档 URL 近期有过迁移(本次有两条旧 URL 返回 404 后改用现行地址)。

一、核心问题

GitHub Actions 把不可信输入(外部 fork 的 PR、第三方 Action、依赖包、PR 标题等事件上下文)和高权限资产(仓库写权限的 GITHUB_TOKEN、secrets、自托管 runner、云部署凭证)放进了同一个执行面。核心问题是:如何系统性收紧这条信任边界,使得"一次恶意 PR"或"一次第三方 Action 被投毒"不再等于仓库沦陷或 secrets 泄漏。五个子问题恰好对应主题的五个关键词:

二、主要主张

2.1 权限最小化:默认拒绝、按 job 提升

2.2 触发器即权限边界:pull_request vs pull_request_target vs workflow_run

官方 events 文档的原文(本次提取):

pwn request(Security Lab 核心结论):*Combining pull_request_target workflow trigger with an explicit checkout of an untrusted PR is a dangerous practice that may lead to repository compromise.* 攻击路径很朴素——fork PR 提交恶意 build 脚本/新增 npm 依赖(preinstall/postinstall 钩子)/新测试用例,一旦特权 workflow 显式 checkout PR head(ref: ${{ github.event.pull_request.head.sha }}),不可信代码就在带 secrets 与写 token 的上下文里执行了。官方加固文档直接定性:*"These vulnerabilities can be exploited to take over a repository."*,并规定 *"Workflows that use these triggers must not explicitly check out untrusted code, including from pull request forks"*。

安全的两段式模式(Security Lab 给出的标准解,示例代码已更新到 2026-06 版且所有 action 钉 SHA):无特权的 pull_request workflow 跑构建并把 PR 号等结果存为 artifact 后退出;带特权的 workflow_run workflow 下载 artifact、校验(PR 号按整数解析校验)后再执行需要权限的动作。文中明确提示:artifact 本身也是不可信输入——"Artifacts resulting from untrusted PR data are themselves untrusted",绝不能在特权上下文里运行不可信 PR 构建出的二进制。

2.3 OIDC:用短期身份令牌替代长期云凭证

官方 OIDC 文档的机制:云侧先配置对特定 GitHub 仓库/工作流的信任(trust relationship);每个 job 运行时 GitHub 的 OIDC provider *"auto-generates an OIDC token"*(RS256 签名的 JWT,每个 job 唯一);workflow 步骤取 token 交给云厂商验签,云厂商校验 claims 后 *"provides a short-lived cloud access token that is available only for the duration of the job"*。关键主张:

2.4 Secrets 管理:写权限即读权限

2.5 依赖钉扎:tag 可被追溯改写,SHA 不可

三、方法(技术路线与论证方式)

四、数据与证据(含关键原文引述)

#证据出处
1fork PR 默认隔离:*"The GITHUB_TOKEN has read-only permissions in pull requests from forked repositories."*;*"With the exception of GITHUB_TOKEN, secrets are not passed to the runner when a workflow is triggered from a forked repo"*GitHub Docs: Events that trigger workflows
2pwn request 定性:*"Combining pull_request_target workflow trigger with an explicit checkout of an untrusted PR is a dangerous practice that may lead to repository compromise."*;*"These vulnerabilities can be exploited to take over a repository."*;修复建议含 label 门控仅作临时方案(*"prone to a race condition in which the attacker may push new changes after the workflow was approved ... but has not started yet"*)GitHub Security Lab: Preventing pwn requests
3token 落盘坑:actions/checkout 的 persist-credentials 默认为 true,*"it makes it even worse ... any running code can simply read the stored repository token from the disk"*GitHub Security Lab: Preventing pwn requests
4修复后遗留 PR 坑:*"All PRs that were opened before a fix was made to the vulnerable workflow will use the version of the workflow as it existed at the time the PR was opened."* → 需关闭或 rebase 旧 PRGitHub Security Lab: Preventing pwn requests
5secrets 边界:*"Any user with write access to your repository has read access to all secrets configured in your repository."*;结构化数据/派生值脱敏失效两条规则GitHub Docs: Security hardening / Secure use reference
6SHA 钉扎理由:*"they would need to generate a SHA-1 collision"*;lockfile *"supersedes SHA pinning"*;Dependabot 告警对 SHA 钉扎不生效GitHub Docs: Security hardening for GitHub Actions
7tj-actions 事件量化:约 23,000 仓库受影响;v1–v45.0.7 全部 tag 追溯改指恶意 commit 0e58ed86...;payload 从 Runner.Worker 进程内存 grep "isSecret":true 后双 base64 打进日志;入口为 @tj-actions-bot 的 PAT 失陷;检测靠 egress 异常(gist.githubusercontent.com);*"There is no evidence that the leaked secrets were exfiltrated to any remote network destination"*StepSecurity 复盘 + GHSA-mw4p-6x4p-x5m5
8OIDC 短期性:*"is only valid for a single job, and then automatically expires"*;permissions: id-token: write;2026-07-15 后新仓库用含 owner/repo ID 的不可变 sub 格式(GHES 除外);AWS 不支持自定义 claimsGitHub Docs: About security hardening with OpenID Connect
9自托管 runner:*"can be persistently compromised by untrusted code in a workflow"*,公共仓库*"should essentially never be used"*;JIT runner *"perform at most one job before being automatically removed"*GitHub Docs: Security hardening(runner 章节)

证据充分度评估:官方规范与攻击模式互相印证,且有一条真实大规模事件作实证,主张的可信度高。不足:除 tj-actions 外缺少"遵循这些实践能降低多少风险"的量化研究;Security Lab 文章发布于 2021 年(文中锁文件等新机制未覆盖,需以现行文档为准)。

五、结论

GitHub Actions 安全本质是信任边界管理,可压缩成一套最小实践集:

  1. 权限:workflow 级先写 permissions: contents: read,job 级按需提升;默认只读要成为肌肉记忆。
  2. Fork PR:不可信代码只允许在 pull_request(无 secrets、只读 token)下跑;需要写权限/secret 的动作放到 pull_request + workflow_run 两段式,artifact 按不可信数据校验;pull_request_target 仅限"不 checkout PR 内容"的场景,且永不显式 checkout fork head。
  3. OIDC:云部署一律用 OIDC 短期 token 替代长期 AK/SK secret;信任条件写到 sub/aud 级别。
  4. Secrets:能不存就不存;绝不整体存 JSON/YAML;派生值也要注册;泄漏即删日志+轮换;高敏 secret 用 environment + required reviewers 门控。
  5. 依赖钉扎:第三方 action 钉 full-length commit SHA(或采用 actions lockfile);配合 CODEOWNERS 保护 .github/workflows/、开启 Dependabot version updates 弥补 SHA 钉扎下的告警盲区;公共仓库基本不用自托管 runner。
  6. 检测:CodeQL Actions 扫描 + OpenSSF Scorecards + 审计日志 + 运行时 egress 监控(tj-actions 正是被 egress 异常抓到的)。

反直觉要点合集:persist-credentials 默认 true(token 落盘);修复 workflow 后旧 PR 仍跑旧版 workflow(需 close/rebase);"safe to test" label 门控有竞态;pull_request_target 默认 checkout 的是 base 分支——常见"失效但不构成漏洞"的假 CI;Dependabot 安全告警看不见 SHA 钉扎的 action。

六、局限

七、对我的启发

  1. 对我自己仓库的立即行动:给所有 workflow 顶部加 permissions: contents: read,需要写权限的 job 单独声明;uses: 全部换成 full-length commit SHA(可借 Dependabot version updates 保持更新,弥补告警盲区)。
  2. 模板即防线:把 Security Lab 的 pull_request + workflow_run 两段式沉淀成自己的可复用 workflow 模板(含 artifact 校验、persist-credentials: false、action 全部钉 SHA 的版本),新项目直接继承,而不是每次重写时重新踩坑。
  3. 把 OIDC 当默认而非优化:任何涉及云厂商部署(AWS/Azure/GCP)的项目,第一选择是 OIDC trust policy,secret 只留真正不可联邦化的凭据;这直接消灭了"日志泄漏 secret"这一整类事故。
  4. 审计视角迁移:review 别人/生成的 workflow 时,先看三件事——on: 里有没有 pull_request_target、run: 里有没有插值 ${{ github.event.* }}、uses: 是不是 tag 引用;这三处命中就是高危信号(CodeQL/Scorecards 的关注点也是如此)。
  5. 写自动化流水线的通用原则:可追溯引用(SHA/lockfile)+ 短期凭证(OIDC)+ 权限默认拒绝(permissions 键)+ 不可信输入隔离(触发器边界)——这套组合不依赖具体平台,迁移到其他 CI(GitLab CI、Gitea Actions)时对应物基本都存在。

*笔记由精读员·#2 于 2026-11-01 生成;仅写入本文件,未改动其他条目笔记、matrix.csv、backlog 或 nightly-log.md。*

← 资料库今日播报 →