# 仓库所有权变更：交接前先审计访问权限

仓库所有权变更属于安全事件，即使表面上只是一次普通的整理工作。仓库可能在一个下午就转移到新团队，但周边权限仍可能附着在已经离开的维护者、停用的自动化流程、旧云账户，以及几个月没人查看过的凭据上。

我见过一些团队把仓库所有者改掉，再从协作者页面移除两个人，就宣布交接完成。结果，某个被遗忘的部署任务仍在使用个人令牌发布内容，某个孤立的 SSH 密钥仍能读取私有源代码，或者旧的 Git 托管平台应用仍有权修改拉取请求。仓库页面看起来很干净，真正的控制面却没有。

实际规则很简单：只有在你能说清楚每个可能造成重要影响的身份和服务后，才转移所有权。这里的「重要影响」包括读取非公开代码、推送提交、创建或批准拉取请求、修改构建定义、访问密钥、发布软件包、部署软件，以及改变访问权限本身。

## 仓库转移不等于责任转移

仓库所有者只是一个管理标签。真正的责任意味着，现在有人能够解释仓库连接了什么、每个连接为什么存在、由谁维护，以及发生事故时如何撤销它。

这两件事并不相同。Git 托管平台的权限只描述某项服务内部的访问情况。现代项目还依赖 CI 运行器、软件包仓库、云角色、代码扫描应用、发布机器人、文档发布服务、聊天通知、DNS 服务商和构建产物存储。每个系统都有自己的身份，也都保留着关于这个项目的记录。

项目因为重组、收购、内部平台迁移或人员变动而转移时，这种区别尤其重要。接收团队往往只关注让构建保持绿色，离开团队则只关注移除自己的名字。但这两件事都不能证明控制权已经安全转移。

在任何人修改访问权限前，先写下所有权边界。至少应明确：

- 接受运维责任的技术负责人
- 决定谁可以访问代码和发布内容的业务负责人
- 可以授权紧急撤销权限的事故联系人
- 记录仓库成员和服务身份的权威来源
- 前团队失去权限的日期

不要把群组邮箱或含糊的团队名称作为最终负责人。群组可以收邮件，却无法在凌晨两点作出决定。应明确写出具体人员，并在团队发生变化时更新这份记录。

还有一个让人不舒服的事实：一个没有当前负责人的项目，不应仅仅因为仍能构建，就继续保留广泛的生产权限。如果没有人能为部署凭据负责，就应暂停发布，或先降低权限，直到有人接手。系统可用不能成为保留未知权限的理由。

## 根据实际影响建立清单，而不是只看仓库页面

有用的访问清单，应先从可能发生的操作开始，再反向找出能够执行这些操作的身份。直接从协作者列表开始虽然更快，却会漏掉太多内容。

问问自己：不需要开发者坐在 Git 托管平台网站前，这个项目还能发生什么？机器人可以推送提交。CI 工作流可以获取云令牌。标签出现后，软件包可以自动发布。Webhook 接收器可以触发生产部署。代码审查应用可以发表评论或修改检查状态。每一种影响都对应着一个需要负责人的权限。

可以用五列工作表记录这些信息：能力、身份、凭据位置、当前负责人和撤销流程。撤销流程应单独列出，因为「删除令牌」往往并不是真正的操作。你可能还需要卸载应用、删除云信任规则、删除部署密钥、使软件包仓库令牌失效，或同时修改两端的 Webhook 密钥。

第一轮盘点至少应涵盖以下类别：

- 人员身份，包括直接协作者、组织团队、外部协作者和组织管理员。
- 非人员身份，包括机器用户、服务账户、CI 运行器身份和应用安装。
- 凭据，包括 SSH 部署密钥、个人访问令牌、应用私钥、Webhook 密钥、软件包仓库令牌和云凭据。
- 执行路径，包括工作流文件、可复用工作流引用、运行器组、部署环境、发布脚本和定时任务。
- 数据出口，包括 Webhook、软件包发布、文档发布、备份、镜像、问题跟踪集成和通知服务。

