Astral 的开源供应链安全实践:从 pwn requests 到双人审批发布
阅读时间: 大约 11 分钟
Astral 的开源供应链安全实践:从 pwn requests 到双人审批发布

2026 年 4 月 8 日,Astral 的 William Woodruff(@woodruffw)发表《Open source security at Astral》,系统披露了这家维护 Ruff、uv、ty 等「数百万开发者依赖」工具的公司如何加固自己的 CI/CD、仓库、发布与依赖链路。文章的直接背景是接连发生的供应链攻击——近期的 Trivy、LiteLLM 入侵,以及更早的 Ultralytics、tj-actions、Nx 事件,官方指出这些 compromise「都始于 pwn requests 这类老掉牙的弱点」。本文对照原文,把措施、它们解决的问题、以及官方自己承认的不足逐条拆开。
一、CI/CD 安全:把危险触发器整个禁掉
Astral 的判断是:GitHub Actions 集成好、贡献者工作流成熟,但「安全默认值很差」。他们的做法不是修修补补,而是先划线:
- 全组织禁用
pull_request_target与workflow_run这类高权限触发器。官方经验是很多项目「以为自己需要」它们(最常见的是给第三方 PR 留言),但绝大多数场景用普通pull_request、job summary 或日志就能替代。 - 所有 action 必须钉到具体 commit,而不是可变的 tag/branch;并用两种方式交叉校验不是「冒名 commit」(impostor commit):本地用 zizmor 的
unpinned-uses与impostor-commit审计,再加 GitHub 官方「require actions to be pinned to a full-length commit SHA」的硬门禁。为了让「间接 action(我们调用的 action 再调用的 action)」也全部哈希钉死,他们还和下游依赖协调,把整条依赖图都钉上。 - 权限默认最小:组织级默认只读,每个 workflow 开头写
permissions: {},再按 job 逐项放宽。 - 按部署环境隔离 secrets:测试、lint 这类 job 拿不到发布制品所需的凭证,缩小被攻破后的爆炸半径。辅助工具是 zizmor(静态分析)与 pinact(自动钉版本)。
这里有一个官方主动写下的 nuance:哈希钉死是「必要不充分」。它保证 action 内容不可变,但挡不住「不可变的内容去做可变的决定」——比如 action 内部从某个 GitHub release 下载「最新版」二进制。官方承认目前没有工具能很好发现这类不可变性缺口,只能靠人工 review action 依赖;发现缺口后再推动上游补上「下载 URL↔密码学哈希」映射,让哈希成为 action 不可变状态的一部分。
二、仓库与组织安全:保护连 admin 也绕不过

