GitHub 和 GitLab 的令牌权限,表面看是 scope,真正难的是边界

发布于 · 3,645 字 · 约 9 分钟#DevOps#安全
GitHub 和 GitLab 的令牌权限,表面看是 scope,真正难的是边界 封面图
  • 先分身份再谈权限,区分用户、项目机器人或CI临时身份
  • 能用仓库级token就别用账号级,缩小攻击面
  • 能用临时token就别存长期,设置过期和轮换
  • 最小权限原则:只给必要scope,检查泄漏影响范围

大家好,我是若风。

前段时间我帮朋友排一个 CI 问题,场景特别典型:本地 git push 没事,放到流水线里就开始 403;换了另一个 token 之后,代码能推了,但改 .github/workflows 还是失败;再换成第三个 token,终于通了,结果权限给大了,整个仓库都能写。

这种事说实话太常见了。

很多人第一次接触 GitHub 或 GitLab 的 token,直觉都是“这不就是一串密码吗”。但真到实战里你会发现,令牌问题最难的从来不是生成,而是边界搞错了。表面上看是 scope 没选对,真正关键是你根本没分清楚:这个 token 到底代表“我这个人”,还是“这个仓库里的自动化”,还是“只负责部署的机器人”。

这篇文章我按 2026 年 6 月 4 日重新对照了 GitHub 和 GitLab 官方文档,尽量用最人话的方式,把两家的令牌权限讲清楚。你只要记住 3 件事:

  1. 先分身份,再谈权限。
  2. 能用仓库级,就别上账号级。
  3. 能用临时 token,就别存长期 token。

令牌权限最容易错在“身份”和“动作”混在一起

先别急着背名词,我们先把问题抽象一下。

任何一个 token,本质上都在回答两个问题:

  1. 你是谁。
  2. 你能做什么。

比如你想让脚本自动发版,这里面至少有 4 种完全不同的需求:

  • 只读代码。
  • 推代码到仓库。
  • 调平台 API 创建 release。
  • 只给 CI 在任务运行时临时使用。

这 4 件事看起来很像,实际上对应的 token 可能完全不同。很多事故就是这么来的:本来只想“拉代码”,结果顺手给了“全仓库写 + 全 API”;本来只是自动部署,却拿了一个绑定个人账号的长期 PAT,最后人一离职,流水线全挂。

所以后面你看 GitHub 和 GitLab 时,不要先看 token 名字,要先看它的身份边界。

GitHub 已经从“大 scope”走向“细颗粒权限”

GitHub 现在最常见的 3 类令牌,可以粗暴理解成三层。

类型身份边界典型用途风险特点
Personal access token (classic)绑定用户账号,影响你能访问的所有仓库范围老脚本、兼容场景、某些 fine-grained 还不支持的能力权限最宽,最容易“顺手给大”
Fine-grained personal access token绑定单个用户或组织,还能限制到指定仓库CLI、脚本、单仓库自动化更安全,但不是所有场景都支持
GITHUB_TOKEN只属于当前 workflow 所在仓库,且生命周期跟 job 走GitHub Actions 内部自动化默认更安全,但边界严格

Fine-grained PAT 才是 GitHub 现在更推荐的默认选项

GitHub 官方文档里把 fine-grained PAT 讲得很明确:它可以限制到单个资源所有者,还能继续收缩到指定仓库,再往下才是具体权限,比如 Contents、Issues、Pull requests、Workflows。

这套模型的好处很直接。

如果你只是想让一个本地脚本更新某 1 个仓库的 release note,那它没必要碰你账号下另外 20 个仓库,更没必要有组织级管理权限。fine-grained PAT 就是为这个场景生的。

但 GitHub 也明确写了一个现实问题:fine-grained PAT 还不能覆盖 classic PAT 的所有能力。比如某些历史接口、某些跨组织/协作者场景,classic PAT 还在兜底。所以你会看到不少老工具 README 还在教你配 classic PAT,这不是他们懒,很多时候是兼容性决定的。

我的建议很简单:

  • 新脚本、新项目,先试 fine-grained PAT。
  • 遇到能力缺口,再退回 classic PAT。
  • 一旦退回 classic PAT,就默认把它当高风险凭证管理。

Classic PAT 真正危险的地方,不是名字老,而是边界太大

GitHub 官方对 classic PAT 的提醒其实挺直白:这类 token 很容易访问到你本来就有权限的所有仓库资源。也就是说,它不是按“这个脚本需要什么”来授权,而是按“你这个人本来能碰什么”来授权。

这就是为什么很多团队明明只想做一个小自动化,最后却给了 repo 这种大 scope。短期很爽,长期很揪心。

尤其是下面两个场景,特别容易踩坑:

  1. 你只是想 push 代码,却给了完整 repo。
  2. 你要改 workflow 文件,却忘了额外的 workflow 相关权限。

