API Key 泄露后别只删代码:从止损、轮换到审计的完整应急手册

把 API Key 误提交到 Git 仓库、贴进工单、截进群聊,往往只需要几秒。真正危险的不是这次手滑,而是团队把“删除那一行”误当成“事故已经结束”。

一把已经公开过的 Key,即使随后从最新代码中消失,仍可能留在 Git 历史、Fork、构建日志、镜像层、缓存、聊天记录和他人的本地克隆中。只要它仍然有效,拿到副本的人就仍可能调用接口。

GitHub 的泄露凭据处置文档也明确建议把泄露的 Secret 视为已被攻破。简单删除代码、再提交一次,或者重新创建仓库,都不能让原凭据自动失效。

因此,API Key 泄露后的正确目标不是“把字符串藏回去”,而是完成四件事:

  1. 让旧凭据失效。
  2. 恢复合法调用。
  3. 确认是否已被滥用。
  4. 降低下一次泄露的概率。

本文给出一套适用于 AI API、云服务、支付接口、数据库令牌和内部服务 Token 的通用处理方法。具体控制台名称、审计字段和轮换能力,应以凭据发行方的当前官方文档为准。

一、先记住最重要的顺序:撤销优先于删历史

发现泄露后,很多人的第一反应是删文件、改提交、强推分支。

这些动作能减少后续暴露,却不能阻止已经复制走的 Key 被继续使用。真正的止损动作发生在凭据发行方:撤销旧 Key,或者先创建替代 Key、完成切换,再撤销旧 Key。

可以把处置顺序记成一句话:

先让旧钥匙开不了门,再清理散落的钥匙照片,最后检查门有没有被打开过。

对高风险凭据——例如生产环境、公开仓库、管理员权限、可产生费用或可读取敏感数据的 Key——默认按“已被第三方获得”处理,不要等待出现异常账单后再行动。

公开互联网存在自动化 Secret 扫描,暴露窗口越长,风险越高。

推荐的总流程是:

  1. 记录发现时间、泄露位置和凭据类型,但不要在新工单里再次粘贴完整 Key。
  2. 判断凭据权限、环境、有效状态和依赖范围。
  3. 立即撤销,或执行受控的“新旧并行—切换—验证—撤销”。
  4. 检查调用日志、账单、资源变更和异常来源。
  5. 清理仓库、日志、制品、聊天和文档中的副本。
  6. 修复根因,增加 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. 不能立即停机时,执行受控轮换

当大量服务共享同一凭据时,直接撤销可能导致生产中断。

此时可以采用短时间双凭据窗口:

  1. 创建权限不高于旧 Key 的新 Key。
  2. 把新 Key 写入 Secret 管理系统,而不是源代码。
  3. 按照消费者清单逐个更新并重新部署。
  4. 通过请求 ID、成功率和认证错误确认新 Key 已生效。
  5. 在预先设定的最短窗口结束时撤销旧 Key。
  6. 再次验证旧 Key 失效,并监控是否仍有服务尝试使用它。

双凭据窗口不是“以后再说”。

它必须有明确的截止时间、负责人和自动提醒。否则团队很容易留下两把长期有效的 Key,攻击面反而会扩大。

轮换时不要顺便扩大权限。

新 Key 应采用最小权限、单一环境、单一应用和明确有效期。若平台支持作用域、来源限制、IP 限制、预算或配额告警,可以在不影响业务的前提下启用。

但不能把这些控制写成任何供应商都必然支持的功能。

3. 共享 Key 需要拆分

如果开发、测试、生产和多个应用共用一把 Key,事故调查几乎无法判断异常调用来自哪里。

应当把轮换当成拆分机会:每个环境、服务或工作负载使用独立身份。

这样既能缩小爆炸半径,也能在审计中把异常调用定位到具体消费者。

五、审计:回答“它有没有被用过”,而不是只看账单

撤销完成后,第二个核心问题是:凭据在暴露窗口内是否被未经授权地使用。

审计窗口应当从“首次可能暴露”开始,而不是从“被发现”开始。

如果某次提交在三天前进入公共仓库,今天才收到告警,调查至少要覆盖这三天,并为时钟偏差和日志延迟留出余量。

建议从四类证据交叉检查。

1. API 调用证据

检查以下信息:

  • 请求时间;
  • 接口路径;
  • 模型或资源;
  • 状态码;
  • 请求 ID;
  • 来源网络;
  • User-Agent;
  • 用量;
  • 错误类型。

不要仅仅依赖 IP。

合法流量可能经过动态出口,攻击者也可能使用云代理。更可靠的是寻找多维异常,例如:

  • 陌生地区;
  • 从未使用过的接口;
  • 异常时间段;
  • 突然升高的请求速率;
  • 与部署记录不一致的调用模式。

2. 资源与权限变更

检查是否出现以下操作:

  • 创建新 Key;
  • 创建新用户;
  • 创建新项目;
  • 创建新规则;
  • 修改回调地址;
  • 修改权限;
  • 删除数据;
  • 下载敏感对象。

具备写权限的 Key 泄露后,影响可能不只是一笔调用费用。

3. 费用与配额

查看用量、账单、配额和失败重试的变化。

没有费用异常,不能证明没有发生滥用。攻击者可能只做少量探测,或者访问不会直接计费的资源。

4. 本地与供应链证据

