# Reality 密钥主控托管(Key Hosting) Reality 密钥对由**控制面**生成、保存和下发,节点只是落地执行。这样节点重装 系统后重新部署的仍是同一把密钥,客户端订阅里的 `pbk`(publicKey)永不变化, 客户无需重新复制配置。 ## 数据模型 - 控制面数据库 `nexusgate.json` 新增顶层对象 `keyStore`: `{ "reality-": { "privateKey": "...", "publicKey": "...", "updatedAt": "..." } }` - 密钥格式与此前一致:Node JWK 导出的 x25519 密钥对,`privateKey`/`publicKey` 为 base64url 无填充(43 字符),与 `sing-box generate reality-keypair` 输出一致。 - 旧数据库没有 `keyStore` 时自动补为空对象,不影响启动。 ## 协议变更 ### 下发(控制面 → Agent) `apply_resource` 任务的 payload 新增可选字段: ```json { "resource": { "...": "inbound 里仍是 ${REALITY_PRIVATE:reality-} 占位符" }, "kind": "node", "keyMaterial": { "reality-": { "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 = { "": { 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-"]` 一并移除。 ## 恢复语义 节点重装系统 → 重新注册 agent → 主控重新下发节点资源,payload 携带原 keyMaterial → agent 落地同一私钥 → sing-box 配置与重装前一致,客户端 订阅(pbk + shortId)逐字不变,客户无感。