首页
/ claude-mem Server 安全机制解析:API 密钥认证、local-dev 回环旁路与密钥存储模型

claude-mem Server 安全机制解析:API 密钥认证、local-dev 回环旁路与密钥存储模型

2026-09-06 15:09:02作者:仰钰奇

Claude-Mem 的 server 运行时默认采用 API 密钥认证来保护记忆读写接口,其安全文档 security.md 定义了三层关键约定:默认 API-key 认证、local-dev 回环开发旁路的启用条件,以及 BullMQ 队列模式下的存储边界。读完本文,你可以理解 claude-mem 中认证模式的环境变量组合、cmem_ 前缀 API 密钥的生成/存储/校验全链路,以及部署时避免将服务端口裸暴露在公网的具体约束,并能结合仓库源码定位到每一项安全约定的实现位置。

认证模式总览:默认 api-key,显式才可降级

SettingsDefaultsManager 的默认值可以确认,CLAUDE_MEM_AUTH_MODE 的出厂默认值是 api-key,即 server beta 运行时开箱即要求每个请求携带密钥,没有任何隐式放行路径。

认证中间件 requireServerAuth 的密钥提取逻辑为:

  • 首选 Authorization: Bearer <key>(canonical 形式);
  • 回退到 X-Api-Key: <key> 请求头,以兼容使用 @better-auth/api-key 默认约定的客户端(例如 Windows-canary 线发布的 worker bundle)。

Bearer 解析由 parseBearerToken 完成,采用 /^Bearer\s+(.+)$/i 正则并做 trim 处理。缺失密钥时中间件直接返回 401 及提示 Missing API key (Authorization: Bearer <key> or X-Api-Key: <key>);密钥无效或权限不足则返回 403 Invalid API key or insufficient scope

local-dev 回环旁路:双环境变量 + 三重网络判据

安全文档明确了一条强约束:CLAUDE_MEM_AUTH_MODE=local-dev 只有在同时设置 CLAUDE_MEM_ALLOW_LOCAL_DEV_BYPASS=1才启用回环开发旁路,且不得置于反向代理之后或绑定在公网可达的地址上。源码中这个"双开关"设计在 auth.ts 体现为五个必须同时满足的条件:

  1. 请求未携带任何 API 密钥(!rawKey);
  2. authMode === 'local-dev'(来自环境变量,默认 api-key);
  3. CLAUDE_MEM_ALLOW_LOCAL_DEV_BYPASS === '1' 显式打开;
  4. 客户端 IP 是回环地址——isLocalhost 只接受 127.0.0.1::1::ffff:127.0.0.1localhost 四种取值;
  5. Host 请求头是回环主机名——hasLoopbackHostHeader 仅接受 127.0.0.1localhost::1(支持 [::1]:port 这类带方括号的 IPv6 写法);
  6. 不存在任何代理转发头——hasForwardedClientHeaders 会检查 forwardedx-forwarded-forx-forwarded-hostx-real-ip 四个头,只要出现任意一个就拒绝旁路。

正是第 6 条从机制上解释了文档为何警告"不要放在反向代理之后使用":反代会在请求上注入 X-Forwarded-For 等头,本地旁路随之失效,攻击者即使能从外部打进来也无法借道"本地开发模式"免密访问。这套判定逻辑由 tests/server/auth-api-key.test.ts 中的用例逐条验证:localhost 无 Bearer 通过、未显式 opt-in 时返回 401、出现转发头时返回 401、方括号 IPv6 回环 Host 被接受、未设置 CLAUDE_MEM_AUTH_MODE 时默认走 API-key 认证。