清单应使用通俗语言说明每个凭据的用途。「CI 令牌」还不够。「推送已签名的发布标签后，读取源代码并发布内部命令行软件包」能让下一位维护者知道要测试什么，也知道失败后会带来什么风险。

你会找到一些没人认识的条目。不要因为名称听起来合理就让它们继续存在。要求提供证据：它配置在哪里，最近哪个任务使用过它，它能做什么，转移后由谁负责。如果答案仍然含糊，就安排删除。未知凭据往往要等到事故发生后才会变得「已知」。

## 个人凭据会让交接变得脆弱

任何依赖某位维护者个人令牌的生产或发布路径，都早该更换了。项目转移只是暴露了这个弱点。

个人凭据会带来两种故障。第一种很明显：前维护者离开项目后，可能仍然保留访问权限。第二种更常见：其账户被停用或令牌过期，导致发布路径恰好在接收团队最需要它时中断。团队往往会让前维护者再创建一个令牌。这样虽然修复了眼前的问题，却让这种依赖变得更牢固。

只有在服务确实需要长期身份时，才应将个人凭据替换为服务身份。为它授予完成文档化任务所需的最小权限。发布者可能只需要发布一个软件包的权限，不需要组织级管理权限，也不需要访问所有仓库。

不要把机器用户误认为管理良好的服务身份。机器用户只是一个供自动化使用的账户。它仍可能有未知密码、个人恢复邮箱、广泛的成员资格，且没有负责人。应像管理普通身份一样管理它的生命周期：有计划地创建，记录负责人，审查成员资格，并在任务结束后删除。

这正是「只用一个共享自动化账户」这种流行建议失效的地方。它之所以流行，是因为可以快速让自动化运行起来，也省去了理解每个集成的工作。但它会把互不相关的权限集中到一个身份上。一个项目转移团队后，任何人都无法在不影响其他依赖该账户项目的情况下撤销它的访问。

按运维目的分开身份。构建读取者、发布者和生产部署者通常需要不同的权限，也应由不同的人负责。这样的隔离能降低撤销权限的风险，也让事故调查更清楚。

也要检查 Git 托管平台之外的用户令牌。部署脚本可能从 CI 密钥中读取令牌，但令牌本身可能属于已经离开团队人员的云账户。密钥放在哪里，并不能说明它拥有什么权限。应继续追踪到签发方，并在那里检查权限。

## 部署密钥和应用安装需要分别审查

部署密钥、Git 托管平台应用和 OAuth 集成都能提供仓库访问权限，但它们的失效方式不同。把它们放进同一张列表，会导致撤销工作不够严谨。

部署密钥通常是附加到某个仓库的 SSH 公钥。根据配置不同，它可能只提供读取权限，也可能提供写入权限。它的优势是绑定范围较窄，弱点是身份信息很少：密钥本身很难说明持有私钥的系统是什么。如果备注写着「构建服务器」，而那台服务器已经换过两次负责人，仓库记录也无法帮你确认情况。

对于每个部署密钥，都要确认四件事：私钥存在哪里，哪个进程在使用它，是否需要写入权限，以及负责管理该主机或密钥存储的人员是谁。只拉取源代码的密钥应移除写入权限。任何无法对应到当前系统和具体负责人的密钥，都应删除。

应用安装呈现出相反的问题。它通常有更清晰的身份、权限和事件历史，但可能被安装在许多仓库中。从一个项目中移除它，并不一定能阻止其他地方的相关服务继续运行。检查应用申请的权限、安装范围、私钥轮换流程、回调地址，以及能够修改该安装的组织账户。

GitHub 的文档将部署密钥与 GitHub Apps 分开说明，这是有原因的。部署密钥直接附加到仓库，应用则通过安装获得权限，并使用自己的凭据。不要以为删除部署密钥会影响应用访问，也不要以为卸载应用会使 SSH 密钥失效。它们是彼此无关的权限路径。

