原创

大模型 API Key 安全:Node.js 服务端存储、加密、轮换与泄露应急

面向 Node.js 与容器部署,系统说明大模型 API Key 的服务端边界、静态加密、主密钥管理、日志脱敏、最小权限、轮换流程和泄露应急,给出可落地但不过度承诺的安全方案。

大模型 API 生产运维专题 · 第 2/4 篇查看专题目录 →

先给结论

大模型 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'),
  };
}

实现时还需要做到:

  1. 主密钥不能与密文放在同一个配置文件或同一 Git 仓库中;
  2. 主密钥由部署平台 Secret 或 KMS 提供,应用启动时读取;
  3. 解密只发生在真正发起模型请求的服务进程内,明文不写回磁盘;
  4. 配置接口只返回 hasKey、末尾少量字符或密钥版本,不返回完整明文;
  5. 为密文记录格式版本,便于以后更换算法或重新加密;
  6. 主密钥变更必须配套重加密流程,不能直接替换后让旧数据无法解密。

静态加密主要保护备份、磁盘或配置文件被单独复制的场景。它无法抵御已经取得应用执行权限、主密钥和密文的攻击者,因此还必须限制运行身份和后台管理权限。

日志脱敏要在入口完成

只靠开发者“记得不要打印”并不可靠,应在日志基础设施统一过滤敏感字段:

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”。更稳妥的是双密钥交叠:

  1. 创建新 Key,权限和额度从最小集合开始;
  2. 将新 Key 作为新版本写入 Secret 系统或加密配置;
  3. 重启或热加载一小部分实例,完成连通性和业务校验;
  4. 逐步切换全部实例,观察错误率、调用量和供应商控制台;
  5. 确认没有旧 Key 流量后撤销旧 Key;
  6. 记录操作者、时间、旧新 Key ID、验证结果和回滚窗口。

应用配置可以保存 keyVersion 和 activatedAt,日志只记录版本,不记录密钥。若供应商只允许一把有效密钥,就需要准备短维护窗口和明确回滚路径。

轮换频率应结合供应商能力、暴露风险和团队运维成本制定;比固定天数更重要的是在人员离职、权限变化、仓库误提交、日志误记录或依赖供应链事件后立即轮换。

泄露后的应急处置顺序

发现密钥出现在 Git、日志、截图或未知调用中时,应把它视为已经泄露。仅删除文件或重写 Git 历史不够,因为密钥可能已被复制。建议按以下顺序处理:

  1. 立即撤销或禁用旧 Key:先止损,不等待原因调查结束;
  2. 启用备用 Key 并恢复业务:使用预演过的轮换流程,避免把旧 Key 再次上线;
  3. 检查供应商调用记录和账单:确定异常时间、模型、用量、来源 IP 和请求特征;
  4. 隔离泄露路径:关闭公开文件、限制日志访问、暂停可疑构建或令牌;
  5. 搜索扩散范围:检查 Git 历史、镜像层、CI 日志、制品、工单、聊天和备份;
  6. 保留审计证据:记录发现时间、处置动作和相关 ID,但不要再次复制明文密钥;
  7. 修复根因并复盘:增加 Secret 扫描、权限控制、脱敏测试和告警;
  8. 按合同和法规评估通知义务:涉及用户数据或重大费用时交由安全、法务和供应商共同判断。

如果密钥进入 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 中文教程与安全配置实测。

实测与内容说明

实测记录

  • 本文为架构与运维实践说明,未进行供应商密钥平台或攻击防护效果的量化实测
  • Node.js 示例仅展示 AES-256-GCM 加密核心流程,生产接入仍需结合部署平台 Secret、权限和恢复机制审核
  • 已检查正文未包含真实 API Key,未声称静态加密或环境变量可提供绝对安全

参考资料

内容版本 1.1 · 审核:推荐智能手记 · 计划复审:2026-11-13