小李@Free2FA头像
关注

GitHub PAT 用 classic 还是 fine-grained:一份给 Bot 账号和 CI 的凭据清单

2025 年 3 月,有个叫 tj-actions/changed-files 的 GitHub Action 被投毒了。这个 Action 被大约 23000 个仓库使用。恶意代码做的事情不复杂:把 runner 内存里的 CI/CD secrets 打到 workflow 日志里。后续统计有 218 个仓库在事件中泄露了凭据。

调查往下追的时候发现源头不在 tj-actions 本身,而在上游 reviewdog/action-setup。而让这次攻击真正扩大的,是一件事:tj-actions 那个 bot 账号的 PAT 提供了对该 Action 仓库的写权限。

一个可复用的 bot PAT,把一次被攻陷的 workflow 依赖,转换成了对另一个被广泛信任的依赖的写权限。

由此我们来讲讲:GitHub 的个人访问令牌该怎么选、怎么配、什么时候必须换掉。


PAT 是什么性质的东西

个人访问令牌是一个 bearer credential。这个词的意思是:持有即足够。

用它的时候不需要密码,不需要账号的第二重验证,不需要任何交互式确认。拿到那串字符就等于拿到了权限。

在开发者自己的终端里,这已经够敏感了。放到一个 bot 账号上,它能更新共享的 GitHub Actions、移动 tag、给几十个仓库写代码——这时候这枚 token 就已经是软件供应链的一部分了。

所以评估 PAT 的时候,问的不是"它安全吗",而是**“它泄了之后能碰到多少东西”**。


Classic 和 fine-grained 的实际差别

GitHub 现在有两类 PAT,差别比大多数文档说的要大。

Classic PAT 用粗粒度 scope 描述权限。最典型的是 repo——勾上这一项就是当前账号能访问的所有仓库的完整读写,没法按仓库细分。

它的默认行为有两个坑:

  • **不强制过期。**官方文档建议你设过期时间,但系统不要求。只有整整一年完全没被用过,GitHub 才会自动移除它。而任何一个周期性跑的 job 都能让一枚被遗忘的 token 无限期保持活跃。
  • **权限范围跟着人走。**一个带 repo scope 的 classic PAT 继承的是这个账号能触及的所有仓库和组织,约束它的只有那几个粗粒度的 scope 和组织策略。

Fine-grained PAT 能限定到具体资源:

  • 指定 resource owner(个人或单个组织)
  • 指定具体哪几个仓库
  • 指定每类操作的具体权限项(比如只对某个仓库给 Contents: Read and write)
  • 可以被组织管理员审批后才生效——开发者申请访问某个私有仓库,管理员批准才管用

另外有个容易忽略的:组织层面 fine-grained PAT 的默认最长生命周期是 366 天。个人账号下的 fine-grained token 可以设为无过期,组织管理员也可以改这个策略。

换句话说:fine-grained 不等于自动会过期,但它把爆炸半径从一个账号收窄到了几个仓库几个权限项。


什么时候用哪个

我的判断标准比较简单。

用 classic 的场景:个人小项目、一次性脚本、临时迁移。两三个 scope 两分钟搞定,没必要上细粒度。

必须 fine-grained 的场景:公司项目、多人协作仓库、任何接触敏感代码的情况,以及所有的 bot 账号和 CI 场景。

第三条要加粗:任何 token 挂在自动化流程里,就应该用 fine-grained,并且把仓库范围和权限项都收到最窄。

有两个细节很实用:

往 .github/workflows/ 目录推文件时,classic token 需要额外勾选 workflow scope;fine-grained 需要 Workflows: Read and write。很多人配完发现 workflow 文件推不上去,就是卡在这。

**组织层面可以强制修这个事。**Owner 能直接封禁 classic PAT,或者给 classic 和 fine-grained 都设最长生命周期。不符合策略的存量 token 在使用访问组织资源时会被拦住——注意是访问组织资源时被拦,不是被静默吊销,这个区别很重要,因为它意味着那些 token 在其他地方可能还活着。


Bot 账号的 PAT 为什么比泄露密码更糟

