OmniRoute CLI Machine-ID Token 全解析:HMAC 本机认证、盐值轮换与 Loopback 安全边界

发布时间:2026/9/14 7:00:38
OmniRoute CLI Machine-ID Token 全解析:HMAC 本机认证、盐值轮换与 Loopback 安全边界
OmniRoute CLI Machine-ID Token 全解析HMAC 本机认证、盐值轮换与 Loopback 安全边界【免费下载链接】OmniRouteNever stop coding. Free MIT AI gateway: one endpoint, 352 providers (150 free), 1200 models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Cline Copilot. Quota-aware auto-fallback, RTKCaveman compression saves 15-95% tokens, MCP/A2A, Desktop/PWA. Built by 550 contributors项目地址: https://gitcode.com/GitHub_Trending/om/OmniRouteOmniRoute 的 CLI 子命令如omniroute status、omniroute providers不需要用户每次手动输入 JWT 或密码靠的是一枚基于硬件 machine ID 派生的 HMAC 令牌。本文以 CLI Token 安全文档英文版见 docs/security/CLI_TOKEN.md为主体完整讲清这枚令牌的派生、传输、服务端校验流程并结合仓库源码拆解其常量时间比较、loopback 边界判定、盐值轮换与 legacy 格式兼容等安全细节读完后你可以独立排查 CLI 认证失效问题并正确执行令牌轮换。一、机制概览为什么需要 Machine-ID TokenOmniRoute CLI 命令通过HMAC-SHA256(machine-id, salt)令牌对本地 management API 进行认证令牌通过请求头x-omniroute-cli-token传递。这一机制的价值在于CLI 子命令omniroute status、omniroute providers等可以调用 management 端点而无需用户在每次调用时提供 JWT 或密码。令牌与硬件 machine ID 绑定是确定性的、不可逆的、且与本机绑定——本机进程可自动算出正确值远程调用者则无法伪造除非拿到同一台机器的 machine ID 和盐值。二、令牌派生getMachineTokenSync 的完整流程1. 派生四步走getMachineTokenSync()通过node-machine-id读取硬件 machine ID失败时回退为空字符串等效于禁用 CLI 认证。计算HMAC-SHA256(machine_id, salt)返回完整的 64 字符十六进制摘要——一个确定性的、不可逆的、与本机绑定的令牌。CLI 将令牌作为x-omniroute-cli-token发送到http://localhost:port/api/...。服务端src/server/authz/policies/management.ts用相同盐值重新计算期望令牌并通过timingSafeEqual比较防止基于时序timing的令牌提取攻击。2. 服务端派生实现src/lib/machineToken.ts从 src/lib/machineToken.ts 源码可以看到派生细节核心函数deriveMachineToken(rawId, salt)src/lib/machineToken.ts#L22-L25直接执行createHmac(sha256, rawId).update(salt).digest(hex)——注意 HMAC 的 key 是 rawId、消息是 salt而非简单拼接模块加载node-machine-id时通过createRequire(process.argv[1])将运行时解析锚定到进程入口点避免打包器改写createRequire(import.meta.url)后无法加载外部 CommonJS 包的问题任何加载失败都会让machineIdSync回退为() 即“派生不可用 → 空令牌 → CLI 认证禁用”machineIdSync(true)参数表示取原始未哈希的硬件 IDgetMachineTokenSync()src/lib/machineToken.ts#L38-L54内部带有cached/cachedSalt缓存盐值不变时重复调用直接返回缓存结果盐值变化则自动失效重算——这正是盐值轮换能即时生效的底层机制默认盐值为omniroute-cli-auth-v1源码常量BUILTIN_DEFAULT_SALTsrc/lib/machineToken.ts#L16。3. CLI 侧派生实现bin/cli/utils/cliToken.mjs打包后分发的 CLI 在 bin/cli/utils/cliToken.mjs 中维护一份与源码镜像一致的派生逻辑deriveCliToken(machineIdModule, salt)同样调用machineIdSync(true)取原始 ID再执行crypto.createHmac(sha256, rawId).update(salt).digest(hex)源码注释记录了一个真实踩坑#10148node-machine-id是 CommonJS 包在await import()下其导出落在.default上直接解构machineIdSync会得到undefined异常被 catch 吞掉后令牌变成空串导致所有 management 请求静默无认证 401。现在的写法按machineIdModule?.machineIdSync || machineIdModule?.default?.machineIdSync的顺序做互操作兼容getCliToken()带盐值感知的缓存_cached/_cachedSalt派生失败时打印[CLI_TOKEN] machine-id resolution failed, CLI auth disabled调试日志而不是静默失败。CLI 端还遵循更严格的发送纪律见英文版文档只有当解析出的目标是显式 loopback URLlocalhost、127.0.0.0/8或 loopback IPv6时才会携带该令牌携带令牌的请求使用redirect: error本地重定向无法把令牌转发到其他 origin。远程上下文则改用 scoped access token。若派生不可用CLI 直接省略该请求头并由omniroute doctor上报失败而不是把空令牌当成有效值。三、服务端校验链从策略层到中间件1. 策略层判定peerContext.tsmanagement 策略中真正执行令牌校验的是hasValidLoopbackCliToken()src/server/authz/peerContext.ts#L69-L80其判定顺序是OMNIROUTE_DISABLE_CLI_TOKEN true→ 直接返回 false机制整体关闭非 loopback 请求 → false。这里注意loopback 判定不是看Host头该头完全由客户端控制、可伪造而是看服务端真实 TCP peer 地址打上的可信 locality 标记requestPeerAddress()读取经OMNIROUTE_PEER_STAMP_TOKEN校验的PEER_IP_HEADER戳记或直连时的 socket peer无x-omniroute-cli-token头 → false构建expectedTokens [getMachineTokenSync(), getLegacyCliTokenSync()].filter(Boolean)逐一做长度先行 timingSafeEqual常量时间比较任一匹配即通过。校验通过的请求会被打上本地 CLI 主体标记LOCAL_CLI_SUBJECTkind: management_key,id: cli,label: local-cli-tokensrc/server/authz/peerContext.ts#L83-L87然后在 management 策略里完成放行src/server/authz/policies/management.ts#L216-L218if (hasValidLoopbackCliToken(ctx)) { return allow({ ...LOCAL_CLI_SUBJECT }); }请求头常量CLI_TOKEN_HEADER x-omniroute-cli-token统一定义在 src/server/authz/headers.ts#L25。2. 中间件层判定cliTokenAuth.ts对于 PUBLIC 分类但仍调用requireManagementAuth()的路线如GET /api/monitoring/healthsrc/lib/middleware/cliTokenAuth.ts 提供独立的isCliTokenAuthValid()校验同样先检查OMNIROUTE_DISABLE_CLI_TOKENisLocalCliRequest()的本地性判定分四级真实 socket peer单元测试/直连场景→ 存在转发头cf-connecting-ip/x-forwarded-for/x-real-ip则判为经代理、非本地 → 只信任 authz 管道打上的x-omniroute-peer-locality戳记 → 无任何可信 locality 信号则 fail closed令牌比较同样是expectedTokens [getMachineTokenSync(), getLegacyCliTokenSync()].filter(Boolean) 常量时间比较src/lib/middleware/cliTokenAuth.ts#L74-L91。这套“绝不从Host头推导 locality”的设计与 src/server/authz/headers.ts 中对PEER_IP_HEADER、VIA_PROXY_HEADER的注释一致任何客户端自带的值都会在管道处理前被删除防止远程调用者伪造本地来源。四、安全属性总览属性细节Loopback-only仅当服务端可信 peer-locality 戳记源自真实 TCP peer 地址判定为 loopback 时才接受。客户端可控的Host头从不用于 locality 判定。Constant-time comparecrypto.timingSafeEqual防止时序攻击。Non-reversibleHMAC 输出无法反推出 machine-id。Noalways-protected bypassisAlwaysProtectedPath()在 CLI 令牌检查之前求值/api/shutdown和/api/settings/database始终要求 JWTCLI 令牌不能绕过。Non-exportable令牌从不写入磁盘也不记入日志。关于第四行的“保护层级优先级”从 src/server/authz/policies/management.ts#L253-L256 可以看到isAlwaysProtectedPath(path)的判定先于requireLoginfalse的匿名放行分支执行而/api/shutdown、/api/settings/database等路径被显式列入 src/server/authz/routeGuard.ts 的ALWAYS_PROTECTED_API_PATHS清单routeGuard.ts#L128-L157。完整的三层路由保护模型Tier 1 LOCAL_ONLY / Tier 2 ALWAYS_PROTECTED / Tier 3 MANAGEMENT详见 ROUTE_GUARD_TIERS 文档。五、盐值轮换Salt Rotation设置环境变量OMNIROUTE_CLI_SALT即可在不改任何代码的情况下轮换派生令牌。轮换后本机所有 CLI 进程会自动使用新令牌得益于第二节所述的盐值感知缓存。适用场景进程列表泄漏可能暴露了之前的派生值之后。# 持久化轮换加入 shell profile export OMNIROUTE_CLI_SALTmy-secret-salt-2026 # 验证新令牌已生效 omniroute status默认盐值omniroute-cli-auth-v1。盐值在 CLI 侧与服务端侧的读取逻辑完全一致——两侧都执行process.env.OMNIROUTE_CLI_SALT || BUILTIN_DEFAULT_SALTCLI 侧见 bin/cli/utils/cliToken.mjs#L11-L13 的getActiveSalt()服务端见 src/lib/machineToken.ts#L18-L20。只要两侧进程环境中的盐值同步令牌就能自动对齐这也是轮换只需改环境变量的原因。六、Legacy 格式SHA-25632 字符——仍被接受在上述 HMAC 格式之前CLI 派生令牌的方式是SHA-256(machineId salt).hex[0..32]取前 32 字符前缀实现位于 bin/cli/utils/cliToken.mjs 对应的服务端函数getLegacyCliTokenSyncsrc/lib/machineToken.ts#L56-L64其核心deriveLegacyCliToken见 src/lib/machineToken.ts#L27-L33。为向后兼容服务端同时接受两种格式校验器构建expectedTokens [getMachineTokenSync(), getLegacyCliTokenSync()]对传入请求头逐一执行timingSafeEqual比较实现见 src/server/authz/policies/management.ts 与 src/lib/middleware/cliTokenAuth.ts 的hasValidLoopbackCliToken/isCliTokenAuthValid。因此只要令牌匹配64 字符的 HMAC 摘要或32 字符的 legacy SHA-256 前缀中任意一个即视为有效。显式关闭Opt-out设置OMNIROUTE_DISABLE_CLI_TOKENtrueenv 或.env可完全禁用 CLI 令牌机制此后所有访问都要求显式 API key。在多用户主机上建议启用该开关因为machine-id是 per-device设备级而非 per-user用户级同一主机上的其他用户可以算出相同的令牌。七、相关文件索引文件职责src/lib/machineToken.ts令牌派生getMachineTokenSync、getLegacyCliTokenSyncsrc/server/authz/headers.tsCLI_TOKEN_HEADER常量与可信 locality 头定义src/server/authz/policies/management.tsmanagement 策略层的服务端校验src/server/authz/peerContext.tshasValidLoopbackCliToken、isLoopbackRequest等共享判定src/server/authz/routeGuard.tsisLoopbackHostloopback 判定与三层保护路径清单bin/cli/utils/cliToken.mjs打包 CLI 的令牌派生getCliTokensrc/lib/middleware/cliTokenAuth.tsPUBLIC 路线的 CLI 令牌中间件校验八、测试用例中的行为保证tests/unit/cli-machine-token.test.ts 用可执行断言锁定了上述设计承诺CLI 与服务端派生一致性设置随机盐值后断言打包 CLI 的getCliToken()输出与服务端getMachineTokenSync(salt)完全相等且匹配/^[0-9a-f]{64}$/盐值轮换可达 CLI删除与设置OMNIROUTE_CLI_SALT后两次派生结果必须不同若 CLI 侧硬编码盐值而忽略 env该测试会失败派生不可用 → 空串deriveCliToken在模块缺少machineIdSync、返回空 ID 或抛异常时均返回与文档“回退为空字符串、禁用 CLI 认证”的描述一一对应纯 node 环境的 64 字符断言测试通过execFileSync(process.execPath, ...)在纯 node而非 tsx loader下运行 CLI 模块专门回归 CJS 互操作 bug 修复后 token 长度为 64 的契约缓存稳定性重复调用getCliToken()返回相同值。九、延伸阅读ROUTE_GUARD_TIERS — 路由保护三层模型CLI 令牌的 loopback 判定是其 Tier 1 的一部分AUTHZ_GUIDE — 完整授权管道理解 CLI 令牌校验在整个 authz 流水线中的位置docs/security/CLI_TOKEN.md — 本文主体文档的英文原版包含 CLI 仅向显式 loopback 目标发送令牌、redirect: error防转发等补充细节。【免费下载链接】OmniRouteNever stop coding. Free MIT AI gateway: one endpoint, 352 providers (150 free), 1200 models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Cline Copilot. Quota-aware auto-fallback, RTKCaveman compression saves 15-95% tokens, MCP/A2A, Desktop/PWA. Built by 550 contributors项目地址: https://gitcode.com/GitHub_Trending/om/OmniRoute创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考