机器人身份、私有源与权限:把依赖更新关进最小授权边界
更新成功了,为什么安全团队仍然要求停掉机器人
一个依赖更新机器人刚把私有 npm 包从旧版本升级到修复版,锁文件也生成了,Pull Request 看起来没有异常。安全团队复盘时却发现:运行它的是某位工程师的长期个人令牌;令牌可以读取整个组织、推送所有仓库,还能修改工作流;同一份 Registry Token 被放进普通 Actions Secret,日志中又打印过带用户名的下载地址。机器人做对了一次更新,却建立了一条比漏洞本身更宽的横向移动路径。
这里容易混淆两种完全不同的通行证。第一种回答“谁可以读取仓库、创建分支和 Pull Request”,属于代码托管平台身份;第二种回答“谁可以查询私有版本并下载包来重建锁文件”,属于制品源身份。仓库写权限不能自动推出私有源读取权限,能够查询版本元数据也不代表包管理器能下载 tarball。把两者塞进一个万能 Token,故障少了一步,爆炸半径却扩到了所有仓库和所有包。
正确的运行链是:调度器取得短期平台令牌,只进入明确安装的仓库;更新器读取 manifest,使用独立的只读 Registry 凭据完成版本查询和 artifact 更新;Git 平台再用平台身份创建分支与 PR。任何一步失败都应停在原地并留下不含秘密的证据,而不是降级成匿名查询、复用开发者凭据或跳过锁文件。
运行方式会改变谁承担这条边界。Dependabot Version Updates 是 GitHub 托管功能,更新作业可以按平台能力落到 GitHub 托管或自托管 Actions runner,但它不是一套把 dependabot.yml 搬出 GitHub 就能独立运行的通用 bot;开源的 dependabot-core 也不等于 GitHub 托管服务的权限与支持承诺。Renovate CLI 采用 MIT 许可,可使用 Mend 托管 App 或自托管;自托管要自行维护令牌签发、包管理器、网络、缓存和版本升级,而且 Renovate 官方只支持最新发行版。Renovate 43.x 与当前 GitHub App、Dependabot 私库配置构成后续操作基线;GitHub Enterprise Server 或 GitLab 的实例发行线决定实际可用的 API 和保护规则。
先把两个身份和四个状态拆开
平台身份至少经历 installed -> token_issued -> branch_written -> pr_opened。GitHub App 的私钥只用于签发 JWT,JWT 再换取短期 installation token;真正执行仓库 API 和 Git 操作的是 installation token。安装范围决定它最多能看到哪些仓库,App permission 决定它在这些仓库能做什么。GitHub 明确要求 HTTP Git 访问使用 Contents 权限,修改 .github/workflows 还需要单独的 Workflows 权限,见 Choosing permissions for a GitHub App。因此“Contents 可写”与“工作流可写”应视为两道权限门,不要为了偶尔更新 Action 就给所有更新任务永久开放工作流修改。
制品身份至少经历 credential_loaded -> metadata_authorized -> artifact_downloaded -> lockfile_generated。Renovate 在版本查询阶段自己发 HTTP 请求,而重建锁文件时会运行 npm、Maven、Bundler 等包管理器;后者不认识 Renovate 的 hostRules 对象,Renovate 必须把凭据转换成包管理器理解的环境变量或临时配置。官方的 Private package support专门区分了私有 preset、版本查询、changelog 查询和 artifact 更新四个用凭据的位置。这解释了一个常见现场:日志里已经找到新版本,npm install 却在下载 tarball 时返回 401。
把四个状态分开记录后,失败才可定位:
repository_auth=denied 表示平台安装或仓库权限错误,更新器甚至不该触碰 Registry。metadata_auth=passed, artifact_auth=denied 表示凭据只够查索引,不能重建锁文件。artifact_auth=passed, branch_write=denied 表示依赖更新已在临时目录完成,但平台身份无权推分支。
pr_opened=true 只证明 PR 已创建,不证明它有权合并,更不证明私库凭据没有泄漏。
这些状态应保存错误类别、HTTP 状态、主机别名、仓库匿名标识和 correlation ID,不保存 Authorization header、完整下载 URL、私有包名或内部仓库路径。
选择 App 或 Bot,而不是借用人的身份
在 GitHub 上,托管 Renovate 通常以 Mend Renovate App 安装;自托管既可以使用专用 bot 账号,也可以运行成 GitHub App。Renovate 的 GitHub platform 文档列出了当前所需的平台权限,并提醒 GitHub App installation token 需要定期重新签发。实际授权不能机械复制完整权限表,而要从仓库中的更新面反推:
普通依赖 PR 需要读取代码、创建分支并管理 PR。只有机器人确实更新 workflow 引用时,才需要工作流写权限;更稳妥的做法是拆成单独 App 或单独执行池,让普通包更新身份永远没有这项权限。只有工具确实读取 Dependabot alerts、创建 dashboard issue、写 commit status 时,才启用相应权限。
App 只安装到选定仓库;组织发现功能与实际写入范围分离,不能把 autodiscover 当授权系统。
GitHub App 比个人 PAT 更适合长期自动化,因为安装范围可见、权限可审核、installation token 短期有效,并且人员离职不会使任务突然失去身份。PAT 适合隔离实验或迁移期,但必须来自专用机器账号,限定资源 owner、仓库与有效期;不能使用维护者自己的 Token。GitHub 的 App 权限和各 API 端点映射会变化,实施时应从 GitHub App permission reference反查实际调用,而不是照抄一张长期不变的权限清单。
GitHub App installation token 固定在安装边界内,并在一小时后失效;签发时还可以用 repositories / repository_ids 和 permissions 再向下收窄,但不能扩大 App 原有授权。若省略这两个字段,令牌会继承该 installation 当前可访问的全部仓库和全部已授予权限。因此“App 只装到 selected repositories”和“每个作业只签发本次仓库、只含本次权限的 token”是两层约束,不能只做前一层。
旧配置可能把 installation token 写成 x-access-token:<token>。Renovate 当前接受普通 ghs_ token,并已说明将移除前缀形式的兼容;迁移时先让 canary 使用无前缀 token 完成读仓库、建分支和开 PR,再切换生产,不能等升级后才发现认证失败。RENOVATE_X_GITHUB_HOST_RULES 自动为 GitHub Packages 派生凭据的能力也已被退回实验状态,不能把它当作生产私库认证基线;应继续使用显式、只读且可审计的 hostRules 或 Registry 联合身份。这两项状态都记录在 Renovate GitHub platform 文档中。
在 GitLab 上,自托管 Renovate 可以使用专用 Bot。项目访问令牌会创建与项目关联的 bot user,其可用能力由角色与 scope 共同决定,见 Project access tokens。更新器只处理单项目时,优先项目级身份;确需跨项目读取共享 preset 或内部模块时,再把读取能力授给明确项目,而不是把一个组级高权限 Token 注入全部作业。Renovate 官方也说明共享 GitLab bot 会带来跨项目凭据风险,应按执行环境隔离并限制仓库发现,见 GitLab bot security。
无论平台,合并权都不属于更新机器人。机器人可以提议变更,受保护分支、Required Checks、owner review 和合并队列决定是否接受。给机器人管理员或绕过规则权限,只会把仓库控制面变成一条无法证明的旁路。
GitHub App 的最小安装与短期令牌
创建 App 时先关闭不需要的 webhook 和用户授权流程,只保留更新器实际调用的 repository permissions。下面是普通依赖更新的起点,不是所有仓库的固定答案:
Metadata: read
Contents: read and write
Pull requests: read and write
Issues: read and write # 仅在使用 Dependency Dashboard 或标签时
Commit statuses: read and write # Renovate 当前 GitHub App 模式会写状态
Checks: read and write # Renovate 当前 GitHub App 模式会写检查
Workflows: no access # 普通包更新保持关闭
Administration: read # 仅在工具确实读取保护规则时这是按功能裁剪后的起点,不是 Renovate 官方完整权限表的替代品。当前自托管 GitHub App 文档还列出 Workflows: write、Dependabot alerts: read、Members: read 等权限;普通包更新执行池不应因此全开。需要更新 Actions 引用时拆到单独 App 和执行池,并把 workflow diff 强制路由给平台 owner;确需 dashboard、alert 或成员查询时,再根据实际 API 调用增加对应权限。App 安装时选择测试仓库,而非全部仓库。运行器保存 APP_ID、INSTALLATION_ID 和私钥引用,私钥本体进入密钥系统;作业开始时换取 installation token,作业结束即销毁内存与临时文件。
在 GitHub Actions 中可以使用 GitHub 官方维护的 token action 生成安装令牌。生产 workflow 应把 action 固定到审核过的完整 commit SHA;下例中的占位符必须替换成当前已审核 SHA:
permissions:
contents: read
steps:
- name: Mint installation token
id: app-token
uses: actions/create-github-app-token@<reviewed-full-commit-sha>
with:
app-id: ${{ vars.RENOVATE_APP_ID }}
private-key: ${{ secrets.RENOVATE_APP_PRIVATE_KEY }}
owner: your-org
repositories: dependency-lab
- name: Run Renovate against the selected repository
env:
RENOVATE_TOKEN: ${{ steps.app-token.outputs.token }}
RENOVATE_REPOSITORIES: your-org/dependency-lab
run: npx --yes renovate --platform=github作业自己的 GITHUB_TOKEN 只有读取 workflow 所需的最低权限,真正的 App token 又被 owner 与 repositories 约束。不要把 installation token 写入 job output、artifact 或 debug dump;也不要在同一 job 中运行未经审核的仓库脚本,因为同进程环境里的脚本可以读取环境变量。
actions/create-github-app-token 默认会在 job 结束时撤销生成的 token;无论使用 action 还是自行调用 API,都要把显式撤销作为提前失败和人工取消路径的一部分。只等待一小时自然过期会把被取消任务的有效窗口留给同一 runner 上的残留进程。撤销后再次请求目标仓库应得到认证失败,随后清空 Git credential helper、进程环境、工作目录和包管理器临时配置。
私有 Registry 要过两道认证
以自托管 Renovate 访问私有 npm Registry 为例。管理员在运行器 Secret 中保存只读 Token,仓库配置只引用主机和策略:
// config.js 位于受控运行器配置,不提交真实 token。
module.exports = {
platform: 'github',
repositories: ['your-org/dependency-lab'],
hostRules: [
{
hostType: 'npm',
matchHost: 'https://registry.example.invalid/npm/',
token: process.env.RENOVATE_NPM_TOKEN,
},
],
};matchHost 使用带协议和末尾路径的基地址,可以把凭据收窄到 Registry 的命名空间;规则过宽会把认证头发送到同主机的其他服务。Token 只授予目标包命名空间的 read/download,不授予 publish、delete、admin。若代理会重定向请求,还要确认认证头不会被带到不同主机。
第一阶段验证元数据,第二阶段验证真正的 artifact 和锁文件。下面的命令应在隔离测试项目执行,环境变量由临时凭据注入;命令关闭脚本执行,避免包的生命周期脚本读取凭据:
export NPM_CONFIG_USERCONFIG="$(mktemp)"
trap 'rm -f "$NPM_CONFIG_USERCONFIG"' EXIT
printf '%s\n' \
'@example:registry=https://registry.example.invalid/npm/' \
'//registry.example.invalid/npm/:_authToken=${NPM_READ_TOKEN}' \
'always-auth=true' > "$NPM_CONFIG_USERCONFIG"
# 第一阶段:索引授权。成功时输出一个版本字符串。
npm view @example/dependency-lab version \
--registry=https://registry.example.invalid/npm/
# 第二阶段:下载并解析 artifact。成功时 package-lock.json 有可审查 diff。
npm install @example/dependency-lab@latest \
--package-lock-only --ignore-scripts \
--registry=https://registry.example.invalid/npm/
git diff -- package.json package-lock.jsonnpm view 成功而 npm install --package-lock-only 返回 E401 或 E403,通常意味着索引与 tarball 使用不同 URL、Token 缺少下载权限、代理重定向丢失认证,或包管理器没有收到 Renovate 转换后的凭据。修复后必须重跑第二阶段,并确认 lockfile 中没有 Token、用户名或带认证查询参数的 URL。
Dependabot 使用另一套安全域。顶层 registries 声明连接,具体 updates 块决定哪些更新作业可以使用它;凭据进入 Dependabot Secret,而不是把明文写进 .github/dependabot.yml:
version: 2
registries:
private-npm:
type: npm-registry
url: https://registry.example.invalid/npm/
token: ${{ secrets.NPM_READ_TOKEN }}
updates:
- package-ecosystem: npm
directory: /
registries:
- private-npm
schedule:
interval: weekly组织级 Dependabot Secret 必须使用 selected repositories,而不是默认扩散给全部仓库。GitHub 说明 Dependabot 事件触发的 workflow 只能读取 Dependabot secrets,不能读取普通 Actions secrets;各种 Secret 的作用域见 Understanding GitHub secret types。一旦配置私有 Registry,Dependabot 会默认限制外部代码执行;只有包管理器确实无法更新且风险评审通过时,才按更新块启用 insecure-external-code-execution: allow。该开关及其凭据可见边界应以 Dependabot private registry guidance为准,不能把“名字里有 insecure”当作可以忽略的提示。
第三方 Registry 支持联合身份时,优先让平台用 OIDC 换短期下载凭据,而不是保存长期 Token。GitHub 的组织级私库配置已支持 Token、用户名密码和 OIDC,具体 Registry 类型支持的字段不同;例如 JFrog 使用 jfrog-oidc-provider-name,可附加 audience 与 identity mapping。信任策略至少绑定组织、仓库或安全功能身份、目标 Registry 与只读动作,不能只校验 issuer。OIDC 只消除静态 Secret,不会自动缩小权限;subject、audience 或映射规则过宽,拿到的短期凭据仍可能横向读取整个制品库。
普通 GitHub Actions 作业访问云制品服务时同样可以使用 OIDC:只在负责换证的 job 授予 id-token: write,同时把 contents 保持只读,并在云端校验仓库、分支或 environment claim。id-token: write 只允许请求 OIDC JWT,不直接授予仓库或云资源写权限;真正的权限来自云端信任与角色策略。执行 fork PR 或未审核依赖脚本的 job 不授予 id-token、也不调用可换证的 reusable workflow。
Fork 和依赖脚本必须停在无凭据执行池
来自 fork 的 pull_request 代码、机器人改写的 manifest、包管理器插件和 install lifecycle script 都是不可信代码。GitHub 对 fork PR 默认不给仓库 Actions secrets,并把 GITHUB_TOKEN 收紧为只读;Dependabot 触发的多类事件同样使用只读 token,且只能读取 Dependabot secrets,不能读取普通 Actions secrets。这些默认限制是底线,不是授权设计的全部。
测试 job 只 checkout PR head,权限显式设为 contents: read,不授予 id-token、packages、deployments 或写权限,并优先使用临时 GitHub 托管 runner 或每任务销毁的隔离 runner。它用 npm ci --ignore-scripts、包管理器等价开关或容器沙箱完成不需要脚本的验证。确实必须执行依赖脚本时,使用无 Registry 凭据、无生产网络、无共享 cache 写权的单独 job;需要私包的下载阶段先在可信 job 取得经过校验的 artifact,再把不含凭据的只读 artifact 交给无凭据 job,不能把 .npmrc 一起传递。
pull_request_target、workflow_run 和 base-branch workflow 可能拥有 Secret 或写权限,绝不能 checkout fork/机器人 head 后执行其中代码,也不能直接信任低权限 job 上传的脚本、路径或可执行 artifact。特权 job 只读取 GitHub API 返回的 PR 编号、固定 schema 的检查结论与当前 head SHA;任何标题、分支名、label 或正文进入 shell 前先作为环境变量传入并做白名单校验,避免表达式注入。
在隔离 fork 做一次负向实验:PR head 增加脚本,检查 ACTIONS_ID_TOKEN_REQUEST_URL、NPM_READ_TOKEN 和 RENOVATE_TOKEN 是否存在,并尝试向仓库创建 tag。预期三个变量都为空,tag 写入返回权限拒绝,日志只打印 PASS credential unavailable 和脱敏后的拒绝类别。若任一变量存在或写入成功,立即取消 workflow,撤销已暴露凭据,删除 fork runner 与共享 cache,随后检查 job-level permissions、environment、reusable workflow 权限传递和 self-hosted runner 分组。修复后从新 fork、新 runner、新凭据 generation 重跑;旧 runner 上的第二次成功不能证明残留已清除。
实验完成后关闭测试 PR,删除探针分支、tag、artifact 与 cache,撤销临时角色会话,并检查审计日志没有探针结束后的 Registry 下载。不要把 fork 加入“可信仓库”来让实验变绿;正向路径应由受控更新分支证明能下载指定私包,反向路径则持续证明 fork 和脚本拿不到凭据。
在隔离仓库执行允许与越权实验
准备两个无生产数据的仓库:App 安装到 your-org/dependency-lab,不要安装到 your-org/identity-denied-lab。本机需要 gh、git、curl 和可签发 installation token 的测试 App。通过静默提示把短期令牌注入 GH_TOKEN,避免令牌字面量进入 shell history:
read -rsp 'Short-lived installation token: ' GH_TOKEN
printf '\n'
export GH_TOKEN
# 正向:列出安装可见仓库,应包含 dependency-lab。
gh api /installation/repositories \
--jq '.repositories[].full_name'
# 正向:读取目标仓库默认分支,应返回分支名。
gh api repos/your-org/dependency-lab \
--jq '.default_branch'
# 反向:未安装仓库必须拒绝。保存状态码,不打印响应正文。
code="$(curl -sS -o /dev/null -w '%{http_code}' \
-H "Authorization: Bearer $GH_TOKEN" \
-H 'Accept: application/vnd.github+json' \
https://api.github.com/repos/your-org/identity-denied-lab)"
test "$code" = 404 && echo 'PASS denied repository is hidden' || {
echo "FAIL unexpected status=$code"; exit 1;
}正向输出应该只出现 App installation 可见的仓库。越权请求预期为 404,这样调用方不能利用 403 枚举私有仓库;若返回 200,先撤销当前令牌,随后把 App 安装从 all repositories 改为 selected repositories,再签发新 token 重试。旧 token 不应被当作修改授权后的验证凭据。
还要验证“能建 PR,不能直推默认分支”。下面先读取真实默认分支与基线 SHA,再在临时克隆中创建无害分支;不要把这组命令指向含生产数据或发布钩子的仓库:
gh auth setup-git
WORKDIR=''
PROBE_BRANCH=''
PR_URL=''
cleanup_identity_probe() {
if [ -n "$PR_URL" ]; then
gh pr close "$PR_URL" --delete-branch || true
elif [ -n "$PROBE_BRANCH" ] && [ -d "$WORKDIR/dependency-lab/.git" ]; then
git -C "$WORKDIR/dependency-lab" push origin --delete "$PROBE_BRANCH" || true
fi
cd /
if [ -n "$WORKDIR" ] && [ -d "$WORKDIR" ]; then
rm -rf -- "$WORKDIR"
fi
unset GH_TOKEN
}
trap cleanup_identity_probe EXIT
DEFAULT_BRANCH="$(gh api repos/your-org/dependency-lab --jq '.default_branch')"
BASE_SHA="$(gh api "repos/your-org/dependency-lab/commits/$DEFAULT_BRANCH" --jq '.sha')"
WORKDIR="$(mktemp -d)"
gh repo clone your-org/dependency-lab "$WORKDIR/dependency-lab"
cd "$WORKDIR/dependency-lab"
PROBE_BRANCH="renovate/identity-probe-$(date +%s)"
git switch -c "$PROBE_BRANCH"
printf '\nidentity-probe\n' >> .bot-identity-probe
git add .bot-identity-probe
git -c user.name='dependency-update[bot]' \
-c user.email='dependency-update[bot]@users.noreply.github.com' \
commit -m 'test: verify bot branch permission'
git push origin HEAD
PR_URL="$(gh pr create --title 'test: bot identity probe' \
--body 'Disposable permission probe.' --base "$DEFAULT_BRANCH")"
# 反向:受保护默认分支必须拒绝直接推送。
probe_failed=0
if git push origin "HEAD:$DEFAULT_BRANCH"; then
echo 'FAIL bot bypassed protected branch'
probe_failed=1
# 测试仓库发生了意外写入,立即追加补偿提交恢复文件内容。
git revert --no-edit HEAD
git push origin "HEAD:$DEFAULT_BRANCH"
else
echo 'PASS protected branch rejected direct push'
fi
# 最后一条断言决定实验结果;EXIT trap 负责关闭 PR、删除分支与临时目录并清除令牌。
test "$probe_failed" -eq 0预期是分支和 PR 创建成功,直推显示 protected branch 或 repository rule violation,最后一条 test 返回成功。若直推意外成功,脚本会先追加补偿提交恢复探针文件,再以非零状态结束;BASE_SHA 用于审计核对,不应用强制推送把默认分支重置回旧 SHA。随后移除 App 的 bypass、admin 或直接推送授权,启用要求 PR 的规则,并在新分支重复实验。即使中途命令失败,也要按清理段关闭 PR、删除探针分支与临时目录并 unset GH_TOKEN。
GitLab 的等价探针使用项目访问令牌访问已授权项目,再请求一个未授权项目。API 成功返回目标项目元数据,越权项目应不可见;随后机器人只向普通分支 push 并创建 Merge Request。默认分支要显式保护,Allowed to push and merge 设为 No one 才真正禁止直推,GitLab 对这两个字段的区别见 Protected branches。不要把空白设置误读成拒绝。
日志脱敏不能只替换 Token 字符串
最危险的日志往往没有完整 Token,却仍足以泄密:带用户名的 URL、私有包名、项目路径、HTTP 重定向位置、npm 配置文件内容、Git remote、环境变量列表和 curl verbose header。运行器应在日志进入集中平台前做结构化过滤:
authorization, proxy-authorization, cookie, set-cookie -> [REDACTED]
token, password, privateKey, npmToken -> [REDACTED]
https://user:secret@host/path -> https://host/path
private package/repository path -> stable_hash + class
query keys access_token, signature, X-Amz-* -> [REDACTED]默认使用 info 日志,只在受控重放中短时打开 debug;采样前先脱敏,不能先把原始事件发到外部观测平台再处理。错误指标使用 registry_alias、credential_generation、error_class 和 stage,避免以真实仓库名、包名、人员或完整 URL 作为高基数标签。日志保留期由审计与数据最小化共同决定,不跟随工具默认值。
一旦怀疑泄漏,顺序是:撤销凭据,停止所有仍可读取旧凭据的任务,确认审计中的最后使用时间与来源,删除或隔离含秘密日志,再签发新一代凭据并运行两阶段验证。先轮换后清日志会让旧 Token 在窗口内继续可用;先删日志又会破坏判断爆炸半径的证据。
轮换时让新旧凭据有可证明的交接
轮换不要直接覆盖同名 Secret 后祈祷。维护 credential_generation,以 old -> dual-valid -> new-verified -> old-revoked 推进:
创建权限不扩大的新凭据,记录 owner、用途、作用域和到期策略。把新凭据注入一个隔离 canary 作业,完成平台仓库探针、Registry 元数据查询和 artifact 更新。切换生产引用,观察所有运行器都报告新 generation;排空旧作业。
撤销旧凭据,再执行一次正向验证,并确认旧凭据返回认证失败。清理临时配置、缓存和失败 artifact,保留不含秘密的审计事件。
反向验证很重要:旧 Token 若在“撤销”后仍能访问,可能是缓存、复制延迟、另一个同名 Secret 或任务仍持有旧会话。此时不能宣布轮换完成。恢复时也不要重开旧 Token;从密钥系统签发权限等价的新 generation,并让更新器从最后一个未完成状态重新开始。锁文件生成了一半的工作目录应丢弃重建,避免把不同凭据、不同索引快照产生的中间结果拼成一个 PR。
容量、限流与成本是身份设计的一部分
一个组织级 App 扫描大量仓库时,平台 API 配额、Registry 并发、包下载带宽、临时磁盘和日志量会同时增长。权限越宽,单个 token 可以发起的并发越大,错误重试也越容易演变成封禁。调度器应按 installation、Registry 主机和凭据 generation 分桶限流;401/403 不自动高频重试,429 与服务端退避头进入延迟队列,artifact 下载失败与平台 PR 失败使用不同重试预算。
Registry Token 按仓库或信任域拆分会增加 Secret 数量与轮换工作,却能缩小泄漏半径;全组织共享 Token 管理简单,但任何一个可执行仓库都可能间接影响所有私有包。架构师应按“可运行未审核代码的仓库是否与高敏包共享凭据”划边界,而不是按团队名字分组。
成本核算记录每次更新的 API 请求、下载字节、锁文件 CPU/时长、失败重试和日志字节。不要把真实包名发送给外部成本系统;用稳定匿名标识聚合。出现成本突增时,先区分版本查询风暴、锁文件反复失败、平台限流重试和重复机器人,而不是简单把并发上限调大。
接进项目后,责任链要能持续审计
仓库 owner 维护更新面与允许修改的文件;平台 owner 维护 App 安装和保护规则;制品 owner 维护 Registry 读权限;安全 owner 审核 Secret 域和泄漏处置;自动化 owner 维护运行器、日志过滤、轮换与退出。任何一个角色都不应同时持有 App 私钥、Registry 管理权和保护规则绕过权。
项目模板至少注入三类可机器检查的事实:允许的机器人身份、允许的更新分支前缀、允许的 Registry 别名。每日或每次运行比较实际 installation repositories、App permissions、Secret repository policy 与声明基线,漂移即停止新 PR。机器人“还能工作”不是权限正确的证据;它可能正依赖一项已经无人记得的过度授权。
退出时先停动作,再撤身份
迁移到另一个更新器时,先暂停旧调度并禁止创建新分支,等待运行中作业结束;再关闭或接管旧 PR,清理 dashboard issue 和临时分支;确认新机器人不会同时处理同一依赖;最后撤销 App 安装、项目访问令牌、Registry Token、webhook secret 和运行器缓存。Renovate 退出时可先配置 enabled: false 让它执行清理,再撤销身份,相关行为见 enabled configuration。
删除 .github/dependabot.yml 只停止 Version Updates,不等于关闭 Security Updates,也不自动删除 Dependabot Secrets;这些开关和凭据要分别检查。退出证据应包括最后一次作业、未合并 PR 处置、安装撤销、旧 Token 反向失败、Registry 审计无后续访问和临时日志清理。
最终不变量不是“机器人拥有足够权限”,而是:每次仓库写入都能映射到一个短期平台身份,每次私有包读取都能映射到一个只读制品身份;两者作用域互不推导,任何越权访问都明确失败,凭据轮换与产品退出后旧身份无法继续行动。
