Files
NexusGate/KEYHOSTING.md
T

3.4 KiB
Raw Blame History

Reality 密钥主控托管(Key Hosting)

Reality 密钥对由控制面生成、保存和下发,节点只是落地执行。这样节点重装 系统后重新部署的仍是同一把密钥,客户端订阅里的 pbk(publicKey)永不变化, 客户无需重新复制配置。

数据模型

  • 控制面数据库 nexusgate.json 新增顶层对象 keyStore: { "reality-<nodeId>": { "privateKey": "...", "publicKey": "...", "updatedAt": "..." } }
  • 密钥格式与此前一致:Node JWK 导出的 x25519 密钥对,privateKey/publicKey 为 base64url 无填充(43 字符),与 sing-box generate reality-keypair 输出一致。
  • 旧数据库没有 keyStore 时自动补为空对象,不影响启动。

协议变更

下发(控制面 → Agent)

apply_resource 任务的 payload 新增可选字段:

{
  "resource": { "...": "inbound 里仍是 ${REALITY_PRIVATE:reality-<nodeId>} 占位符" },
  "kind": "node",
  "keyMaterial": { "reality-<nodeId>": { "privateKey": "...", "publicKey": "..." } }
}
  • 控制面在构建部署任务时(rebuildNode / 对账 reconcileServer):节点是 Reality 协议且 keyStore 无此 keyId → 面板现场生成并入库;已有则复用。
  • 出口(node-exit)任务不带 keyMaterial(出口资源没有 Reality 占位符)。
  • /api/jobs 管理接口不返回 payload,私钥不会经管理 API 泄漏。

Agent 行为(Go agent)

  1. 收到 keyMaterial 时,先写入本地 /etc/nexusgate/keys.json(0600), 再做占位符 materialize —— 部署的必然是主控托管的那把钥匙。
  2. 若某个被使用的 keyId 不是本次 payload 提供的(本地已有或本地新生成), 在任务结果里回传:result.artifacts.keyMaterial = { "<keyId>": { privateKey, publicKey } }。
  3. payload 无 keyMaterial(旧控制面)时行为与托管前完全一致。

Node 版 agent 不读 keyMaterial 字段,照旧本地生成/使用密钥,协议兼容; 其任务结果不带 keyMaterial 回传,控制面也不会被它改写(见下)。

回填(Agent → 控制面)

任务成功完成(completeJob)时,控制面合并 artifacts.keyMaterial:

  • keyStore 无此 keyId → 入库(存量节点密钥由此自动迁入主控)。
  • 已有且相同 → 无操作。
  • 已有且不同 → 保留主控的,写一条 reality_key_conflict 审计日志; 下一次下发会把主控密钥重新发给 agent,agent 本地副本随之被纠正。
  • 合并后节点的 artifacts.realityPublicKey 强制以 keyStore 为准, 保证订阅输出稳定。keyMaterial 本身不会存进 node.artifacts。

优先级

主控 keyStore > 本次下发 payload > 节点本地 keys.json

payload 的内容就是 keyStore 的副本,所以前两者永远一致;本地仅在主控 从未托管过该密钥的迁移期短暂生效,回填完成后即以主控为准。

删除清理

节点记录被真正删除时(草稿直接删除、删除任务确认后、服务器强制遗忘), 对应的 keyStore["reality-<nodeId>"] 一并移除。

恢复语义

节点重装系统 → 重新注册 agent → 主控重新下发节点资源,payload 携带原 keyMaterial → agent 落地同一私钥 → sing-box 配置与重装前一致,客户端 订阅(pbk + shortId)逐字不变,客户无感。