在账号与仓库层,Astral 的措施围绕「减少可被攻破的高价值账号 + 保护规则不可被管理员绕过」:
- 限制拥有 admin 等高权限角色的账号数量,多数成员只对需要的仓库有读写;
- 全员强制强 2FA,门槛高于 GitHub 默认的「任意 2FA 即可」——要求不弱于 TOTP;等 GitHub 允许只允许 WebAuthn/Passkey 时会再收紧;
- 组织级分支保护:main 禁止 force-push、必须走 PR;禁止创建
advisory-*、internal-*这类分支模式,防止安全工作提前暴露; - tag 保护:release tag 必须等 release 部署成功才能创建,而该部署本身要至少一名其他成员手动批准;tag 创建后不可改、不可删;且 release 部署只能针对 main,防止攻击者拿一个无关的一方分支绕控制;
- 关键一点:仓库 admin 也不能绕过上述保护,所有规则在组织级生效。即使某仓库 admin 账号被攻破,攻击者也关不掉这些控制。
三、自动化:该搬出 Actions 的就搬出
有些事 GitHub Actions 「做了但不安全」,最典型的是给第三方 issue/PR 留言。Astral 的选择是用自研 GitHub App(astral-sh-bot)隔离这类任务:GitHub 把同样的 webhook 事件发给 App,但没有 Actions 那种代码与数据混在一起的隐式状态。
官方同样没把这条路说满:GitHub App 不消除敏感凭证,只是把它放进一个「代码与数据不那么容易混」的环境;App 自己仍可能有 SQL 注入、prompt 注入等弱点,照样会被用来滥用凭证;用了 GitHub App 也不等于可以安全运行不可信代码——真要跑不可信代码,必须用 pull_request 这类不给第三方 PR 特权凭证的触发器。代价是复杂度:要自己开发并托管一个 App,对个人/业余项目是负担。
四、发布安全:让攻击者至少要攻破两个账号
发布是供应链攻击的最后一环。Astral 叠了一组纵深措施(见上图):
| 措施 | 解决的问题 | 官方口径 |
|---|---|---|
| Trusted Publishing(PyPI/crates.io/NPM) | 消除长期 registry 凭证,缓解最常见的包接管来源 | 官方称「where possible」 |
| Sigstore 证明(二进制 + Docker 镜像) | 把制品与产生它的 workflow 密码学绑定,用户可验证 | 脚注 1:因 PyPI Trusted Publishing 与证明身份不兼容,暂未上传到 PyPI |
| GitHub immutable releases | 防止事后替换已发布构建;Trivy 攻击正是 force-push 覆盖旧 tag | 官方明确点名 |
| 发布构建不做缓存 | 防止 GitHub Actions 缓存投毒 | 官方做法 |
| 发布隔离在专用 deployment 环境 | 测试/lint job 碰不到发布 secret | 组织级规则 |
| 环境激活需另一名特权成员批准 | 单个 rogue/被攻破账号无法发恶意 release | 攻击者需同时攻破两个都带强 2FA 的账号 |
| tag ruleset | 防止绕过正常流程直接建 tag 发版 | 与第二节联动 |
对 uv 这类发布 job 很多的仓库,他们还用一个 release-gate 环境 + 一个最小权限 GitHub App 来做二次跳转,既保留双人审批,又避免 GitHub 对每个使用 release 环境的 job 都弹审批。
另两个值得记下的诚实声明:其一是独立安装脚本里内嵌了校验和,但脚注坦白——安装脚本和 release 本身在同一台主机上分发,curl ... | bash 这种用法其实吃不到校验和的保护,真正受益的是把脚本 vendor 进自己 CI 的人;其二是 standalone 安装器的校验和设计。
五、依赖安全:冷却期 + 少加依赖
依赖层的做法相对「传统但扎实」:用 Dependabot 与 Renovate 更新依赖并通告已知漏洞;用 cooldown(冷却期)避开在新发布后立刻升级——官方点出这正是「临时被植入后门的依赖」最容易得手的窗口;对一方依赖可放宽冷却、第三方依赖保留;积极向关键上游贡献代码(包括帮对方加固 CI/CD);克制新增依赖、避免引入二进制 blob、按需关闭依赖里不需要的功能;并通过 OSS Fund 出资反哺生态。
六、客观评价:优势、局限与适用边界
优势:这套做法的价值在于「可复现、可外借」——他们把规则集分享成 gist,并明确说这不是一份最终答案,而是一个时间点上的快照,会随攻击者手法演化而调整。对维护高被依赖开源项目的团队,这几乎是一份可直接对照的 checklist。
局限与官方自认的缺口:
- 哈希钉死后的「可变决定」类风险目前仍靠人工 review,没有自动化兜底;
- GitHub App 路线对单人项目托管成本过高,官方也承认这是「不幸的现实」;
- Sigstore 证明暂未覆盖 PyPI 上传(身份不兼容);
- 整套体系依赖 GitHub 平台能力(deployment environment、tag ruleset、immutable releases),迁移到其他代码托管平台要重做。
对中小团队,不必全盘照搬:最划算的三条是——禁掉 pull_request_target/workflow_run、action 钉 commit、发布走 Trusted Publishing + 双人审批。其余(组织级 ruleset、release-gate、Sigstore)随被依赖规模逐级引入即可。