OAuth 集成同样需要谨慎对待。它们可能代表用户执行操作，而不是使用专门的应用身份。交接期间，要确认集成的授权是否依赖某位前维护者。如果依赖，就将它迁移到受支持的服务身份，或直接删除。等某个人离开公司后再处理，不能算撤销计划。

## 工作流文件可能授予超出名称所暗示的权限

一个看起来只运行测试的工作流，仍可能获取凭据、调用可复用工作流、写入仓库，或触发部署系统。在判断它的访问是否无害前，先读完文件。

检查所有可执行的仓库配置，不要只看负责发布的工作流。这包括 CI 定义、这些定义调用的脚本、依赖更新配置、部署清单、基础设施代码、软件包发布设置，以及由评论或拉取请求触发的脚本。

重点查看任务跨越信任边界的地方。常见例子包括：工作流使用身份令牌换取云角色，任务在拥有仓库写入权限的情况下运行拉取请求中的代码，或可复用工作流从另一个仓库引入。工作流名称可能写着「代码检查」，真正的情况要看它拥有的权限。

GitHub 的 Actions 文档提醒过，`pull_request_target` 会在基础仓库的上下文中运行，因此可能访问普通拉取请求工作流无法访问的权限。这个事件本身并不一定错误，但如果它与不受信任的拉取请求代码，或外部人员可以影响的脚本结合使用，就会产生问题。交接期间应找出这些工作流，并让接收团队明确接受相关风险。

简单的仓库搜索可以发现许多明显引用。获取需要检查的完整仓库历史后，在本地运行：

```sh
git grep -nE '(AWS_|AZURE_|GCP_|TOKEN|SECRET|DEPLOY|PUBLISH|ssh |curl |webhook)' -- \\
  '.github' '.gitlab-ci.yml' 'scripts' 'infra' 'package.json' 2>/dev/null
```

输出可能如下：

```text
.github/workflows/release.yml:42:  id-token: write
scripts/publish.sh:18: curl -H "Authorization: Bearer $REGISTRY_TOKEN"
infra/deploy.sh:9: ssh -i "$DEPLOY_KEY" "$DEPLOY_HOST"
```

这条命令不能证明某个密钥确实存在，也不能证明某个任务一定危险。它只是为审查提供一个队列。把每个命中项追踪到凭据签发方、权限范围和失败路径。还要搜索指向仓库外部的工作流引用，因为代码可能通过一个不受本仓库控制的可复用工作流继承权限。

不要为了让继承来的工作流通过而授予宽泛的默认权限。修正任务声明的权限，并测试它必须执行的那一个操作。交接是清理那些仅仅因为没人愿意打扰旧流水线而一直存在权限的好时机。

## 按照能保留证据并避免中断的顺序撤销权限

撤销权限需要安排顺序。如果先删除所有内容，可能会丢失确认活跃依赖所需的证据。如果等到文档完美后再行动，旧访问可能会无限期保留。

审查期间冻结非必要变更。接收负责人应能知道在基线完成前，是否出现了新的应用安装、新的部署密钥或新的组织管理员。这不要求停止普通开发，但必须确保变更可见。

导出或记录一份快照，内容包括成员资格、外部协作者列表、部署密钥、应用安装、Webhook、CI 密钥元数据、云信任关系和近期审计事件。不要把密钥值保存在记录中，只记录标识符、权限范围、负责人、可用的创建背景，以及检查时间。

然后按以下顺序执行：

1. 移除前维护者的直接访问权限，并在交接需要时降低前组织管理员的权限。
2. 禁用或卸载未知集成，并撤销没有当前负责人的部署密钥。
3. 用服务身份替换已知的个人凭据，然后测试准确的构建、发布或部署路径。
4. 替换成功后，轮换共享密钥，例如 Webhook 密钥、应用私钥和软件包仓库凭据。
5. 根据项目的发布节奏，审查一段时间内的审计事件和失败任务，然后删除临时例外。

这个顺序把未知权限与已知依赖分开处理。未知部署密钥没有受支持的用途，因此可以尽早删除。已知的发布凭据则需要先找到替代方案，否则安全修正会变成可以避免的中断。

