先给结论
大模型 API Key 应当只存在于服务端的受控运行环境中。环境变量可以避免把密钥写进代码,但它不是完整的密钥管理方案;AES-GCM 等静态加密可以降低配置文件泄露的风险,但如果密文、主密钥和应用权限同时失守,攻击者仍可能取得明文。真正有效的防护来自分层控制:服务端隔离、最小权限、独立主密钥、日志脱敏、用量告警、定期轮换和可执行的泄露处置流程。
本文以 Node.js 和 Docker 容器部署为例,目标不是宣称“绝对安全”,而是缩小密钥暴露面,并让泄露能够更快被发现和止损。
先画清 API Key 的安全边界
典型调用链应该是:
浏览器或移动端 -> 自己的 Node.js 后端 -> 大模型 API
前端只携带本站登录态或短期会话凭证,不能收到上游模型的 API Key。以下位置都不适合保存长期密钥:
- 前端 JavaScript、HTML、Source Map 和移动端安装包;
- Git 仓库、镜像构建参数、Dockerfile 的
ENV指令; - 可公开下载的配置文件和对象存储;
- 错误堆栈、请求日志、分析平台和聊天记录;
- 为排错而保存的完整 HTTP 请求头。
即使前端做了混淆或加密,只要浏览器最终需要解密并发送密钥,访问者就能从运行时或网络请求中取得它。正确做法是由后端代理调用,并在本站接口增加身份验证、限流、参数白名单和额度控制。
环境变量能解决什么,不能解决什么
Node.js 从环境变量读取密钥很简单:
const apiKey = process.env.LLM_API_KEY;
if (!apiKey) throw new Error('缺少 LLM_API_KEY');
它能避免密钥进入源码,也方便部署系统注入不同环境的配置。不过,环境变量仍可能被同权限进程、调试工具、容器配置查看权限或错误诊断信息读取。不要把 process.env 整体打印到日志,也不要把生产环境的 .env 文件提交到仓库。
容器场景中还应注意:
- 不要在 Dockerfile 中使用
ENV LLM_API_KEY=...,否则密钥可能进入镜像层或构建记录; - 优先使用部署平台的 Secret、Docker Secret 或外部密钥管理服务,在运行时挂载或注入;
- 限制谁能查看容器详情、进入容器、读取 Secret 挂载目录和拉取生产镜像;
- 开发、测试和生产使用不同密钥,避免一个低权限环境牵连生产。
环境变量是交付密钥的一种通道,不应被视为保险箱。
静态加密与主密钥如何分离
如果管理后台需要持久保存多个模型配置,可以使用认证加密算法加密 API Key,例如 AES-256-GCM。认证加密不仅隐藏明文,也能发现密文或附加数据被篡改。每次加密应生成新的随机 IV,并同时保存认证标签。
下面是一个简化示例:
import crypto from 'node:crypto';
const ALGORITHM = 'aes-256-gcm';
export function encryptSecret(plaintext, masterKeyBase64) {
const key = Buffer.from(masterKeyBase64, 'base64');
if (key.length !== 32) throw new Error('MASTER_KEY 必须解码为 32 字节');
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
return {
version: 1,
algorithm: ALGORITHM,
iv: iv.toString('base64'),
tag: tag.toString('base64'),
ciphertext: ciphertext.toString('base64'),
};
}
实现时还需要做到:
- 主密钥不能与密文放在同一个配置文件或同一 Git 仓库中;
- 主密钥由部署平台 Secret 或 KMS 提供,应用启动时读取;
- 解密只发生在真正发起模型请求的服务进程内,明文不写回磁盘;
- 配置接口只返回
hasKey、末尾少量字符或密钥版本,不返回完整明文; - 为密文记录格式版本,便于以后更换算法或重新加密;
- 主密钥变更必须配套重加密流程,不能直接替换后让旧数据无法解密。
静态加密主要保护备份、磁盘或配置文件被单独复制的场景。它无法抵御已经取得应用执行权限、主密钥和密文的攻击者,因此还必须限制运行身份和后台管理权限。
日志脱敏要在入口完成
只靠开发者“记得不要打印”并不可靠,应在日志基础设施统一过滤敏感字段:
const SECRET_FIELDS = new Set([
'authorization', 'apiKey', 'api_key', 'token', 'secret', 'masterKey',
]);
function redact(value) {
if (Array.isArray(value)) return value.map(redact);
if (!value || typeof value !== 'object') return value;
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
key,
SECRET_FIELDS.has(key) || SECRET_FIELDS.has(key.toLowerCase())
? '[REDACTED]'
: redact(item),
]));
}
还要检查反向代理访问日志、APM、错误监控和任务队列失败记录。对于 Authorization: Bearer ... 这类嵌在字符串中的内容,应增加模式过滤,但不要只依赖正则;优先从源头禁止记录请求头和敏感配置。
安全日志应保留模型名、密钥标识、请求 ID、HTTP 状态、Token 用量、延迟和调用业务,但不能保留完整密钥。密钥标识可以是供应商提供的 Key ID,或内部随机配置 ID,不建议把密钥本身做无盐哈希后公开展示。
用最小权限限制泄露影响
如果模型平台支持项目、角色、额度或来源限制,应为每个环境和业务创建独立密钥:
- 日报总结和后台测试不要共用一把密钥;
- 生产服务只获得所需模型和接口权限;
- 设置单日或单月预算、速率限制和异常用量告警;
- 后台配置接口使用强认证,并记录增删改审计事件;
- 服务进程使用非 root 用户,配置文件只允许该运行身份读取;
- CI/CD 只在必要阶段获得 Secret,避免传给不受信任的脚本或分支构建。
当供应商无法提供细粒度权限时,可以在自己的模型网关实施模型白名单、最大 Token、并发数和业务配额。密钥泄露无法完全避免,但独立密钥与额度控制能显著缩小影响范围。
建立不中断业务的轮换流程
轮换不应只是“删除旧 Key 再创建新 Key”。更稳妥的是双密钥交叠:
- 创建新 Key,权限和额度从最小集合开始;
- 将新 Key 作为新版本写入 Secret 系统或加密配置;
- 重启或热加载一小部分实例,完成连通性和业务校验;
- 逐步切换全部实例,观察错误率、调用量和供应商控制台;
- 确认没有旧 Key 流量后撤销旧 Key;
- 记录操作者、时间、旧新 Key ID、验证结果和回滚窗口。
应用配置可以保存 keyVersion 和 activatedAt,日志只记录版本,不记录密钥。若供应商只允许一把有效密钥,就需要准备短维护窗口和明确回滚路径。
轮换频率应结合供应商能力、暴露风险和团队运维成本制定;比固定天数更重要的是在人员离职、权限变化、仓库误提交、日志误记录或依赖供应链事件后立即轮换。
泄露后的应急处置顺序
发现密钥出现在 Git、日志、截图或未知调用中时,应把它视为已经泄露。仅删除文件或重写 Git 历史不够,因为密钥可能已被复制。建议按以下顺序处理:
- 立即撤销或禁用旧 Key:先止损,不等待原因调查结束;
- 启用备用 Key 并恢复业务:使用预演过的轮换流程,避免把旧 Key 再次上线;
- 检查供应商调用记录和账单:确定异常时间、模型、用量、来源 IP 和请求特征;
- 隔离泄露路径:关闭公开文件、限制日志访问、暂停可疑构建或令牌;
- 搜索扩散范围:检查 Git 历史、镜像层、CI 日志、制品、工单、聊天和备份;
- 保留审计证据:记录发现时间、处置动作和相关 ID,但不要再次复制明文密钥;
- 修复根因并复盘:增加 Secret 扫描、权限控制、脱敏测试和告警;
- 按合同和法规评估通知义务:涉及用户数据或重大费用时交由安全、法务和供应商共同判断。
如果密钥进入 Git 历史,应先撤销密钥,再清理历史;顺序不能反过来。清理历史会影响协作仓库,执行前还要协调所有克隆和镜像副本。
可以落地的检查清单
发布前
- 代码和镜像中不存在真实 API Key;
- 浏览器请求和页面源码中看不到上游密钥;
- 管理接口不会返回已经保存的明文;
- 日志脱敏覆盖请求头、配置对象和异常上下文;
- Secret 文件权限、容器运行用户和后台鉴权已检查;
- 为密钥设置额度、用量告警和独立标识。
运行中
- 按密钥版本监控调用量、429、401/403 和异常地域;
- 定期扫描 Git、镜像与日志中的疑似 Secret;
- 轮换过程有双 Key 验证和撤销确认;
- 备份只包含密文,并验证主密钥恢复方案;
- 应急联系人、供应商入口和操作手册保持可用。
与多模型配置结合
多模型后台通常会保存多个供应商的 Key,风险比单模型更集中。可以为每条模型配置保存供应商、端点、模型名、密文、密钥版本和启用状态;列表接口仅返回 hasKey。轮询或故障转移时,服务端在调用前按配置 ID 解密,调用结束后不缓存到磁盘,也不把请求头传入业务日志。
多模型统一配置方式可参考 OpenAI 兼容 API 多模型统一配置。当密钥失效、额度不足或接口出现 429、超时和 5xx 时,排查与重试策略见 大模型 API 429、超时和 5xx 排查。为调用链增加可追踪但不泄密的指标,可以继续阅读 AI 内容管线可观测性。
常见问题
把 API Key 加密后就安全了吗?
不是。加密能降低密文文件或备份单独泄露的风险,但应用运行时必须能够解密。如果攻击者同时取得应用权限、密文和主密钥,仍可能拿到明文。
.env 文件可以提交到私有仓库吗?
不建议。私有仓库也可能因账号、CI、镜像或权限配置问题泄露。仓库只保留 .env.example,真实值由部署环境的 Secret 系统提供。
多久轮换一次最合适?
没有适用于所有系统的固定周期。应依据供应商能力、业务风险和合规要求设定,并确保任何疑似泄露发生时都能立即撤销和轮换。
总结
大模型 API Key 安全不是选择“环境变量还是加密文件”的单选题。环境变量负责运行时交付,认证加密保护静态配置,独立主密钥降低单点文件泄露风险,最小权限和配额限制损失,日志脱敏与监控帮助发现异常,轮换和应急流程负责快速恢复。把这些环节一起建设,才能让 Node.js 与容器中的模型调用具备可维护的安全边界。
在本地 AI 编程代理中同样不能把长期密钥直接提交到项目配置。关于环境变量占位、会话分享和“权限确认不等于沙箱”的具体案例,可参考 OpenCode 中文教程与安全配置实测。