旁路通过后,请求上下文被标记为 mode: 'local-dev'scopes: ['local-dev'],与真实密钥路径的 AuthContext 结构(auth.ts#L13-L21)保持同构,下游路由无需感知认证来源。

API 密钥生成:cmem_ 前缀与一次性展示

文档指出 API 密钥以 cmem_ 前缀生成、仅展示一次。生成实现非常简洁,见 createRawServerApiKey

export function createRawServerApiKey(): string {
  return `cmem_${randomBytes(32).toString('base64url')}`;
}

即固定前缀 cmem_ 加 32 字节密码学随机数的 base64url 编码(约 43 字符熵载体),总熵约 256 bit。"仅展示一次"是密钥模型的自然结果——数据库里从不落盘明文,创建接口(createServerApiKey)返回的 CreatedServerApiKey 结构中,rawKey 与入库记录 record 分离,调用方只在创建瞬间拿到明文,之后任何途径都无法再从存储中还原。

密钥存储模型:从 SHA-256 到加盐 scrypt 的演进

security.md 对存储的描述是:"Claude-Mem 只把 SHA-256 哈希、前缀元数据、scopes、状态与时间戳存入 SQLite,不存明文。" 结合源码可以看到这一约定在实现上又向前演进了一步:

新密钥采用加盐、慢速、计时安全的 scrypt 派生hashServerApiKey,对应内部工单 #2541):

const SCRYPT_COST = 16384; // N — memory-hard,必须是 2 的幂
const SCRYPT_KEYLEN = 64;
const SCRYPT_SALT_BYTES = 16;

export function hashServerApiKey(rawKey: string): string {
  const salt = randomBytes(SCRYPT_SALT_BYTES);
  const derived = scryptSync(rawKey, salt, SCRYPT_KEYLEN, { N: SCRYPT_COST });
  return `scrypt$${SCRYPT_COST}$${salt.toString('hex')}$${derived.toString('hex')}`;
}

存储格式为 scrypt$<N>$<saltHex>$<derivedHex>:每把密钥独立 16 字节随机盐(抗彩虹表)、scrypt 的内存硬化特性(N=16384)抬高离线爆破成本、比较使用 crypto.timingSafeEqual 抗时序侧信道(safeEqualHex)。

遗留的无盐 SHA-256 路径被保留但只读hashServerApiKeyLegacySha256 仅用于验证既有旧密钥,不再写入任何新键。verifyRawKeyAgainstStoredHash 通过 scrypt$ 前缀识别存储格式并分派到对应验证分支;当旧密钥被成功验证、明文在手中时,upgradeLegacyKeyHashIfNeeded 会透明地将其升级为 scrypt 格式——即旧密钥在首次使用时自动完成强哈希迁移。

SQLite 表结构与文档所述字段一一对应,见 api_keys 表映射

说明
key_hash 密钥哈希(scrypt 新格式或 SHA-256 旧格式)
prefix 明前 10 字符(rawKey.slice(0, 10)),非机密,用于候选收敛
scopes 权限范围 JSON 数组
status active / revoked
last_used_at_epoch / expires_at_epoch 最近使用时间 / 过期时间(epoch 毫秒)
created_at_epoch / updated_at_epoch 创建与更新时间戳

创建与吊销操作还会写入审计日志(api_key.create / api_key.revoke,见 sqlite-api-key-service.tsL233-L248)。

密钥校验链路:前缀收敛 + 常量时间比较 + 过期与 scope 检查

verifyServerApiKey 的完整校验顺序为:

  1. 候选收敛:加盐哈希不再具有"同一明文恒定哈希"的性质,因此无法直接按哈希查库。校验先用密钥前 10 位前缀(非机密)通过 listActiveApiKeysByPrefix 缩小候选集——本地密钥数量小且前缀区分度极高;
  2. 逐候选常量时间验证:对每个候选执行 scrypt(或 SHA-256)派生并用 timingSafeEqual 比较;
  3. 过期检查expiresAtEpoch 存在且不晚于当前时间即拒绝;
  4. scope 检查hasRequiredScopes 要求授权 scope 覆盖所需 scope,* 通配符可覆盖一切(* 管理员通配是显式 opt-in,默认不授予);
  5. 副作用:命中后透明升级旧哈希、markApiKeyUsed 刷新 last_used_at_epoch

本地 SQLite 后端新建密钥的默认 scope 是 memories:read + memories:writeDEFAULT_LOCAL_API_KEY_SCOPES),因为本地 V1 路由按这两个 scope 分别门控读与写接口;此前"无显式 scope 即 []"会导致默认密钥静默无权访问任何路由,这一修复保证了默认密钥开箱即用。

队列与存储边界:BullMQ 模式下 Redis 只是传输层

security.md 最后一段给出 BullMQ 模式的两条约束:

  • BullMQ 模式依赖 Redis 或 Valkey;队列 payload 被刻意限定为"恢复观察处理所需的最小工作",SQLite 仍然是规范(canonical)记忆存储——即使 Redis 数据丢失,也只是丢失待处理任务,不会丢失记忆本体;
  • 可部署示例应启用 Redis 持久化,且避免在无认证的情况下把 server 端口暴露到公网

这一边界与 CLAUDE_MEM_QUEUE_ENGINE 的默认值 sqliteSettingsDefaultsManager)以及 CLAUDE_MEM_REDIS_URL / CLAUDE_MEM_REDIS_HOST 等 Redis 连接配置项相互印证:默认走 SQLite 队列,显式切到 BullMQ 时才需要外部 Redis/Valkey,且密钥认证(前文所述的 CLAUDE_MEM_AUTH_MODE=api-key 默认路径)是暴露网络边界的最后一道防线。

部署要点小结

把文档约定与源码证据对齐后,可以归纳出 claude-mem server 的部署安全清单:

  1. 生产环境保持默认 CLAUDE_MEM_AUTH_MODE=api-key,为客户端签发 cmem_ 前缀密钥并以 Authorization: Bearer 传递,注意密钥只展示一次;
  2. 本地开发如需免密,必须同时设置 CLAUDE_MEM_AUTH_MODE=local-devCLAUDE_MEM_ALLOW_LOCAL_DEV_BYPASS=1,且服务只绑定回环地址、不加任何反向代理(转发头会使旁路自动失效);
  3. 公网部署绝不可跳过认证直接暴露端口;使用 BullMQ 时为 Redis/Valkey 开启持久化,并理解 SQLite 才是记忆数据的最终落点;
  4. 密钥治理依赖 api_keys 表的 expires_at_epochstatus 与 scopes 列:定期轮换、吊销(revoked 状态即时生效)、按需最小化 scope,避免无谓授予 * 通配。

以上每一条都能在 src/server/middleware/auth.tssrc/server/auth/sqlite-api-key-service.tssrc/storage/sqlite/auth.tstests/server/auth-api-key.test.ts 中找到对应实现与回归测试,可作为进一步审计与二次开发的入口。

登录后查看全文
热门项目推荐
相关项目推荐