为替换失败准备一条明确的紧急恢复路径。应写明谁可以恢复服务、例外持续多长时间，以及团队如何记录这次操作。不要因为发布延迟就恢复已离职维护者的广泛令牌。应在当前所有权下创建一个临时的、权限范围有限的凭据，记录例外，并在修复后删除它。

## 代理访问也必须遵循同一所有权边界

自主编程代理可以编辑代码、调用 API、使用 SSH、发布构建产物，并通过所连接的工具影响基础设施。忽略代理访问的仓库交接，会留下很大的审查缺口。

不要只问哪些人可以运行代理。还要问哪些代理进程可以代表仓库执行操作、它们能调用哪些工具、这些工具使用哪些凭据，以及事后能否重建某个具体操作。代理从开发者的终端、CI 任务或远程运行器中启动时，即使使用同一个模型，也可能拥有不同的权限。

不要把长期凭据放进代理提示词或工作文件。通过环境变量或工具输出传递密钥，会让它出现在日志、子进程、意外提交和代理自身上下文中。在某个日志查看器里隐藏值，并不能让进程边界变得安全。

对于使用 Sallyport 的团队，Mac 应用可以将 HTTP 和 SSH 凭据保存在加密保险库中，让支持 MCP 的代理通过本地 shim 请求操作，而不是直接获得凭据。它的会话日志和活动日志还能分别记录代理运行和单次调用。需要审查交接时，这比单纯的对话记录更有用。

过渡期间，撤销与旧工作站或旧进程绑定的代理会话，并要求新负责人批准新的运行。然后按凭据检查每次使用：只读源代码拉取和生产部署不应使用同一种批准标准。目的不是让每条命令都变得麻烦，而是让拥有后果责任的人能看到高风险调用。

书面的代理清单应包括仓库范围、执行位置、工具通道、凭据负责人、操作负责人和紧急撤销动作。如果这些字段无法填写，就说明项目无法负责任地转移这项代理访问。

## 审计记录必须回答是谁改变了控制权

所有权变更后，一条只写着「令牌已使用」的日志是不够的。你需要知道令牌属于谁、哪个进程使用了它、它做了什么、目标仓库是什么，以及该操作是否符合新的所有权安排。

将仓库审计事件、CI 执行记录、云审计记录、软件包仓库事件和代理操作记录放在交接文件或事故系统中。它们不需要使用相同格式，这没有问题。重要的是使用共同的时间基准，并保留足够的标识符，以便在不同系统之间追踪同一个事件。

在宣布交接完成前，先测试这些记录。通过每条受支持的路径执行一个无害变更：普通开发者推送、拉取请求自动化事件、定时任务（如果存在）、向非生产目标发布软件包（如果条件允许），以及一个需要批准的代理操作。确认接收团队无需联系前维护者，就能找到相关证据。

防篡改证据很有用，但它不能替代保留期限和访问控制。追加写入日志可以告诉你某人是否修改过记录，却无法解决事件源从未记录调用、日志已经过期，或事故发生时没有当前人员能读取记录的问题。

在迁移完成后设定复查日期。第一次复查可以发现每周或每月才运行一次的自动化，第二次复查则能发现有人因为某项功能停止工作而申请例外。这些例外本身就是诊断材料，每一个都可能暴露出初始清单漏掉的依赖。

## 接收团队应该能够移除每一条访问路径

当接收团队无需旧团队解释某个密钥、寻找某台机器或批准某项变更，就能撤销所有重要访问路径时，交接才算完成。这个标准比仓库转移对话框严格得多，也能避免一个反复出现的问题：代码所有权在纸面上完成了转移，控制权却仍然散落在其他地方。

从最不起眼的资料开始，也就是访问清单。在每个关联服务旁边写上负责人和撤销流程。然后删除那些既没有负责人，也没有撤销流程的条目。这项工作在第一次紧急轮换凭据之前会让人觉得乏味，但到了那时，清单就会决定你是在控制范围内完成修复，还是花一周时间猜测问题所在。