如果你推的是普通代码,权限模型相对简单;但只要涉及 .github/workflows/*.yml,要求就会明显更高。GitHub 在 classic PAT 体系下有单独的 workflow scope;在 fine-grained PAT 体系下,对应的是仓库级 Workflows 权限。很多“为什么我明明能 push,唯独工作流文件不行”的问题,根因就在这里。

GITHUB_TOKEN 的本质不是“迷你 PAT”,而是仓库内置临时身份

这个点很多人第一遍都会理解偏。

GITHUB_TOKEN 不是你自己生成的 PAT,它本质上是 GitHub Actions 在 job 启动时给当前仓库发的一张临时票。官方文档写得很清楚:它的权限被限制在包含该 workflow 的仓库里,而且生命周期跟 job 绑定。

这就意味着两件事:

第一,它天然适合“仓库内自动化”。比如:

  • 创建 issue
  • 创建 release
  • 评论 PR
  • 读取代码、构建、上传 artifact

第二,它天然不适合被你拿去冒充“长期通用钥匙”。

另外还有一个很关键的细节:GitHub Actions 里的 permissions 一旦你显式写了某些权限,没写到的其他权限会被置成 none。这一点非常好,也非常容易让人翻车。

比如这段配置:

permissions:
  contents: read
  issues: write

它的含义不是“额外加两个权限”,而是“只保留这两个,其余默认都不要”。如果你后面某一步还想发 deployment、改 PR、写 packages,就要自己再补。

所以在 GitHub 里,我的经验是:

  • 在 Actions 里能用 GITHUB_TOKEN 就先用它。
  • 只给 job 需要的最小 permissions。
  • 只有当 GITHUB_TOKEN 做不到时,才考虑 PAT 或 GitHub App。

GitLab 的 token 更多,但边界反而更好理解

GitLab 这套设计,第一眼看会觉得选项很多:PAT、project access token、group access token、deploy token、CI_JOB_TOKEN……但说白了,它其实比 GitHub 更强调“谁来干活”。

先看一张总表:

类型身份边界能不能调 API适合什么
Personal access token用户账号能个人脚本、CLI、临时运维
Project access token单个项目能项目级机器人、单仓库自动化
Group access token单个组能多项目共享自动化
Deploy token项目或组的部署身份不能直接调 GitLab 公共 API拉代码、拉推镜像、包仓库
CI_JOB_TOKEN当前 CI job只能访问受限 API流水线内短时操作

GitLab PAT 最像“真人分身”,所以也最该慎用

GitLab 的 personal access token 很像 GitHub classic PAT,都是直接挂在用户身上。官方文档也提醒得很明确:它继承创建者本人的权限,只是再叠加 scope 限制。

这里最容易被忽略的一点是,GitLab 的 api scope 很大。

如果你只是想读 API,就优先看 read_api;如果你只是想通过 HTTP 拉私有仓库代码,就看 read_repository;如果你只是想通过 Git push,就看 write_repository。GitLab 文档甚至单独写了,write_repository 只管 Git over HTTP 的读写,并不等于你能拿它调用所有 API。

这个边界反而是好事。因为它逼着你别偷懒。

Project access token 是 GitLab 里最好用的“项目机器人”

如果你只服务一个项目,我倒是非常推荐优先考虑 project access token。

GitLab 官方文档里明确说了,它被限制在单个项目范围内,而且创建时会生成项目级 bot user。这类 bot user 不占 license seat,对团队自动化来说非常友好。

它适合什么?

  • 某个项目自己的发布脚本
  • 某个项目自己的镜像构建
  • 单项目的 API 调用
  • 只想让机器人碰 1 个项目,不想碰整个组

说白了,project access token 的核心价值不是“它更方便”,而是“它出事时波及面更小”。

Group access token 解决的是多项目协同,不是“更高级的 PAT”

很多团队一上来就喜欢配 group access token,觉得以后省事。这个思路我能理解,但风险也确实更大。

因为它的边界已经不是一个项目,而是一整个 group。你只要想到这一点,就知道它应该用在什么地方了:共享模板仓库、多项目统一发版、跨项目机器人账号。这些都合理。

但如果你的自动化只服务 1 个项目,那上来就给 group access token,其实就是在扩大攻击面。

能不用,就别用。

Deploy token 的价值是“会干活,但不碰管理面”

GitLab deploy token 这个设计我一直挺喜欢,因为边界特别清楚。

它能做的,基本都是“交付动作”:

  • 克隆仓库
  • 拉取或推送容器镜像
  • 读写 package registry

它不能直接用来调 GitLab 公共 API。这一点官方文档写得非常明确。

这意味着 deploy token 特别适合部署机、运行时环境、镜像拉取脚本。你把它放在服务器上,心里会踏实很多,因为哪怕它泄漏了,攻击者也不太容易顺着它去改 issue、改 MR、改成员权限。

如果你只是为了让生产机 docker pull 一下镜像,结果塞了个 PAT 进去,说实话,这就有点拿大炮打蚊子了。

CI_JOB_TOKEN 才是流水线里的第一选择

GitLab 官方在 token overview 里有一句建议,我觉得应该贴墙上:做 CI/CD 时,尽量别用个人 PAT,当 job token 或 project access token 能满足时,优先选它们。

原因很简单,CI_JOB_TOKEN 是临时的,而且跟 job 生命周期绑定。任务结束,票也就过期了。

它当然有限制。比如它不是全能 API token,也不能拿去随便调 GraphQL;跨项目访问时还经常要配 allowlist。但这些限制本身就是安全边界的一部分。

如果你的需求是在流水线里拉另一个私有项目、触发部分 API、做包下载,这种时候先试 CI_JOB_TOKEN,往往是最稳的。

像下面这种用法,就很典型:

git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.example.com/group/project.git

这串命令看起来普通,背后其实是一个很重要的安全策略:让临时身份做临时的事。

两个平台最容易踩的 6 个坑

1. 能 push 代码,不代表能改工作流

GitHub 里这事太常见了。普通代码 push 成功,不代表你有修改 Actions workflow 的权限。涉及 .github/workflows 时,要单独确认 classic PAT 的 workflow scope,或者 fine-grained PAT 的 Workflows 权限。

2. 能拉仓库,不代表能调 API

GitLab 的 write_repository 就是典型例子。它支持 Git over HTTP 的 push/pull,但不等于完整 API 权限。很多人配完以后去打 REST API,看到 401 或 403 就开始怀疑人生。

3. 部署场景错用用户级 PAT

这是我最不推荐的坏习惯之一。部署机、运行时环境、镜像拉取机,优先应该拿 deploy token、project token 或临时 job token,而不是某个同事的个人 PAT。

4. 把“跨项目方便”当成“给 group 级权限的理由”

只要你上了 group access token,风险边界就变大了。除非你的自动化天然服务多个项目,否则项目级 token 往往更合适。

5. scope 够了,但资源边界不对

这个坑在 GitHub fine-grained PAT 上特别典型。你可能权限勾得没问题,但 token 只绑定了某个组织、某几个仓库,目标仓库压根不在授权范围里,那照样不行。

6. token 不过期,最后一定出事

GitLab 现在新建很多 access token 默认就要求过期时间;GitHub 这几年也一直在强化过期和审批策略。平台都在往“短期、可审计、可撤销”走,你如果还习惯“先建一个永久 token 再说”,基本就是在给未来埋雷。

真正有用的不是记住名字,而是学会一套选型顺序

如果你懒得背一堆文档,我给你一套特别实用的判断顺序。

场景一:在平台自带 CI 里跑自动化

  • GitHub:先用 GITHUB_TOKEN
  • GitLab:先用 CI_JOB_TOKEN

这是默认答案。

场景二:只服务单个仓库或项目的机器人

  • GitHub:先用 fine-grained PAT,限制到指定仓库
  • GitLab:先用 project access token

场景三:只做部署、拉包、拉镜像

  • GitHub:优先看平台原生部署凭证或 Actions 内部令牌
  • GitLab:优先 deploy token

场景四:必须跨多个项目协作

  • GitHub:优先评估 GitHub App,其次再看 PAT
  • GitLab:必要时再上 group access token

场景五:只是你自己本地命令行偶尔用一下

  • GitHub:fine-grained PAT 优先,classic PAT 兜底
  • GitLab:PAT 够用,但 scope 尽量收窄

最后给你一个最小权限检查清单

每次创建 token 前,我都会快速过一遍下面 7 条:

  1. 这个 token 代表的是人、项目机器人,还是 CI 临时身份?
  2. 它只服务 1 个仓库,还是一组仓库?
  3. 它到底需要 Git 权限、API 权限,还是 registry/package 权限?
  4. 如果只读就够,为什么要给写?
  5. 如果 job 内临时可解决,为什么要存长期 PAT?
  6. 这个 token 泄漏后,最坏会影响几个仓库、几个项目、几个环境?
  7. 我有没有设置过期时间、轮换机制和撤销预案?

这 7 个问题里,前 3 个决定“该选谁”,后 4 个决定“别出事”。

写在最后

很多权限问题,最后看起来像平台坑,像文档难懂,像 403 很玄学。但你真的把 GitHub 和 GitLab 的 token 体系拆开看,会发现它们其实都在往同一个方向走:少给一点,短活短票,按边界发身份。

表面上看,我们是在学习 repo、workflow、read_api、write_repository 这些术语;真正该学会的,是别再把“方便”误当成“合理”。

如果你们团队现在还在用一两个超大权限的长期 token 扛所有自动化,我倒是建议抽 30 分钟做一次盘点。很多时候,权限收一轮,风险能直接降一个数量级。

参考文档

评论互动

© 2026 王若风的技术博客 · Powered by Astro