API key 泄露进 git 的三道防线
一二
一二
后端一二事
2026年8月14日 18:13
北京
在小说阅读器读本章
去阅读
API key 泄露进 git 的三道防线
上周清理代码,差点把 API key 硬编码进 main.go 提交上去。envOr 拦住了,但这事儿让我想明白:防泄露得分三层守。
第一层:构建阶段——别让 key 进源码
envOr 模式
var (
apiBase = envOr(\"API_BASE\", \"https://api.example.com/v1\")
apiKey = os.Getenv(\"API_KEY\") // 不给默认值,强制从环境变量读
)
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != \"\" {
return v
}
return fallback
}
apiKey 为什么不用 envOr?因为它没有合理的默认值。写成 envOr(\"API_KEY\", \"sk-test123\") 那个 fallback 迟早进 git。
.env.local + .gitignore
# .gitignore
.env.local
*.key
*.pem
# .env.local (不提交)
export API_KEY=\"sk-prod-xxxx\"
export API_BASE=\"https://www.haoshuangapi.com/v1\"
# start.sh (可以提交)
#!/bin/bash
set -a
source .env.local
set +a
exec ./docqa \"$@\"
set -a 让 source 的变量自动 export,set +a 关掉,别污染后续命令。
实际数据:我的 docqa 在 8 月 8 日前,API\_KEY 写死在 main.go 第 31 行。改完后 grep -r \"sk-\" projects/docqa/*.go 返回空,密钥彻底出了源码。
第二层:运行阶段——空 key 要炸得响
启动时检查
func main() {
if apiKey == \"\" {
log.Fatal(\"API_KEY 环境变量未设置\")
}
// 启动服务...
}
为什么不在 init() 里检查?log.Fatal 调 os.Exit(1),defer 不执行。后面加了 DB 清理,放 main() 开头安全。
第一次请求时检查
func callEmbedding(ctx context.Context, text string) ([]float64, error) {
if apiKey == \"\" {
return nil, fmt.Errorf(\"API_KEY 未配置,无法调用 embedding\")
}
// 构造请求...
}
两层都检查,因为有些服务只在第一个用户上传文档时才调 embedding。只在启动检查,空 key 可能跑一周才暴露。
实际数据:8 月 8 日排查,API_KEY 环境变量是空的(printenv API_KEY 返回空),程序能跑是因为 main.go 第 31 行有 fallback 默认值 sk-xxx。只靠启动检查不够,调用时也得查。
第三层:代码审查——自动化 + 人工复查
pre-commit hook
#!/bin/bash
# .git/hooks/pre-commit
# 检查疑似 key
if git diff --cached | grep -E \"(sk-[a-zA-Z0-9]{20,}|api[_-]?key.*=.*['\\\"][^'\\\"]{20,})\"; then
echo \"❌ 检测到疑似 API key,请移除后再提交\"
exit 1
fi
这个正则只拦明文 key,拦不住 base64。更安全用 gitleaks[1] 或 truffleHog[2]。
CI 阶段扫描
# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
开发者能 git commit --no-verify 跳过 pre-commit hook,CI 是最后一道。
人工复查
提交前跑:
git diff --cached | grep -i \"key\\|token\\|secret\\|password\"
匹配到了先确认:
- 变量名还是实际值?(
apiKey :=安全,apiKey := \"sk-xxx\"不安全) - 测试数据还是生产数据?(
\"test-key-123\"能提交,真实 key 不行) - 公开文档示例吗?(AWS 文档的
AKIAIOSFODNN7EXAMPLE安全)
实际案例:docqa 的修复
8 月 8 日上午发现 projects/docqa/main.go 第 31 行硬编码了 API key。
修复步骤:
- key 从 main.go 删掉,改成
os.Getenv(\"API_KEY\") - 创建
.env.local,真实 key 写进去 .gitignore里加.env.local- 写
start.sh脚本,source .env.local 后启动 - main() 开头加
if apiKey == \"\" { log.Fatal(...) }
核实方法(任何人可验证):
cd projects/docqa
grep -n \"sk-\" main.go # 返回空
grep -n \"apiKey.*=\" main.go # 只有第 31 行 os.Getenv(\"API_KEY\")
grep -n \"API_BASE\" main.go # 只有 3 处:定义 + 两次拼 URL
一个坑:apiBase 是 envOr(\"API_BASE\", \"https://...\"),看着安全,但拼接是 apiBase + \"/embeddings\",所以 API_BASE 不能带尾斜杠,否则拼成 https://example.com//embeddings(双斜杠)。接入时唯一容易翻车的地方。
三层协同
| 防线 | 拦截时机 | 成本 | 覆盖率 |
|---|---|---|---|
| envOr + .env | 编码时 | 低 | 80% |
| 启动/请求检查 | 运行时 | 低 | 90% |
| hook + CI | 提交/合并前 | 中 | 95% |
为什么三层都要?每一层都有洞:
- 第一层挡不住\"先硬编码测试,忘了删\"
- 第二层挡不住\"提交了但没跑\"
- 第三层挡不住\"base64 绕正则\"
三层叠起来,泄露概率压到接近 0。
总结
密钥永远不该出现在 git 历史里。一旦提交,即使删了,还在 git log,得用 git filter-branch 或 BFG Repo-Cleaner 彻底清,并立即轮换密钥。
三道防线三句话:
- 构建时:密钥从环境变量读,不给默认值
- 运行时:空 key 立即报错
- 提交时:pre-commit + CI + 人工复查
代价:多写 20 行代码,多配 1 个 .env.local,多加 1 个 pre-commit hook。比泄露后的损失(密钥轮换 + 事故复盘 + 可能的账单),值。
引用链接
[1]gitleaks: https://github.com/gitleaks/gitleaks
[2]truffleHog: https://github.com/trufflesecurity/trufflehog
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论