把 API Key 误提交到 Git 仓库、贴进工单、截进群聊,往往只需要几秒。真正危险的不是这次手滑,而是团队把“删除那一行”误当成“事故已经结束”。
一把已经公开过的 Key,即使随后从最新代码中消失,仍可能留在 Git 历史、Fork、构建日志、镜像层、缓存、聊天记录和他人的本地克隆中。只要它仍然有效,拿到副本的人就仍可能调用接口。
GitHub 的泄露凭据处置文档也明确建议把泄露的 Secret 视为已被攻破。简单删除代码、再提交一次,或者重新创建仓库,都不能让原凭据自动失效。
因此,API Key 泄露后的正确目标不是“把字符串藏回去”,而是完成四件事:
- 让旧凭据失效。
- 恢复合法调用。
- 确认是否已被滥用。
- 降低下一次泄露的概率。
本文给出一套适用于 AI API、云服务、支付接口、数据库令牌和内部服务 Token 的通用处理方法。具体控制台名称、审计字段和轮换能力,应以凭据发行方的当前官方文档为准。
一、先记住最重要的顺序:撤销优先于删历史
发现泄露后,很多人的第一反应是删文件、改提交、强推分支。
这些动作能减少后续暴露,却不能阻止已经复制走的 Key 被继续使用。真正的止损动作发生在凭据发行方:撤销旧 Key,或者先创建替代 Key、完成切换,再撤销旧 Key。
可以把处置顺序记成一句话:
先让旧钥匙开不了门,再清理散落的钥匙照片,最后检查门有没有被打开过。
对高风险凭据——例如生产环境、公开仓库、管理员权限、可产生费用或可读取敏感数据的 Key——默认按“已被第三方获得”处理,不要等待出现异常账单后再行动。
公开互联网存在自动化 Secret 扫描,暴露窗口越长,风险越高。
推荐的总流程是:
- 记录发现时间、泄露位置和凭据类型,但不要在新工单里再次粘贴完整 Key。
- 判断凭据权限、环境、有效状态和依赖范围。
- 立即撤销,或执行受控的“新旧并行—切换—验证—撤销”。
- 检查调用日志、账单、资源变更和异常来源。
- 清理仓库、日志、制品、聊天和文档中的副本。
- 修复根因,增加 Secret 扫描、最小权限和短期凭据。
撤销、轮换、删除不是同一件事:
- 撤销:让旧凭据立即失效。
- 轮换:创建并部署新凭据,同时淘汰旧凭据。
- 删除:从暴露载体中移除敏感字符串。
三者都可能需要,但安全优先级不同。
二、前十分钟:建立一个不扩散秘密的事故记录
应急响应需要证据,但收集证据不能制造新的泄露。
事故群、工单和截图里只记录可识别但不可使用的信息,例如:
- 凭据发行方;
- 凭据名称;
- 末四位或指纹;
- 所属环境;
- 首次发现时间;
- 暴露位置;
- 仓库可见性;
- 当前负责人。
不要粘贴完整 Key,不要把含 Key 的终端历史直接上传,也不要在公共 Issue 中贴原始请求头。
若必须证明两个位置出现的是同一把 Key,可以在本地计算带盐哈希,或者使用供应商提供的凭据 ID,再记录摘要。
一个最小事故卡片可以写成:
incident_id: SEC-20260701-001 discovered_at: 2026-07-01T11:20:00+08:00 secret_type: API key provider: <provider name> secret_identifier: <key id or last 4 chars> environment: production exposure_location: repository/file/path:line repository_visibility: public | private | unknown first_known_exposure: <commit time or message time> current_status: active | revoked | unknown owner: <team> incident_lead: <person or role>同时应当冻结无关修改。
事故期间反复改 Base URL、权限、模型名和部署配置,会破坏审计基线。除止损必需动作外,所有变更都应该绑定事故编号、时间、执行者和验证结果。
三、快速定级:不是所有 Key 的爆炸半径都一样
定级的目的不是拖延撤销,而是决定需要多快、多大范围地行动。
至少回答下面六个问题:
| 维度 | 低风险信号 | 高风险信号 |
|---|---|---|
| 可见性 | 未发送、本地未提交 | 公共仓库、公开网页、多人群聊 |
| 环境 | 隔离测试环境 | 生产环境 |
| 权限 | 只读、单资源 | 管理员、跨项目、写入或删除权限 |
| 有效性 | 已过期或已撤销 | 当前仍可用 |
| 价值 | 无真实数据、无账单能力 | 可读敏感数据、可产生费用、可修改资源 |
| 暴露时长 | 数秒且被预提交钩子拦截 | 已存在数小时、数天或更久 |
如果无法确认仓库是否曾公开、Key 是否仍然有效、日志是否完整,就把“未知”按照较高风险处理。
未知不是安全证据。
还要识别 Key 的下游消费者,例如:
- 生产服务;
- 定时任务;
- CI/CD;
- 个人脚本;
- 监控任务;
- 数据管道;
- 移动端配置;
- 灾备环境。
没有依赖清单时,撤销可能造成停机;但因为害怕停机而一直不撤销,又会放大安全风险。
解决方法不是无限延迟,而是由安全负责人和服务负责人共同选择轮换策略。
四、止损方案:直接撤销,还是先轮换再撤销
1. 能立即撤销时,直接撤销
这种方式适合以下场景:
- Key 已经公开暴露;
- Key 权限较大;
- 依赖范围明确;
- 业务可以短暂停止。
撤销后,应当使用旧 Key 发起一个无副作用的最小请求,确认它已经返回认证失败。
不要只相信控制台按钮已经变灰,系统需要证据证明旧凭据不能继续使用。
验证请求必须避免产生写操作,也不要把旧 Key 写回 Shell 历史。可以临时从受控环境变量读取,并在验证完成后立即清除会话变量。
不同平台的认证失败状态和错误体可能不同,应以发行方文档为准。
2. 不能立即停机时,执行受控轮换
当大量服务共享同一凭据时,直接撤销可能导致生产中断。
此时可以采用短时间双凭据窗口:
- 创建权限不高于旧 Key 的新 Key。
- 把新 Key 写入 Secret 管理系统,而不是源代码。
- 按照消费者清单逐个更新并重新部署。
- 通过请求 ID、成功率和认证错误确认新 Key 已生效。
- 在预先设定的最短窗口结束时撤销旧 Key。
- 再次验证旧 Key 失效,并监控是否仍有服务尝试使用它。
双凭据窗口不是“以后再说”。
它必须有明确的截止时间、负责人和自动提醒。否则团队很容易留下两把长期有效的 Key,攻击面反而会扩大。
轮换时不要顺便扩大权限。
新 Key 应采用最小权限、单一环境、单一应用和明确有效期。若平台支持作用域、来源限制、IP 限制、预算或配额告警,可以在不影响业务的前提下启用。
但不能把这些控制写成任何供应商都必然支持的功能。
3. 共享 Key 需要拆分
如果开发、测试、生产和多个应用共用一把 Key,事故调查几乎无法判断异常调用来自哪里。
应当把轮换当成拆分机会:每个环境、服务或工作负载使用独立身份。
这样既能缩小爆炸半径,也能在审计中把异常调用定位到具体消费者。
五、审计:回答“它有没有被用过”,而不是只看账单
撤销完成后,第二个核心问题是:凭据在暴露窗口内是否被未经授权地使用。
审计窗口应当从“首次可能暴露”开始,而不是从“被发现”开始。
如果某次提交在三天前进入公共仓库,今天才收到告警,调查至少要覆盖这三天,并为时钟偏差和日志延迟留出余量。
建议从四类证据交叉检查。
1. API 调用证据
检查以下信息:
- 请求时间;
- 接口路径;
- 模型或资源;
- 状态码;
- 请求 ID;
- 来源网络;
- User-Agent;
- 用量;
- 错误类型。
不要仅仅依赖 IP。
合法流量可能经过动态出口,攻击者也可能使用云代理。更可靠的是寻找多维异常,例如:
- 陌生地区;
- 从未使用过的接口;
- 异常时间段;
- 突然升高的请求速率;
- 与部署记录不一致的调用模式。
2. 资源与权限变更
检查是否出现以下操作:
- 创建新 Key;
- 创建新用户;
- 创建新项目;
- 创建新规则;
- 修改回调地址;
- 修改权限;
- 删除数据;
- 下载敏感对象。
具备写权限的 Key 泄露后,影响可能不只是一笔调用费用。
3. 费用与配额
查看用量、账单、配额和失败重试的变化。
没有费用异常,不能证明没有发生滥用。攻击者可能只做少量探测,或者访问不会直接计费的资源。
4. 本地与供应链证据
搜索以下位置:
- Git 仓库历史;
- CI 日志;
- 构建制品;
- 容器镜像层;
- 包管理缓存;
- 对象存储;
- 备份;
- 文档系统;
- 工单;
- 聊天消息;
- 开发者本地克隆。
Key 可能从一个位置泄露,却在另一个位置被复制扩散。
审计输出应区分三种结论:
- 确认存在滥用。
- 未发现滥用证据。
- 现有证据不足。
不要把“日志里没看到”写成“确定未被使用”,尤其是在日志保留期不足、字段缺失或时钟不一致时。
六、Git 泄露:为什么新增一个删除提交还不够
Git 保存的是历史。
把config.js里的 Key 删除并提交,只会让最新版本看不到它;旧提交仍可能包含完整字符串,Fork、缓存和本地克隆也可能保留副本。
正确顺序仍然是先撤销或轮换。
随后,根据敏感性和传播范围决定是否重写历史。
GitHub 文档提醒,历史重写会改变提交哈希、破坏签名、影响开放中的拉取请求,并要求协作者重新同步。已经存在的克隆也不会自动清除。
因此,历史重写是降低持续暴露的措施,不是替代撤销的魔法橡皮擦。
如果决定重写历史,应当由仓库管理员统一执行并通知所有协作者:
- 确定 Secret 出现的文件、路径、分支和标签。
- 在备份和受控环境中使用
git-filter-repo等工具清理。 - 暂停普通推送,强制更新受影响引用。
- 清理或重新创建受污染的 Fork、缓存与构建产物。
- 要求协作者重新克隆,或按照统一步骤处理本地历史。
- 再次运行 Secret 扫描,确认没有其他副本。
不要在没有协调的情况下随意执行push --force。
它可能打断团队工作,却仍然无法触及已经被复制出去的内容。
七、日志、截图和构建产物也要清理
Key 不只会出现在代码中。
最常见的二次泄露源包括:
- 开启
set -x后输出的 Shell 命令; - 打印完整请求头的 HTTP 调试日志;
- CI 环境变量转储;
- Dockerfile 中的
ARG、ENV或构建层; - 异常堆栈、Sentry 附件和 APM Span;
- 教程截图、录屏和终端历史;
.env备份、压缩包和临时目录;- 聊天机器人对话、工单附件和知识库页面。
清理日志时要兼顾取证完整性。
不要让个人直接删除所有审计日志。应当由有权限的负责人先保存受控证据,再对可公开访问或不应长期保留的副本进行脱敏、下架或缩短保留期。
同时记录清理对象、时间、执行者和依据。
应用日志默认不应记录以下内容:
Authorization;- Cookie;
- URL 查询串中的完整 Token;
- 密码;
- 数据库连接串;
- 私钥;
- 完整请求正文。
调试时记录请求 ID、状态码、目标主机、路径模板、响应大小和耗时,通常已经足够定位大多数接口问题。
一个简单的日志脱敏函数可以采取“允许列表”,而不是“发现什么就替换什么”:
functionsafeRequestLog(req){return{requestId:req.headers["x-request-id"],method:req.method,host:req.hostname,path:req.route?.path??"unknown",contentType:req.headers["content-type"],contentLength:req.headers["content-length"],// 不记录 authorization、cookie、正文和完整查询串};}允许列表的好处是,新增加的敏感请求头不会因为漏写正则而自动进入日志。
八、恢复服务:验证新 Key,而不是凭感觉宣布完成
新 Key 部署后,至少执行四类验证:
- 正常业务请求成功,并且使用的是新凭据。
- 旧 Key 的最小无副作用请求明确失败。
- 所有消费者都停止使用旧 Key。
- 日志、错误页面和追踪系统没有记录新 Key 明文。
不要只在开发电脑上测试。
CI、容器、定时任务和生产工作负载可能读取不同的 Secret 版本。
应当为每个消费者记录部署版本、Secret 版本或配置哈希,并使用一次真实但低风险的健康检查进行验证。
如果撤销后仍有认证失败,应当先找出尚未迁移的消费者,不要重新启用旧 Key。
重新启用旧 Key,会让已经关闭的攻击窗口再次打开。
九、根因复盘:从“谁提交的”转向“为什么系统允许提交”
把事故归结为某个人粗心,无法防止下一次发生。
更有效的复盘问题包括:
- 为什么应用需要把长期 Key 放进开发者可见的文件?
- 为什么
.env、示例配置或调试输出进入了版本控制? - 为什么预提交、CI 或平台 Push Protection 没有拦截?
- 为什么生产和测试共用一把 Key?
- 为什么轮换时找不到消费者清单?
- 为什么日志能够看到完整认证头?
- 为什么 Key 没有过期时间、权限边界或用量告警?
根因通常不是单点,而是一条控制链同时缺失:
- Secret 存储不规范;
- 代码审查没有检查;
- 自动扫描未启用;
- 凭据权限过大;
- 监控不完善;
- 应急手册未演练。
十、预防体系:把长期静态 Key 变成例外
1. 使用 Secret 管理系统
生产凭据应由专用 Secret 管理系统、云密钥库或受控运行时注入。
源代码只保留变量名和示例值,例如:
AI_API_KEY=YOUR_API_KEY.gitignore是必要措施,但它不是安全边界。
它只能阻止符合规则的未跟踪文件被加入,无法保护已经提交、被强制添加或写进其他文件的 Secret。
2. 启用提交前与服务端扫描
本地预提交扫描反馈快,CI 扫描覆盖更多入口,托管平台的 Secret Scanning 和 Push Protection 能在服务端提供额外防线。
GitHub 的 Push Protection 会在推送时检查已支持的 Secret 模式并阻止推送,组织还可以按照需要配置自定义模式。
任何绕过操作都应当记录理由并接受审查。
扫描器会产生误报,也可能漏掉自定义格式。
正确做法是维护规则、测试样本和例外审批,而不是因为一次误报就关闭整条防线。
3. 最小权限与分环境
每个 Key 只获得完成任务所需的权限,并绑定单一环境和明确负责人。
避免“万能生产 Key”被复制给多个团队。
OWASP 的 Secret 管理建议强调细粒度访问控制、生命周期管理、撤销和轮换。这些控制能显著缩小泄露后的爆炸半径。
4. 优先使用短期身份
如果平台支持工作负载身份、OIDC 或动态 Secret,应优先使用短期令牌替代长期静态 Key。
以 GitHub Actions 的 OIDC 为例,工作流可以向云服务交换短期访问令牌,从而减少在 CI 中长期保存云凭据的需要。
短期令牌仍需正确限制以下内容:
- 受众;
- 主体;
- 仓库;
- 分支;
- 权限。
不能因为令牌寿命短,就放弃必要的身份校验。
5. 建立可轮换的元数据
每个 Secret 至少记录:
- 负责人;
- 用途;
- 消费者;
- 环境;
- 权限;
- 创建时间;
- 到期时间;
- 上次轮换时间;
- 轮换步骤;
- 紧急联系人。
没有这些元数据,事故发生时团队甚至不知道撤销会影响谁。
6. 定期演练
可以每季度选择一把低风险测试 Key 进行演练:
- 发现告警。
- 通知负责人。
- 创建替代凭据。
- 更新消费者。
- 撤销旧凭据。
- 验证旧凭据失效。
- 审计调用记录。
- 清理暴露载体。
演练应当计时,并把卡住的步骤转化为自动化任务。
十一、可直接复制的应急检查表
发现与定级
- 未在工单、群聊或截图中再次暴露完整 Key
- 已记录凭据 ID、发行方、环境、权限和负责人
- 已确认仓库或载体的可见范围
- 已确定首次可能暴露时间和审计窗口
- 未知项已按较高风险处理
止损与恢复
- 已撤销旧 Key,或启动有截止时间的受控轮换
- 新 Key 权限不高于旧 Key
- 已更新全部消费者并逐一验证
- 已用无副作用请求确认旧 Key 失效
- 未通过重新启用旧 Key 解决迁移遗漏
审计与清理
- 已检查 API 调用、资源变更、账单和权限记录
- 已检查仓库历史、分支、标签、Fork 和本地克隆
- 已检查 CI 日志、镜像层、制品、缓存和备份
- 已检查聊天、工单、文档、截图和录屏
- 结论区分“未发现证据”与“确认未发生”
防止复发
- 凭据迁移到 Secret 管理系统或受控运行时
- 启用预提交、CI 和服务端 Secret 扫描
- 按环境与应用拆分 Key,并落实最小权限
- 能使用时改为短期身份或动态 Secret
- 为每个 Secret 建立消费者和轮换元数据
- 应急手册已经演练并记录完成时间
结语
API Key 泄露不是一个“把字符串删掉”的代码问题,而是一场小型身份安全事件。
旧 Key 是否失效,决定攻击窗口是否真正关闭;消费者清单是否完整,决定轮换能否不靠运气;审计证据是否充分,决定团队能否判断影响;预防控制是否落地,决定相同事故会不会再次发生。
最稳妥的原则仍然很朴素:
泄露即视为失守,先撤销或轮换,随后审计和清理,最后把长期、共享、权限过大的 Key,逐步替换为可管理、可追踪、可快速失效的身份。
安全响应的质量,不在于事故群里有多少消息,而在于旧钥匙什么时候真正开不了门。
参考资料
GitHub Docs:Remediating a leaked secret in your repository
https://docs.github.com/en/code-security/secret-scanning/managing-alerts-from-secret-scanning/remediating-a-leaked-secretGitHub Docs:Removing sensitive data from a repository
https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repositoryGitHub Docs:Push protection
https://docs.github.com/en/code-security/concepts/secret-security/push-protectionGitHub Docs:Secret scanning
https://docs.github.com/en/code-security/concepts/secret-security/secret-scanningGitHub Docs:OpenID Connect
https://docs.github.com/en/actions/concepts/security/openid-connectOWASP Cheat Sheet Series:Secrets Management Cheat Sheet
https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.htmlOWASP Cheat Sheet Series:Logging Cheat Sheet
https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html