技术指南
API Key 与 .env 安全:从注入到撤销的可复查边界
从创建、注入、提交、AI 上下文到轮换撤销,建立可复查的 API Key 与 .env 安全边界。
API Key 安全不是把一个字符串藏进 `.env` 就结束,而是一条从创建、注入、读取、日志、提交到吊销的生命周期。先把密钥当作已经会离开当前终端的敏感凭据,再分别限制代码仓库、AI 上下文、远程主机和部署平台的可见范围;一旦公开,先让旧凭据失效,再处理留下的文件和历史。
先画出密钥经过的路径
| 阶段 | 要回答的问题 | 最小证据 |
|---|---|---|
| 创建 | 谁创建、用于哪个项目和环境、能否限制额度/权限? | 凭据类型、项目、到期和负责人 |
| 注入 | 谁把值放进进程,是否会出现在命令行、镜像层或前端包? | 环境变量/Secret 引用与进程边界 |
| 使用 | 哪个后端请求需要它,日志和错误是否会回显? | 脱敏后的状态码、请求 ID 和日志字段 |
| 轮换 | 旧值失效后怎样验证新值,失败如何回退? | 旧/新版本时间窗与验收结果 |
| 撤销 | 删除后仍有哪里可以继续使用? | 仓库历史、CI、缓存、备份和部署清单 |
按环境和权限拆分
本地实验、共享开发机、预览、生产和 CI 不应共用一把万能 Key。优先使用项目级或环境级凭据、最小 scopes、调用额度和到期时间;没有这些能力时,也要用不同凭据缩小一次泄露的爆炸半径。个人 Key 不应作为团队部署凭据,前端代码更不能承载真正的私密凭据。
- 记录凭据所属项目、用途、创建者和下一次轮换日期。
- 能只读就不要授予写入,能限制单一服务就不要使用组织级 Token。
- 预览环境使用单独值,避免测试请求消耗生产额度或读取生产数据。
- 把恢复入口放进密码管理器或平台 Secret,而不是 issue、README 或 shell 历史。
本地项目只提交模板
`.gitignore` 只会阻止尚未被 Git 跟踪的文件进入后续提交;它不是保险箱,也不会从已有历史中删除 `.env`。仓库保留空值模板和变量说明,真实值只在本地进程或受控 Secret 注入。
.env
.env.*
!.env.example
*.local
*.log
# .env.example:只保留变量名和说明
OPENAI_API_KEY=
OPENAI_PROJECT_ID=
# 应用代码只读取运行时环境
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) throw new Error("OPENAI_API_KEY is not configured");提交前审阅暂存区
git status --short
git diff --cached --name-only
git diff --cached
git ls-files | grep -E '(^|/)\.env(\.|$)'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!.git' \"OPENAI_API_KEY|GITHUB_TOKEN|DATABASE_URL|sk-\" .发现 `.env` 已被跟踪时,先停止继续传播并轮换其中的值,再用 `git rm --cached` 修正索引。这个命令只改变当前索引,不会清除过去的提交;是否需要历史重写取决于仓库是否推送、谁持有克隆和平台的清理流程。
历史泄露先撤销再清理
- 在签发平台吊销或轮换旧 Key,并记录旧值不再被接受的时间。
- 确定泄露位置:公开仓库、私有仓库、fork、Actions 日志、构建产物、截图、聊天和备份。
- 通知拥有旧克隆或镜像的协作者;否则历史重写后仍可能再次推回旧对象。
- 按 GitHub 的敏感数据清理流程重写历史并强制更新受影响分支,再重新扫描。
- 替换所有部署、预览、VPS、Compose、systemd 和本地缓存中的旧值。
给 AI 工具最小上下文
Codex、Cursor 和其他 AI 工具可以读文件、运行命令和回显输出。让工具看 `.env.example`、脱敏日志和错误码,不要让它读取真实 `.env`、SSH 私钥、生产日志、完整 Header 或所有环境变量。命令请求也要避免 `printenv`、上传日志和把 Secret 拼进 URL。
- 在仓库和工具的忽略/上下文排除规则中加入 `.env`、密钥目录、备份和日志。
- 要求生成配置时使用变量名与占位符,并在提交前亲自审阅 diff。
- 将 API 响应、错误堆栈和请求头中的 Token 按字段脱敏,而不是只删 `sk-` 前缀。
- 把需要真实凭据的测试放在本地受控终端,不把值复制进对话、issue 或截图。
远程 VPS 按项目隔离
远程开发机增加了磁盘、备份、多人账号和 SSH 权限的风险。每个项目使用独立目录和环境文件,文件至少由服务用户可读;不要把所有凭据塞进共享的 `~/.bashrc`,也不要因为 `chmod 600` 就忽略主机更新、SSH 访问和备份暴露。
install -d -m 0750 /etc/my-app
install -o app -g app -m 0600 /dev/null /etc/my-app/app.env
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519systemd 的 `EnvironmentFile`、Compose 的 `env_file` 和平台 Secret 都只是注入方式,仍要核对服务用户、备份、容器日志和镜像层。不要把真实值写入 unit、Dockerfile、Compose 文件、构建参数或镜像。
CI/CD 只引用平台 Secret
GitHub Actions、Vercel、Cloudflare Pages 和 Netlify 等平台都有环境变量或 Secret 设置。把 Secret 名称放进 workflow,把值留在平台控制面;区分 development、preview 和 production,并确认失败日志、构建产物和部署预览不会回显值。
- 为每个环境建立最小权限凭据和独立轮换窗口。
- 禁止在 workflow YAML、Dockerfile、脚本参数和 URL 查询字符串中写真实值。
- 验证构建后的浏览器资源和 source map 不包含服务器密钥。
- 轮换后用一条不泄露值的健康请求验证新凭据,随后确认旧值返回预期拒绝。
用一次性仓库验证边界
本批在临时目录创建 Git 仓库和占位变量,不连接任何 API,不使用真实凭据。测试先确认 `.env` 被 ignore、`.env.example` 可提交、`git ls-files` 不包含 `.env`,再把一个明确标注为假的占位符提交到临时历史,证明 `git log -S` 仍能找到历史内容;随后删除目录,证明清理动作不依赖全局 Git 配置。
tmp=$(mktemp -d)
trap 'rm -r -- "$tmp"' EXIT
cd "$tmp"
git init -q
printf 'OPENAI_API_KEY=placeholder-only\n' > .env
printf '.env\n.env.*\n!.env.example\n' > .gitignore
printf 'OPENAI_API_KEY=\n' > .env.example
git add .gitignore .env.example
git check-ignore -v .env
test -z "$(git ls-files -- .env)"
git diff --cached --check第二次检查用 `chmod 600` 收紧临时环境文件,并用 `rg` 扫描仓库中允许的变量名。演练只证明本机索引、权限和清理路径;它不能证明第三方平台已经撤销某个真实 Key,也不能替代平台 Secret Scanning、日志审计或轮换演练。
泄露后的验收记录
| 问题 | 必须留下的答案 |
|---|---|
| 旧值何时失效? | 平台、UTC 时间、撤销/轮换操作和返回结果 |
| 哪里出现过? | 仓库提交/fork、日志、截图、CI、镜像、备份和主机 |
| 谁仍可能持有? | 协作者、部署账号、缓存、构建代理和旧克隆 |
| 怎样确认恢复? | 新值最小请求成功、旧值明确拒绝、日志不回显 |
| 怎样避免复发? | 忽略规则、扫描、权限、轮换日期和责任人 |