早上好,我是まさきん。
我正在准备把家里用的自动化脚本放到 GitHub 的公开仓库上。在排查敏感信息时,扫描工具明明说“没有泄露”,结果还是剩下了2处。这里把整个检查流程记录下来。
※以下为文章发布时的信息,最新条件请以官方网站为准。
- gitleaks 通过正则表达式和熵值,找出 API 密钥这类“能从格式上认出来的密钥”
- 格式上没有特征的硬编码值,有可能从扫描工具眼皮底下溜过去
- 用旧 .env 里的实际值对整个仓库做比对,就能把漏网之鱼找出来
- 公开仓库会免费自动运行 GitHub 的 Secret Scanning
- 密钥一旦进入历史记录,要先吊销并重新签发密钥,再考虑改写历史
显示“no leaks”却还剩下的2处
我家里跑着各种各样的自动化工具。配置文件(.env)分散成14个,密钥文件也到处都是。我把它们统一成了一个配置文件(41个键)加一个 credentials 文件夹,代码这边大约70个文件的引用也都改了过来。
假设把整理好的东西提交到 git,我先跑了一遍 gitleaks,结果是“no leaks”。
不过保险起见,我又拿旧 .env 里的实际值逐一比对了一遍。结果找出了2处 gitleaks 没发现的硬编码:一处是局域网内设备的访问码,另一处是和个人文件名有关的值。
这两个值看起来都不像 API 密钥。从工作原理来看,扫描工具漏掉它们其实也不奇怪。
gitleaks 是怎么找出密钥的
gitleaks 是一款专门查找密码、API 密钥等敏感信息的工具,可以检查 git 历史、文件以及标准输入。
检测的核心是“规则”。每条规则都带有正则表达式,用来匹配各个服务的令牌格式。官方仓库的默认配置里收录了200多条规则。
另一个维度是熵值,也就是衡量一串字符有多随机的数值。通用 API 密钥的规则,就是结合 key 之类的关键词和值的熵来判断的。
整理一下大致如下:
| 类型 | gitleaks 能否发现 | 原因 |
|---|---|---|
| 主流服务的 API 密钥、令牌 | 容易发现 | 有固定的前缀或格式 |
| password= 后面跟着的长随机字符串 | 容易发现 | 关键词和熵值都能命中 |
| 只有几位数字的短访问码 | 不易发现 | 格式和随机性都没什么特征 |
| 内部 URL、主机名、文件名 | 不易发现 | 和普通字符串难以区分 |
| 没有键名的硬编码值 | 不易发现 | 缺少可作线索的关键词 |
安装和基本命令
官方 README 介绍了通过 Homebrew、Docker 安装,或用 Go 从源码构建。Mac 上这样装就行:
brew install gitleaks
要注意的是命令名。旧文章里常见的 detect 和 protect,在 v8.19.0 已被弃用。现在基本用下面3种模式:
| 命令 | 检查对象 | 适用场景 |
|---|---|---|
| gitleaks git | git 历史(git log -p 的差异) | 追溯检查已提交的全部内容 |
| gitleaks dir | 目录或文件 | 检查还没放进 git 的文件 |
| gitleaks stdin | 标准输入 | 直接检查其他命令的输出 |
常用写法如下:
# 检查当前仓库的历史
gitleaks git -v
# 在放进 git 之前,检查准备公开的文件夹
gitleaks dir -v ./my-scripts
发现泄露时退出码为1,什么都没有则为0,可以在脚本里拿来做拦截判断。
误报的处理
测试用的假数据之类,有时会触发误报。官方 README 给出了3种办法:
- 在该行加上
gitleaks:allow注释 - 把报告里的 Fingerprint 写进
.gitleaksignore,只忽略这一条 - 在配置文件
.gitleaks.toml的 allowlist 里,按路径或正则整体排除
allowlist 在 v8.25.0 改成了 [[allowlists]] 的写法,照抄旧配置示例时要留意。
我个人觉得用 Fingerprint 逐条忽略比较稳妥。按路径整体排除的话,以后真有密钥混进来也发现不了。
用 pre-commit 在提交前拦截
作为日常的防线,gitleaks 也可以作为 pre-commit 钩子使用。
- 安装 pre-commit
- 在仓库根目录的
.pre-commit-config.yaml写入以下内容 - 执行
pre-commit autoupdate和pre-commit install
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.24.2
hooks:
- id: gitleaks
GitHub 的 Secret Scanning 和 Push Protection
GitHub 这边也有查找密钥的机制。在公开仓库中的情况如下:
| 功能 | 公开仓库中的情况 | 作用 |
|---|---|---|
| Secret Scanning | 免费自动运行 | 检查所有分支的 git 历史,发现后发出通知 |
| Push protection for users | 按账户默认开启 | 拦截向公开仓库推送含密钥的内容 |
| Push protection for repositories | 需要 GitHub Secret Protection | 按仓库拦截推送 |
Secret Scanning 也会检查 Issue 和 Pull Request 的正文。如果发现合作服务商的密钥,还会通知对应的服务商。
话虽如此,这两者基本上针对的都是“能从格式上认出来的密钥”。把没有格式特征的值也全交给它们,恐怕有点冒险。
把 .env 拆出去,再配合 .gitignore
最有效的办法,就是不把密钥写进代码。GitHub 官方文档也推荐使用环境变量。
我这次的统一整理也是这个思路。公开时的结构大致如下:
| 文件 | 是否放进仓库 | 内容 |
|---|---|---|
| .env | 不放 | 真实的值 |
| .env.example | 放 | 键名,以及假值或说明 |
| .gitignore | 放 | 排除 .env 和 credentials 文件夹的设置 |
| credentials 文件夹 | 不放 | 密钥文件等 |
放一个 .env.example,就能公开需要哪些键,同时把值藏起来。
不过 .gitignore 有个坑。正如 git 官方文档所说,它对已经被跟踪的文件不起作用。提交过一次的文件,需要用 git rm --cached 取消跟踪。
和盘点工具一样,把通信费这类固定支出也盘点一遍,整体开销可能会更清楚。
用实际值比对的自制检查
这是这次最想分享的部分。扫描工具是按“格式”去找的,所以我再补上一种按自己手里的“值”去找的方法:检查旧 .env 里的值有没有出现在仓库中。
- 从旧 .env 中只取出值的部分,做成每行一个值的文件
- 去掉空行,以及太短容易误伤的值(如 true 或 1)
- 值列表放在仓库外面
- 用
grep -F按固定字符串(而非正则)进行比对 - 对 git 历史也做同样的比对
假设值列表放在 ~/private/values.txt,示例如下:
# 比对整个工作区(排除 .git)
grep -rnF -f ~/private/values.txt --exclude-dir=.git .
# 比对历史中的所有提交
git grep -nF -f ~/private/values.txt $(git rev-list --all)
加上 -F 后,含特殊符号的值也会被当作普通字符串查找。注意值列表里如果有空行,会匹配到每一行。
值列表要放在“不会公开的地方”。比对完之后删掉,或者放回加密的位置会更安心。
替换脚本搞出了重复替换
动手过程中还踩了一个坑,发生在用替换脚本改写约70个文件的引用时。
有3处新字符串里包含了旧字符串。比如把“config”替换成“config_shared”这种情况,替换跑两遍就会变成“config_shared_shared”。
我先全部还原,再重新做了一遍。教训很简单:替换之前,先确认“新字符串里有没有包含旧字符串”。如果包含,最好加上边界条件、只替换一次。
重做之后,做了加载确认的18个文件全部正常。
密钥进入历史后的处理顺序
万一把密钥提交了,顺序很重要。GitHub 官方文档要求首先吊销并重新签发密钥(轮换)。
| 顺序 | 要做的事 | 原因 |
|---|---|---|
| 1 | 吊销并重新签发密钥或密码 | 旧值失效后,损失就到此为止 |
| 2 | 把代码改成从 .env 读取 | 避免把新值又硬编码进去 |
| 3 | 必要时用 git filter-repo 改写历史 | 从历史中清除旧值 |
| 4 | 强制推送,必要时联系 GitHub 支持 | 清除缓存和 Pull Request 中的引用 |
官方文档指出,有时仅做轮换就足够了。因为改写历史会改变提交哈希,而且残留在 fork 里的内容也删不掉。
改写历史用的工具是 git filter-repo,需要支持 --sensitive-data-removal 的版本(2.47及以上)。替换值的写法如下:
git-filter-repo --sensitive-data-removal --replace-text ../passwords.txt
只要在公开仓库里出现过一次的密钥,就应当默认已经被别人拿到了。官方给出的顺序是:先吊销密钥并换成新的,再去清理历史。
常见的坑
- 把 gitleaks 的“no leaks”当成没有任何密钥的证明
- 只用了
gitleaks git,忘了检查还没提交的文件 - 明明写进了 .gitignore,已被跟踪的文件却还留着
- 把值列表或替换列表放在了仓库里面
- 替换的新旧字符串部分重叠,导致重复替换
- 只改写了历史,却忘了轮换密钥
第一条就是我这次的经历。扫描工具非常靠得住,但它看的只是“格式”。和按值比对结合起来用,会更放心一些。
不光定下规则、还要实际数一数来确认的话题,我在数了164个任务后发现的事里也写过。检查密钥大概也是同样的道理。
开始之前要确认的事
- 是否已列出哪些文件里有多少个密钥
- 是否已把 .env 和密钥文件集中到一处
- .gitignore 里是否包含 .env 和 credentials 文件夹
- 是否已用 git rm --cached 移除已被跟踪的密钥文件
- gitleaks 的 git 和 dir 两种模式是否都跑过
- 是否已用旧 .env 的值列表比对工作区和历史
- 值列表是否放在仓库外面
- GitHub 的 Push protection for users 是否已开启
我的公开仓库计划,目前还在进行中。
常见问题
Q. gitleaks 显示“no leaks”,就可以放心公开了吗?
对于能从格式上认出来的密钥,基本可以放心。但短代码或内部文件名之类有可能检测不到,建议同时用值列表做比对。
Q. gitleaks detect 和 gitleaks git 有什么区别?
detect 是在 v8.19.0 被弃用的旧命令。现在检查历史用 git,检查文件夹用 dir。旧命令仍然能用,但不会出现在帮助信息里。
Q. 个人的公开仓库也能用 Secret Scanning 吗?
可以。官方文档写明,公开仓库会免费自动运行。除了所有分支的历史,Issue 和 Pull Request 的正文也在检查范围内。
Q. Push Protection 需要自己设置吗?
按账户生效的 Push protection for users 默认是开启的,会拦截向公开仓库推送含密钥的内容。可以在设置的“Code security”里查看状态。
Q. 在 .gitignore 里写上 .env 就够了吗?
如果还没提交过,这样就不会被跟踪了。但对已经被跟踪的文件不起作用。需要先用 git rm --cached 移除,再确认历史里有没有残留。
Q. 不小心提交了密钥,第一步该做什么?
先吊销并重新签发密钥或密码。至于要不要改写历史,之后再判断。
参考的官方资料
- gitleaks(GitHub 仓库·README)
- About secret scanning(GitHub Docs)
- About push protection(GitHub Docs)
- Push protection for users(GitHub Docs)
- 从仓库中删除敏感数据(GitHub Docs)
- git-filter-repo(GitHub 仓库)
- gitignore(Git 官方文档)
- pre-commit(官方网站)
总结
gitleaks 在查找能从格式上认出来的密钥方面,确实非常可靠。但另一方面,没有格式特征的值,可能会藏在“no leaks”的背后。
所以,扫描工具和按值比对结合起来用比较好。就像排查重复订阅一样,多一个视角,看到的东西就不一样。在按下公开按钮之前,不妨先打开手边的 .env 看一眼。
本文包含联盟营销广告。如果您通过本站链接申请相关商品或服务,我们可能会从合作企业获得报酬。此外,运营者是乐天集团的员工,也可能通过员工推荐计划获得报酬。文章内容与评价均基于运营者的真实体验与调查完成,与是否存在广告无关,但请您在阅读前了解上述关系。详情请参阅免责声明与联盟营销说明。 免责声明与联盟营销说明
本文包含机器翻译内容。正式条件请以乐天移动官方网站的多语言页面为准。
如果您对乐天移动的通信区域·信号状况有疑问,可以通过官方的 电波改善调查申请表 进行咨询。