密钥不小心提交上去了,先撤销还是先清历史,顺序错了等于白忙
数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。
密钥泄漏之后最贵的错误,是把它当成一个 Git 问题来处理。 大多数人发现密钥被 commit 上去,第一反应是打开搜索引擎找怎么改写历史、怎么把那次提交抹掉。这个方向从一开始就错了:从密钥出现在远端的那一刻起,它就必须被认定为已经不可信,能不能从历史里删干净跟它还能不能用完全是两件事。你把历史洗得再干净,只要那把密钥还有效,别人手里的副本照样能调用。反过来,只要你在第一时间吊销了它,历史里留着一串已经作废的字符,风险等级立刻掉到接近零。
所以整件事的正确次序是:先让密钥失效,再让业务恢复,再算清楚被用掉了什么,最后才考虑要不要动历史。这篇讲的就是泄漏之后这条动作链,以及每一步怎么判断该不该做。
站内另外三篇跟这个话题挨着但分工不同:API Key 安全管理讲平时怎么存、怎么隔离,是预防;API 密钥轮换讲例行换钥的节奏和自动化,是常态运维;AI 代码安全审计讲怎么在代码里发现这类隐患,是检测。本篇只管一件事:已经出事了,接下来几十分钟里按什么顺序做。
一、先分因:泄漏了什么,泄漏到哪儿
动手之前花两分钟把情况分清楚,后面每一步的力度都取决于这个判断。
看凭证类型。 一次性的调用密钥、可以自动续期的长期令牌、云厂商的访问密钥对、数据库连接串、私钥文件,破坏力差得很远。调用密钥最多是花钱和数据外泄,云厂商的密钥对可能直接等于半个账号控制权,私钥文件泄漏还牵扯到已经签发出去的东西是否要全部作废。
看暴露面。 私有仓库里被提交、私有仓库但有外部协作者、仓库曾经短暂公开过、仓库现在就是公开的、代码被 fork 过——这五种情况的处置强度递增。公开仓库要按”已经被自动化程序抓走”来处理,网上常年跑着扫公开提交的爬虫,这是这类泄漏最典型的路径,不要赌时间差。
看它还有没有在被用。 这是最容易被忽略的一环。有人吊销完就以为结束了,结果账单还在涨,因为项目里不止一把密钥,泄漏的那次提交连带把另外两个也带上去了。
二、判别表:从现象倒推成因
| 现象 | 大概率成因 | 怎么验证 | 处置动作 |
|---|---|---|---|
| 推送被平台拦住,或收到密钥扫描类告警 | 明文密钥确实进了某次提交的 diff | 用 git log --all -S '<特征串>' 搜全部分支的历史(不加 --all 只搜当前分支,容易漏) | 立刻去控制台吊销,别先修代码 |
| 密钥已吊销,调用还在报 401 | 某个注入点还在用旧值:CI 变量、容器镜像、本地 shell、部署平台的环境配置 | 在各环境打印密钥的长度和前后各四位(不要打印全文)做比对 | 把所有注入点列成清单逐个换,换完逐个验 |
| 吊销后账单或调用量仍在增长 | 泄漏的不止一把,或者泄漏的是能换取新令牌的上级凭证 | 在控制台按凭证逐条看最近使用时间与来源 | 该项目下所有凭证全量轮换,不做挑选 |
| 调用量在你没有发版的时段陡增,随后成片出现 403 或 429 | 密钥已被外部使用,调用量撞上了平台的风控或限流 | 先确认自己这边没改过权限和配额,再看平台调用日志里的来源与调用时段是否对得上你的业务节奏 | 按已被滥用处理,吊销之外还要评估账单和数据 |
| 仓库是公开的,或存在 fork | 历史清理在技术上已经追不回来 | 查 fork 列表,以及平台上是否还能通过旧提交哈希访问到内容 | 放弃清理幻想,把资源投到吊销与影响评估 |
改写历史后同事分支大面积出现 <<<<<<< 冲突标记 | 大家还在旧历史上工作,rebase 撞上了被替换的提交 | 先 git fetch,再用 git log --oneline -5 origin/<主分支> 跟本地同名分支比,看同一条提交信息对应的哈希是不是已经变了 | 停止手工解冲突,统一重新 clone 再迁移未推送的改动 |
| 密钥字符串在历史里搜不到,但确实泄漏了 | 它在配置文件、锁文件、构建产物、日志或截图里,不是以原样出现 | 搜密钥的片段和前缀,同时搜配置文件名 | 扩大搜索范围,别因为一次没搜到就下结论 |
三、动作顺序:五步,一步都别提前
第一步,吊销。 打开对应平台的凭证管理页,把那把密钥直接删除或禁用。这一步不需要任何前置条件,不需要等新密钥准备好,不需要等同事确认,不需要等发版窗口。服务短暂报错的代价,远小于密钥继续有效的代价。如果这把密钥背后挂着自动扣费,吊销就是最直接的止血。
第二步,签发新的并注入。 新密钥不要走跟旧的一样的路径进代码。最低限度是走环境变量,读取处保持单一入口:
export APP_API_KEY="$(cat ~/.secrets/app_api_key)"
代码里做一次显式的存在性校验,缺了就快速失败,而不是带着空值一路跑到调用处才报一个含糊的 401:
import os
import sys
key = os.environ.get("APP_API_KEY")
if not key:
sys.exit("APP_API_KEY not set")
第三步,把所有注入点走一遍。 这是实际耗时最长、也最容易漏的一环。常见的注入点包括本地 shell 配置、项目里的 dotenv 文件、CI 的加密变量、容器镜像里烤进去的层、部署平台的运行时配置、定时任务的环境、以及同事各自的机器。漏掉任何一个,症状就是”改完还是 401”,然后你会怀疑是不是新密钥有问题,白白浪费半小时。
验证用一个最轻的探活调用就够,只看状态码:
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer $APP_API_KEY" \
https://api.example.com/v1/models
拿到 200 说明这个环境的注入生效了;401 是凭证本身不对或没读到;403 是凭证被认出来了但不让用,可能是权限范围不够,也可能是服务方对可用区域有限制——部分海外服务并不面向所有国家和地区开放,这种 403 属于账号资质问题,应该走官方渠道确认能不能用、以什么身份用,不要试图绕开,绕开的后果通常是账号被封而不是调用成功;429 说明限流规则被触发,各家的阈值口径不同且会随时调整,以官方最新说明为准。
这里要跟上一节的判别表对上:同样是 403,在探活场景下大概率是你自己的权限或区域配置问题,在”没发版却调用量陡增”的场景下才要往被滥用的方向想。区分点是调用量和来源,不是状态码本身。
第四步,评估影响。 到这一步业务已经恢复,可以静下心算账。要看三样东西:这把密钥从进入远端到被吊销之间的调用记录里有没有不属于你的部分;有没有产生异常费用,账单侧的排查思路可以参考API 账单暴涨排查;以及这把密钥能碰到的数据范围有多大——如果它挂着的是能读业务库的服务账号,事情的性质就从花钱变成了数据事件,处理路径完全不同。
第五步,才轮到历史。 现在再决定要不要清理,你会发现判断轻松很多。
四、清历史这一步,什么情况下别再折腾
改写 Git 历史的代价是实打实的:所有提交哈希变化、所有人的本地仓库作废、进行中的 PR 可能全部失效、CI 缓存和依赖锁的对应关系被打乱、外部引用过旧哈希的地方全断。这些代价必须换来对等的收益才值得付。
这几种情况直接放弃清理,把时间用在别处:
仓库是公开的,或者曾经公开过哪怕一小时。内容已经在别人的缓存和爬虫库里,你在自己这边删得再干净也改变不了事实。
仓库被 fork 过。fork 出去的副本是独立的对象,你无权改写。
泄漏的提交离现在很远,中间累积了大量协作。改写的爆炸半径已经超过收益。
密钥已经吊销并且确认无异常使用。剩下的那串字符就是一堆废字符,没有清理价值。
这几种情况值得清:
仓库是私有的,协作者可控,泄漏发生在最近的少数几次提交里,而且泄漏的是私钥文件、内部证书这类即使作废也不希望留底的东西;或者你有合规要求必须证明代码库里不含明文凭证。
真要做,先把决定同步给所有人并约定一个停推窗口,用专门的历史改写工具(git filter-repo、BFG 这类)而不是手搓 rebase。有两个操作前提容易被忽略:一是这类工具通常要求在一份新鲜 clone 上运行,跑完之后远端配置可能需要重新添加;二是推之前先 git fetch 一次,让本地记录的远端状态是最新的,否则下面这个保护开关等于没开。分支和标签要分两条推,--tags 不能和 --all 写在同一条命令里:
git push --force-with-lease --all
git push --force --tags
--force-with-lease 比 --force 安全,但它的原理要说清楚:它拿本地记录的远端跟踪引用(refs/remotes/origin/*)跟远端当前值比对,不一致就拒绝覆盖。也就是说它防的是”你 fetch 之后别人又推了新东西”,防不了”你手上的远端记录本来就过期”。标签的情况更特殊:默认配置下标签在本地也存放在 refs/tags/*,不进 refs/remotes/origin/*,lease 找不到可比对的旧值。这时它不会”睁一只眼放行”,而是直接以 stale info 拒绝推送——我在两个本地仓库上复现过,对已存在的标签执行 git push --force-with-lease --tags,输出就是 ! [rejected] v1 -> v1 (stale info)。所以标签这条要么显式写成 --force-with-lease=refs/tags/<名字>:<期望的旧对象> 逐个指定期望值,要么就用上面那条 --force --tags 并靠停推窗口和事先沟通兜底,推之前务必确认没人正在打标签。别把它当成”加了 lease 就安全”的一条命令混过去。推完通知所有人重新 clone,而不是让他们 pull——基于旧历史 pull 只会制造一堆无意义的冲突。
止损点要提前定好。 我的经验是给历史清理设一个明确的时间盒,比如半天。超过这个时间还在跟子模块、大文件、LFS 指针、CI 缓存较劲,就说明成本已经失控,应该退回到”新建一个干净仓库、把当前代码作为初始提交导入、旧仓库设为归档只读”这条路。这条路听起来粗暴,但它是有界的,而无止境的历史改写不是。放弃历史不等于放弃安全,因为安全早在第一步吊销时就已经拿到手了。
五、避坑清单
先修代码后吊销。 为什么会踩:修代码看起来更”专业”,吊销显得莽撞,而且怕影响线上。怎么避:把吊销当成消防栓,先拉再说。线上短暂报错是可恢复的,密钥被人拿着用不是。
只吊销了发现的那一把。 为什么会踩:注意力被引到具体那一行,忽略了同一次提交或同一个配置文件里往往躺着好几个凭证。怎么避:把泄漏的那次提交的完整 diff 逐行看一遍,同类凭证一起换。
把密钥写进 .gitignore 就以为解决了。 为什么会踩:.gitignore 只对未被跟踪的文件生效,已经进了索引的文件加不加它都照样提交。怎么避:确认文件是否已被跟踪,必要时先从索引移除;同时明白这只影响将来,对已经推上去的历史毫无作用。
为了排查方便把密钥打进日志。 为什么会踩:排查 401 时想确认到底读到了什么值,顺手 print 了出来。怎么避:只打印长度和前后各四位,够做比对就行;日志系统的留存范围往往比代码仓库还广,一旦写进去等于又泄漏一次。
用 AI 编程工具帮忙清理历史,直接执行它给的命令。 为什么会踩:这类工具生成的历史改写命令看起来很像那么回事,但它读不到你的远端状态、分支保护规则和协作现状,给出的分支强推往往直接是 --force 而不是 --force-with-lease,跑完不可逆。怎么避:改写类命令一律先在仓库的一份完整镜像上演练。AI 生成代码在什么边界内可以直接落地,AI 代码能上生产吗那篇讲得更细,破坏性操作明确在边界之外。
吊销完不改流程。 为什么会踩:应急做完松一口气,觉得这次是意外。怎么避:一次泄漏说明存在一条从明文到远端的通路,这条通路不堵,下次还会走同一条。最低成本的堵法是加一个提交前的本地扫描钩子,命中疑似凭证特征就拦住提交。
把这次泄漏当成纯技术事件。 为什么会踩:只盯着技术处置,没想过密钥背后连着什么数据。怎么避:吊销之后,用AI 数据安全风险那篇的思路过一遍这把密钥能触达的数据面,涉及用户数据的话,事件级别和通报义务都要重新判断。
六、收束
这件事的核心判断只有一句:密钥泄漏是凭证问题,不是版本控制问题。凭证的解药是让它失效,版本控制那边做什么都只是善后。想明白这个先后,剩下的动作就都顺了。
出事时可以照着这份清单走一遍:
- 那把密钥是否已经在平台侧被吊销或禁用,不是”准备吊销”而是已经吊销?
- 同一次提交里还有没有别的凭证一起进去了?
- 新密钥的所有注入点是否逐个换过并逐个验过,包括同事的机器和 CI?
- 从泄漏到吊销这段时间的调用记录、费用和数据触达范围,是否都看过了?
- 仓库是否公开过、被 fork 过?如果是,是否已经放弃清理历史这条路?
- 如果决定改写历史,是否定了停推窗口、用了带保护的推送、通知所有人重新 clone?
- 这条从明文到远端的通路,是否已经被提交前的扫描堵上?
前四条没做完之前,不要碰历史。