代码中删除密码不代表git历史消失,所有历史克隆者仍可访问。推荐扫描全提交历史、轮换密钥、最后清理git历史,三步缺一不可。
有人不小心提交了一个 API 密钥,一小时内注意到了,删掉它然后推送了修复。团队认为这件事已经结束了。没有结束。密钥仍然在 commit 历史里,仍然在每一份克隆里,仍然在每一个 fork 里,而且仍然有效——因为删除一行代码和轮换凭证是完全不同的两回事,而且只做了其中一件。
安全研究员 · 渗透测试工程师 Tarun Jaswani 是一名独立安全研究员和渗透测试工程师,在进攻性安全领域有五年以上经验,提交了 100 多份漏洞报告,并获得 Google、Zoho、TripAdvisor 和 Adafruit 的名人堂认可。他为需要构建和维护实际系统的人撰写安全相关文章。

这是我遇到的最常见的严重问题,而且几乎总是偶然发现的。不是通过什么巧妙的技术——而是通过阅读仓库历史记录。两年前在初始设置时提交的一个云访问密钥,一周后被移除了。旧配置文件中的一个数据库密码。某人在一次测试脚本中写入的支付网关密钥。每个都被迅速删除了,但没有一个被轮换过,这意味着每个都仍然有效,仍然在历史记录中,仍然对任何曾经有权限访问该仓库的人开放。
这个错误背后的思维模型是可以理解的。在大多数系统中,删除某样东西就会移除它。而版本控制的设计恰恰相反——它的全部目的就是确保任何东西都不会丢失。这个特性非常有用,但也使得提交的密钥默认情况下变成永久性的。
这就引出了唯一可靠的规则:一旦密钥被提交,就要将其视为已泄露。不是可能泄露,而是已泄露。问题不再是是否轮换它,而是多快轮换。
唯一规则
任何曾经被提交的密钥都已被焚毁。轮换它。
清理历史只是整理——有用,但不是修复,只做这件事会让一个有效的凭证继续流通。
最明显的地方,也仍然是最常见的。有人设置环境变量之前提交的配置文件。因为测试需要实际运行所以带有真实凭证的测试文件。在 ignore 规则存在之前添加的环境文件——注意 ignore 规则防止的是未来的提交,对历史记录中已有的提交毫无作用。
任何发送到浏览器或移动应用的东西都是公开的,无论它是如何编译、压缩或混淆的。前端构建中的一个密钥就是一个已发布的密钥。移动应用更糟糕,因为开发者认为编译提供了保护,而应用二进制文件可以轻易被提取。
管道会打印各种东西。冗长的构建步骤回显环境变量,失败的测试转储其配置,部署脚本记录自己的参数。构建日志通常比仓库本身能被更多人读取,而且没有人认为它们是数据存储。
共享文档、聊天消息、工单评论、粘贴到事件频道的截图。这些没有轮换、没有过期也没有访问审查,它们在控制要弱得多的系统中存留多年,比每个人都担心的那个仓库还要糟糕。
扫描历史,而不仅仅是当前状态。在你拥有的每个仓库的完整提交历史上运行密钥扫描仪。大多数第一次这样做的团队都会发现问题,而且发现的东西通常仍然有效。
轮换你找到的所有东西,然后清理历史。按这个顺序。轮换是修复;重写历史是事后的整理。
将扫描放在 pre-commit hook 和管道中。在密钥被提交之前捕获它代价极小。事后捕获它就是一次事件。
使用密钥管理器而不是环境文件。集中存储配合访问控制、审计日志和轮换支持,从根本上消除这类问题,而不是事后打补丁。
在任何平台支持的地方优先使用短生命周期凭证。一个一小时过期的令牌比一个存活三年的静态密钥问题小得多。
设置轮换计划并真正执行它们。一个从未被轮换过的凭证拥有无限的暴露窗口,你无法知道它是否在几年前就泄露了。
假设任何客户端的东西都是公开的。如果一个移动应用或前端需要特权访问,特权调用应该属于你控制的后端,让客户端向它进行身份验证。
假设你的一个密钥两年前泄露了,从那以后一直有人在悄悄地使用它。你怎么知道?对于大多数组织来说,诚实的答案是他们不会知道,因为凭证使用很少被监控异常——只监控失败。
今天对你的仓库运行历史扫描。这只需要几分钟,结果通常令人警醒。
轮换扫描发现的每个有效凭证,从任何授予写或管理访问权限的凭证开始。
将扫描添加到管道中,以便下一个密钥在落地之前就被捕获。
编写凭证清单,即使大致即可:所有者、范围、上次轮换时间。
选择最旧的从未轮换过的凭证并轮换它。然后为下一个设定一个日期。
版本控制的设计使得任何东西都不会丢失,这意味着提交的密钥默认情况下是永久的,删除那一行根本不会改变什么。扫描历史而不是工作树,在整理之前先轮换,将凭证移入具有轮换和审计功能的管理器中,并保留一份清单——因为你无法命名的凭证就是你永远不会轮换的那个。