
这篇讲什么密钥这东西写 demo 时随手贴在代码里等项目上了规模就开始出问题谁也说不清某把 Key 是哪个服务在用日志里躺着完整明文交接时靠聊天工具互传仓库历史里还埋着两年前提交的旧密钥。这篇不讲密钥从哪来只讲拿到之后在工程侧怎么管。按「存放 → 启动校验 → 日志打码 → 分环境隔离 → 作废轮换 → 提交前扫描」六步走每步都有可直接落地的代码。环境准备pip install python-dotenv # 本地读取 .env pip install detect-secrets # 提交前扫描可选第六步用第一步密钥只从环境变量读不进代码先立一条硬规矩任何密钥值不出现在任何被 git 跟踪的文件里。项目根目录放两个文件一个真实的、一个模板.env # 真实值加进 .gitignore永不提交 .env.example # 只有键名没有值提交进仓库.envLLM_API_KEYsk-real-value-here LLM_BASE_URLhttps://your-gateway.example.com/openai/v1 DB_PASSWORDanother-real-secret.env.exampleLLM_API_KEY LLM_BASE_URL DB_PASSWORD.gitignore里务必包含.env .env.* !.env.example注意第三行——先忽略所有.env.*再用!把模板文件放回来。顺序反了模板就传不上去新同事 clone 下来不知道该配哪些键。读取侧from dotenv import load_dotenv import os load_dotenv() # 生产环境没有 .env 文件时静默跳过不报错 api_key os.environ[LLM_API_KEY]load_dotenv()的设计很适合这个场景本地有.env就加载线上没有就什么也不做直接用容器/CI 注入的真实环境变量。同一份代码两种环境都跑得通不需要判断if is_local。第二步启动期就校验别等第一次调用最难查的故障是「服务启起来了跑了二十分钟第一个真实请求进来才报鉴权失败」。密钥缺失属于配置错误应该在进程启动的前几行就暴露。# config.py import os import sys REQUIRED_VARS ( LLM_API_KEY, LLM_BASE_URL, DB_PASSWORD, ) EX_CONFIG 78 # sysexits.h 约定配置错误 def load_config() - dict: missing [name for name in REQUIRED_VARS if not os.environ.get(name)] if missing: print( [fatal] 以下必需环境变量未设置\n - \n - .join(missing) \n请参考 .env.example 补齐后重启。, filesys.stderr, ) sys.exit(EX_CONFIG) return {name: os.environ[name] for name in REQUIRED_VARS} CONFIG load_config()在应用入口第一行from config import CONFIG缺变量就立刻退出。退出码选 78 不是随意的。0 是成功、1 是笼统失败而 78EX_CONFIG在 sysexits 约定里专指配置错误。这样容器编排和 CI 能直接根据退出码判断78 不需要重试因为重启一百次配置还是缺的而网络类失败重试往往有效。区分开之后K8s 的restartPolicy和流水线的重试策略才有意义。补一个校验强度更高的版本顺手查格式def validate_key_format(key: str) - bool: 基本形态校验拦住把整行 LLM_API_KEYsk-xxx 粘进值里这种错。 if in key or in key or key.startswith((, )): return False return len(key) 20引号被一起粘进值里是极高频的错误报出来的是 401但你会盯着密钥本身找半天。第三步日志里的密钥必须打码密钥进日志的路径比想象的多打印配置、打印请求头、异常堆栈带上了完整 URL有些平台的 Key 会出现在 query 里、以及最隐蔽的——把整个 client 对象repr出来。写一个打码函数任何要输出的敏感值都先过一遍def mask(secret: str, keep_head: int 4, keep_tail: int 4) - str: sk-abcd1234...wxyz - sk-a***wxyz if not secret: return empty if len(secret) keep_head keep_tail: return * * len(secret) return f{secret[:keep_head]}{* * 6}{secret[-keep_tail:]} print(f已加载密钥: {mask(CONFIG[LLM_API_KEY])}) # 输出: 已加载密钥: sk-a******wxyz保留头尾几位是有意的足以让你在多把 Key 之间对上是哪一把又不足以复原。全打成星号在排查「线上用的到底是哪把 Key」时会很难受。想更彻底一点给 logging 挂一个过滤器全局兜底import logging import re SECRET_PATTERN re.compile(rsk-[A-Za-z0-9_\-]{16,}) class SecretFilter(logging.Filter): def filter(self, record: logging.LogRecord) - bool: if isinstance(record.msg, str): record.msg SECRET_PATTERN.sub(sk-***REDACTED***, record.msg) if record.args: record.args tuple( SECRET_PATTERN.sub(sk-***REDACTED***, a) if isinstance(a, str) else a for a in record.args ) return True logging.getLogger().addFilter(SecretFilter())这一层是保险不是替代品——正则只认得上它见过的形态。主要防线仍然是「输出前先手动 mask」过滤器负责捞漏网的。第四步按「项目 × 环境」分独立的 Key不要一把 Key 走天下。给每个组合单独申请一把用途环境变量名好处服务 A 生产LLM_KEY_SVCA_PROD用量能归因到具体服务服务 A 测试LLM_KEY_SVCA_TEST压测不污染生产统计本地开发LLM_KEY_DEV泄露只影响开发环境CI 流水线LLM_KEY_CI可单独限流跑挂了不影响线上这么拆有三个直接收益。归因看用量面板就知道是哪个服务在跑不用靠猜。爆炸半径某把 Key 泄露只作废这一把其他服务毫无感知不需要全线停机换密钥。排查线上突然出现异常调用看是哪把 Key 发的就能立刻锁定来源服务。大多数平台的控制台都支持一个账户下创建多把密钥并分别查看明细。接入前确认这项能力是否具备——如果只能建一把 Key上面这套隔离就无从实施这是选型时容易被忽略的一个硬约束。国内常见的接入平台如 jiekou.vip在控制台提供多密钥管理和按密钥查看用量明细的能力接入前在文档里核对一下即可。第五步怀疑泄露就立刻作废重建「先查查是不是真泄露了确认了再换」——这个顺序是错的。正确顺序是先作废再调查。因为作废重建的代价很低改一个环境变量、滚动重启而泄露窗口每多一分钟风险都在累积。等你查清楚别人可能已经用了半小时。标准处置流程控制台把可疑的那把 Key 立即作废创建新 Key只更新对应服务的环境变量滚动重启该服务确认恢复正常然后再慢慢查是提交进仓库了、贴到聊天工具里了、还是从日志导出的查清根因后补上对应的防护比如第六步的扫描第 2 步「只更新对应服务」正是第四步分开建 Key 换来的——如果全公司共用一把这一步就变成全服务停机。关键一点作废旧 Key 之前别忘了确认没有其他服务在共用它。分开建 Key 的项目不会有这个问题如果历史上有共用先 grep 一遍配置仓库确认引用范围。另外密钥交接一律不传值只传「去哪个控制台自己建一把」的说明。任何走聊天工具、邮件、共享文档的密钥明文都等于给自己埋一颗定时炸弹——那些平台的历史记录你无法真正删除。第六步pre-commit CI 双层扫描人总会手滑所以要有机器兜底。两层都要有本地 pre-commit 拦住提交CI 兜住绕过 hook 的情况--no-verify或者直接在网页端编辑。本地 hook.git/hooks/pre-commit#!/bin/sh # 提交前扫描暂存区里的疑似密钥 PATTERNsk-[A-Za-z0-9_-]\{16,\}\|api[_-]\?key[ ]*[:][ ]*[A-Za-z0-9_-]\{16,\} if git diff --cached --name-only -z | xargs -0 -r grep -nIE $PATTERN 2/dev/null; then echo echo [BLOCKED] 暂存区检测到疑似密钥已阻止提交。 echo 请改用环境变量确认是误报可用 git commit --no-verify 跳过。 exit 1 fi exit 0记得chmod x .git/hooks/pre-commit。注意grep加了-I跳过二进制文件否则扫到图片、模型权重会很慢。CI 侧用成熟工具比手写正则可靠得多# .github/workflows/secret-scan.yml name: secret-scan on: [push, pull_request] jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 with: fetch-depth: 0 # 扫全量历史不只是本次改动 - name: Install detect-secrets run: pip install detect-secrets - name: Scan run: | detect-secrets scan --all-files \ --exclude-files \.env\.example$ \ .secrets.new detect-secrets audit --report .secrets.newfetch-depth: 0很关键。密钥的麻烦在于提交进去之后再删掉它依然躺在 git 历史里任何能 clone 的人都拿得到。只扫本次 diff 会漏掉这一大类。扫描器的实用配置是三层叠加正则匹配已知形态sk-前缀这种、熵值检测抓形态未知的随机串、白名单排除.env.example和测试固件里的假值。少了白名单这层误报多到大家很快就开始无脑--no-verify等于没装。一页速查环节做法存放环境变量 .envgitignore.env.example提交启动必需变量缺失即退出退出码 78日志输出前mask()另挂 logging 过滤器兜底隔离按「项目 × 环境」各一把 Key泄露先作废再调查不是先调查再作废交接只传获取方式不传密钥值防护pre-commit 拦提交 CI 扫全量历史小结密钥管理不需要引入什么复杂基础设施以jiekou.vip为例上面六步全部是几十行代码加几个配置文件的量但覆盖了绝大多数事故场景。优先级排序是先做第一步和第二步不进代码、启动校验改动量最小、收益最大有多个服务了就补第四步的隔离团队人数上去了再上第六步的扫描。第三步的日志打码建议一开始就做因为等日志已经写脏了回头清理比当初就打码麻烦得多。