对比一枚泄露的密码,一枚 bot PAT 麻烦在五个地方:

**没有交互式 MFA 挑战。**API 调用只需要那串字符,不需要密码和第二重验证。你为账号配的一切 2FA 在这条路径上都不生效。

**自动化会把恶意流量伪装成正常行为。**tag 更新、workflow 运行、仓库写入——这些本来就天天发生。那时候异常流量混在里面并不明显。

**归属人不明确。**这枚 token 可能属于一个共享的 bot、一个已经离职的维护者,或者三年前随手搭了这条流水线的人。出事的时候没人认领。

**爆炸半径跟着人的账号走。**classic PAT 继承账号能触及的所有仓库和组织。

**轮换在工程上有心理障碍。**因为没人能列出全部的使用方,团队会推迟吊销,理由是"万一换坏了发布流程怎么办"。这个"万一"会让 token 一直挂着。


还有一条泄露路径不在 CI 配置里

Unit 42 的研究发现过一个叫 ArtiPACKED 的问题:一些大型开源项目的公开 Actions artifacts 里,能直接翻出可用的 GitHub token 和第三方服务凭据。

常见成因有两个:

  • 上传了整个 checkout 目录,凭据留在 .git 里
  • 上传了 linter 日志,日志里有环境变量

而 artifacts 最长可以保留 90 天可下载。

这条路径的特点是:你的 CI 配置写得再规范也没用,因为泄露不在配置层,在"什么东西被打进了产物"这一层。

建议加一条自查:workflow 里上传 artifacts 的地方,明确只用 actions/upload-artifact 指定具体路径,不要图省事把整个目录传上去。


现在就该做的五件事

第一,把组织里的 classic PAT 列出来。

Settings → Developer settings → Personal access tokens,组织视角能看到组织成员创建的 token 概览。重点找两类:没设过期时间的,以及超过一年没动但最近又活跃的(后者通常意味着有人捡起来用了)。

第二,给 bot 账号换 fine-grained。

把 PAT 换成限定到具体仓库、具体权限项的 fine-grained token。如果场景允许,更进一步的方案是换成 GitHub App——它有独立的身份、细粒度权限、自动过期的安装令牌,不依附于任何人的账号。

Bot 账号最不该出现的东西就是"挂在某个人账号下的长期 token"。

第三,设过期策略。

组织 Owner 在 Settings → Personal access tokens 那里可以封禁 classic PAT、给两类 token 设最长生命周期。设完之后,那些不合策略的存量 token 会被拦。

别一次性开到最严。先设一个宽松的值跑两周,看哪些流程断了,收紧再迭代——这样阻力最小。

第四,检查 workflow 里有没有 scope 缺口。

推 workflow 文件需要 workflow scope 或 Workflows: Read and write。已经配好的 token 如果没这两项,会在某次改 CI 配置的时候突然失败,报错信息通常很不直观。

第五,明确 rotations 的责任人。

每次轮换都得有人负责验证没跑坏的东西。没有责任人,团队就会一直推迟。


说两件容易被混为一谈的事

第一,token 保护和账号登录保护是两个层面。

前面说的所有东西——PAT、GitHub App、SSO——都属于机器凭据。它们和你登录 GitHub 网页时用的第二重验证完全不是一回事。你给账号开了 TOTP,不代表那些 PAT 就安全了。

用户登录那一层的兜底还是那三件事:恢复码单独存一份不要只放手机里、验证器的迁移路径要提前确认、换机之后先真实登录再清理旧设备。

第二,TOTP 验证器不替代 PAT 治理。

这是个需要明确说出来的边界。验证器管的是登录环节的第二重验证,它不管 PAT、SSH Key、OAuth 这类机器凭据。

tj-actions 那次事件里,tj-actions bot 的 2FA 是不是开着并不重要,因为攻击走的根本不是登录那条路——它走的是 runner 内存里那些现成的凭据。把它们当成一件事,就会出现"我明明开了 2FA,怎么 CI 还是被打穿了"这种困惑。

两套东西,分别处理。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/fiya10/article/details/166741288

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--