提示

返回博客列表

key 差点被提交上去

git diff 滚到一半,一行 sk- 开头的东西跳出来。

那一下确实有点凉。手比脑子快,先 git checkout -- .env.example,然后才坐回去看清楚:是我在改示例文件时,为了让同事知道格式,顺手把生产 key 粘进去当"参考",想着待会儿替换成占位符。待会儿没来,差点就 git add . 了。

没有提交,没有推送,key 没进历史。技术上什么都没发生。但我盯着那行看了好一会儿,因为这条路径太顺了——顺到下次未必有人正好在提交前滚一遍 diff。

为什么它会出现在能被提交的地方

项目里密钥是从环境变量读的:

AI_CHAT_API_KEY = config('AI_CHAT_API_KEY', default='')
AI_CHAT_BASE_URL = config('AI_CHAT_BASE_URL', default='https://api.deepseek.com')

config() 会先找环境变量,再找项目根目录的 .env。仓库里只有 .env.example,.gitignore 里明确排除了 .env。这套本身没毛病。

毛病在 settings.py 那行注释——它自己就写着,这个文件随代码入库,密钥直接填在引号里有泄露风险,得从环境变量读。注释是对的,可 .env.example 是个"示例",人在改它的时候心理防线最低,觉得反正是假的。

而且我们那阵子同时开着好几个环境的终端:本地调试、预发、生产。AI_CHAT_API_KEY 在三个地方长得一模一样,复制的时候根本分不清手里这串是哪个。

第一件是提交前扫描。加了个 pre-commit 钩子,对暂存区做一遍特征匹配:

PATS = [
    r'sk-[A-Za-z0-9]{20,}',
    r'(?i)(api[_-]?key|secret|password)\s*[:=]\s*[\'\"][^\'\"]{16,}',
]

命中就拦下,让人自己确认。它拦不住所有情况,但能拦住"顺手粘贴"这一类,而这类恰恰是最常见的。钩子在本地,不是服务端强制,谁都能 --no-verify 绕过去——我知道,但拦自己比拦别人有用。

第二件是把那串 key 作废重发。哪怕没进历史,也当成泄露处理。这一步比加钩子麻烦,因为要动三个环境的配置。

第三件是分环境:开发、预发、生产各一个 key,服务商侧分别设用量上限。开发那个限额压得很低,就算哪天真的漏出去,损失有天花板。以前图省事共用一个,出了事只能整体轮换。

第四件才是轮换流程本身。听着简单,真做的时候卡在两处。一是 gunicorn 进程读的是启动时的环境变量,改了 .env 不重启不生效,滚动重启那几十秒里新旧 key 会并存;二是旧 key 撤销早了,还没滚完的 worker 会集中报 401,被 except Exception 吞成"降级为留言",监控上看不出是配置事故,只看到 AI 突然不回答了。

后来定的是:先上新 key、滚动重启、观察十分钟调用成功率和 401 计数、确认没有旧 key 的流量了,再去控制台撤销。整个过程得有人在旁边盯着,没法全自动。

服务商侧的用量告警到现在没设。也就是说,如果某个 key 真的漏出去被人拿去刷,我们最早发现的时机仍然是月度账单,或者某天接口突然开始大面积超时。这跟前面做的所有事情比起来,是最该做、也最不费劲的一件,偏偏一直没排上。

还有个更琐碎的:那三个 key 现在记在团队的密码库里,名字是 ai-chat-dev、ai-chat-staging、ai-chat-prod。上次有人问"哪个是生产",我愣了半秒才敢答。命名是清楚的,不确定的是自己有没有记反。

VidDown 的部署文档里记过一次密钥轮换,比这篇啰嗦。

那串作废的 key 我没留。倒是那次 git checkout 之后,我在终端前面愣了大概半分钟,什么也没敲。

想亲手试试?用 VidDown 一键解析下载

粘贴视频链接即可解析,多平台支持、网页端即用;下载桌面客户端解锁海外平台本地解析,开通会员更享不限次下载。

顶部