搜索以下位置:

  • Git 仓库历史;
  • CI 日志;
  • 构建制品;
  • 容器镜像层;
  • 包管理缓存;
  • 对象存储;
  • 备份;
  • 文档系统;
  • 工单;
  • 聊天消息;
  • 开发者本地克隆。

Key 可能从一个位置泄露,却在另一个位置被复制扩散。

审计输出应区分三种结论:

  1. 确认存在滥用。
  2. 未发现滥用证据。
  3. 现有证据不足。

不要把“日志里没看到”写成“确定未被使用”,尤其是在日志保留期不足、字段缺失或时钟不一致时。

六、Git 泄露:为什么新增一个删除提交还不够

Git 保存的是历史。

config.js里的 Key 删除并提交,只会让最新版本看不到它;旧提交仍可能包含完整字符串,Fork、缓存和本地克隆也可能保留副本。

正确顺序仍然是先撤销或轮换。

随后,根据敏感性和传播范围决定是否重写历史。

GitHub 文档提醒,历史重写会改变提交哈希、破坏签名、影响开放中的拉取请求,并要求协作者重新同步。已经存在的克隆也不会自动清除。

因此,历史重写是降低持续暴露的措施,不是替代撤销的魔法橡皮擦。

如果决定重写历史,应当由仓库管理员统一执行并通知所有协作者:

  1. 确定 Secret 出现的文件、路径、分支和标签。
  2. 在备份和受控环境中使用git-filter-repo等工具清理。
  3. 暂停普通推送,强制更新受影响引用。
  4. 清理或重新创建受污染的 Fork、缓存与构建产物。
  5. 要求协作者重新克隆,或按照统一步骤处理本地历史。
  6. 再次运行 Secret 扫描,确认没有其他副本。

不要在没有协调的情况下随意执行push --force

它可能打断团队工作,却仍然无法触及已经被复制出去的内容。

七、日志、截图和构建产物也要清理

Key 不只会出现在代码中。

最常见的二次泄露源包括:

  • 开启set -x后输出的 Shell 命令;
  • 打印完整请求头的 HTTP 调试日志;
  • CI 环境变量转储;
  • Dockerfile 中的ARGENV或构建层;
  • 异常堆栈、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 部署后,至少执行四类验证:

  1. 正常业务请求成功,并且使用的是新凭据。
  2. 旧 Key 的最小无副作用请求明确失败。
  3. 所有消费者都停止使用旧 Key。
  4. 日志、错误页面和追踪系统没有记录新 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 进行演练:

  1. 发现告警。
  2. 通知负责人。
  3. 创建替代凭据。
  4. 更新消费者。
  5. 撤销旧凭据。
  6. 验证旧凭据失效。
  7. 审计调用记录。
  8. 清理暴露载体。

演练应当计时,并把卡住的步骤转化为自动化任务。

十一、可直接复制的应急检查表

发现与定级

  • 未在工单、群聊或截图中再次暴露完整 Key
  • 已记录凭据 ID、发行方、环境、权限和负责人
  • 已确认仓库或载体的可见范围
  • 已确定首次可能暴露时间和审计窗口
  • 未知项已按较高风险处理

止损与恢复

  • 已撤销旧 Key,或启动有截止时间的受控轮换
  • 新 Key 权限不高于旧 Key
  • 已更新全部消费者并逐一验证
  • 已用无副作用请求确认旧 Key 失效
  • 未通过重新启用旧 Key 解决迁移遗漏

审计与清理

  • 已检查 API 调用、资源变更、账单和权限记录
  • 已检查仓库历史、分支、标签、Fork 和本地克隆
  • 已检查 CI 日志、镜像层、制品、缓存和备份
  • 已检查聊天、工单、文档、截图和录屏
  • 结论区分“未发现证据”与“确认未发生”

防止复发

  • 凭据迁移到 Secret 管理系统或受控运行时
  • 启用预提交、CI 和服务端 Secret 扫描
  • 按环境与应用拆分 Key,并落实最小权限
  • 能使用时改为短期身份或动态 Secret
  • 为每个 Secret 建立消费者和轮换元数据
  • 应急手册已经演练并记录完成时间

结语

API Key 泄露不是一个“把字符串删掉”的代码问题,而是一场小型身份安全事件。

旧 Key 是否失效,决定攻击窗口是否真正关闭;消费者清单是否完整,决定轮换能否不靠运气;审计证据是否充分,决定团队能否判断影响;预防控制是否落地,决定相同事故会不会再次发生。

最稳妥的原则仍然很朴素:

泄露即视为失守,先撤销或轮换,随后审计和清理,最后把长期、共享、权限过大的 Key,逐步替换为可管理、可追踪、可快速失效的身份。

安全响应的质量,不在于事故群里有多少消息,而在于旧钥匙什么时候真正开不了门。

参考资料

  1. 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-secret

  2. GitHub Docs:Removing sensitive data from a repository
    https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository

  3. GitHub Docs:Push protection
    https://docs.github.com/en/code-security/concepts/secret-security/push-protection

  4. GitHub Docs:Secret scanning
    https://docs.github.com/en/code-security/concepts/secret-security/secret-scanning

  5. GitHub Docs:OpenID Connect
    https://docs.github.com/en/actions/concepts/security/openid-connect

  6. OWASP Cheat Sheet Series:Secrets Management Cheat Sheet
    https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html

  7. OWASP Cheat Sheet Series:Logging Cheat Sheet
    https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html