Files
nodeagent/docs/SECURITY.md
T
jkooe e841fbb187 feat(v22): 第二批安全加固 —— 每客户端独立 PSK + 发现广播最小化
接着 v21 的两件 P0,补齐第二批清单里的两项(对应原建议 ③/④ 的一半)。

## ① 每客户端一把 PSK(`agent.json` 的 `keys{}`)—— 让 ACL 的身份维度真正有效
**问题**:psk 模式下 `client_id` 由客户端自报(server.ts 原注释自认),
一把共享 key 意味着**持有者可冒充任意 client_id**,ACL 的身份维度形同虚设。

**做法**:按 `client_id` 分配独立钥匙;未登记者回落到共享 `key`(旧部署行为不变)。
比 ed25519 更轻量(不用管公钥),同样是「一把一个身份」。

**实测**(五场景全对):
| 场景 | 结果 |
|---|---|
| alice 用自己的 key | ✅ |
| **alice 的 key 冒用 bob 身份** | ❌ E_AUTH_FAILED ← 核心安全性质 |
| bob 用自己的 key | ✅ |
| 未登记 carol 用共享 key | ✅(向后兼容) |
| 已登记 alice 用共享 key | ❌ 拒绝(专属优先,不被共享 key 绕过) |

配套:`nodeagent keygen --psk --id mac_02` 生成专属 key 并打印可直接粘贴的 agent.json 片段;
鉴权日志带 `key=scoped|shared-fallback|shared`,审计区分「专属钥匙失败」与「回落共享失败」。

## ② 发现广播最小化(`discovery.detail` + `discovery.secret`)—— 收情报泄漏
**问题**:UDP 广播未认证,默认明文告知 IP/端口/认证方式/键鼠开关,等于给攻击者一份目标清单。

**做法**:
- `detail: "minimal"` → 顶层只剩 `service / version / node_id / ts`,
  真实信息收进 `detail{}`(**实测顶层已无 host/port/auth_mode/input_enabled**)
- 配 `secret` → `detail` 带 HMAC 签名(`node_id|ts|JSON(detail)`),
  控制端 `discover --discover-secret <同一密钥>` 验签 → 列表标 **🔒 已认证**;
  未配/验签失败标 **⚠️ 明文**(明示「仅作参考、不可当凭据」)
- 配了 secret 自动启用 minimal;未配置时启动横幅**醒目告警**提示收敛

**实测**三视角:正确密钥 🔒 / 无密钥 ⚠️ / 错密钥 ⚠️(验签失败正确降级)✓

## 验证
- 单测 159 → **162**(新增签名往返一致性:错密钥/篡改详情/改时间戳均须验签失败)
- e2e **29** 全绿;本轮改动为纯加法(新增可选字段),未破坏既有行为
- 文档 `SECURITY.md` 新增 §4.5 / §4.6,并把「发现最小化」从待办划掉

## 剩余待办(SECURITY.md §5)
握手失败封禁 / 审计链签名与外部锚定 / 会话上限与空闲断开 / 安装期密钥改用文件传递 /
ed25519 仍非默认(v22 的 `keys{}` 已是更轻量的等价物)
2026-10-05 02:42:12 +08:00

6.4 KiB
Raw Permalink Blame History

安全模型与加固指南

面向使用者与运维者。协议细节见 DEVELOPMENT.md,发版与升级见 VERSIONING.md。

1. 信任模型:钥匙 = 那台机器的 root

system.shell.exec 能执行任意命令,所以拿到预共享密钥(PSK)就等于拿到被控端的 root。 请按 root 密码的标准保管它:不外传、不进版本库、不进公开安装包、不贴进聊天/工单。

被控端首启时用 CSPRNG(crypto.getRandomValues,32 字节 / 256 bit)在本机生成密钥, 只存在 <数据目录>/agent.json,不进任何分发包。

2. 认证做了什么(已具备)

机制 说明
挑战-应答 hello → challenge(nonce) → auth(HMAC / Ed25519 签名),钥匙全程不上网
nonce 一次性 用后即废(server.ts 置 null),抓包重放无效
协议主版本校验 不一致直接 E_PROTOCOL_MISMATCH 拒连
能力级 ACL deny → allow → 默认拒绝(身份维度仅在 ed25519 模式有效)
调用限速 按 client_id 滑动窗口
审计链 每条含前一条哈希,可验篡改
键鼠控制 默认禁用,需显式开启

⚠️ PSK 模式的固有限制:client_id 由客户端自报,ACL 的「身份」维度挡不住 持有同一把钥匙的人伪装成任意 client_id。要真正按身份授权,请用 ed25519 (nodeagent keygen → 公钥登记到被控端 ACL → --auth-mode ed25519)。

3. v21 加固一:证书指纹钉住(TOFU)

自签证书没有 CA 链,历史上只能 insecure 接受任意证书 —— 于是同网段的中间人可以 透明中继(拿不到钥匙,但能读到截屏/剪贴板/键入内容)。现在:

  1. 被控端在 install.ps1 输出里打印 TLS 证书指纹(sha256 of DER)
  2. 控制端首次连接时把它钉进 ~/.nodeagent/config.json(nodes.<名>.cert_sha256)
  3. 之后每次连接严格比对,不一致直接拒绝:
✗ E_CERT_MISMATCH: 证书指纹不匹配,拒绝连接:期望 2ed4957b… 实际 ca8dc20c…
   (被控端可能重装了、或中间人冒充)

带外核对(重要):装机时屏幕/终端上会印一个 64 位指纹,与控制端首次连接打印的 🔐 已钉住被控端证书指纹 比对 —— 不一致说明你连的不是那台机器。

被控端重装系统 / 证书重建后指纹必然改变,此时显式重钉:

nodeagent connect <地址> --port 8765 --key <密钥> --insecure --forget-cert

查看当前状态:nodeagent info(会显示「已钉住 … 本次一致 ✓ / 未钉住」)

4. v21 加固二:来源网段白名单(allow_from)

被控端默认监听 0.0.0.0(为了能远程接入)。这意味着机器一换网络就自动暴露 —— 连到咖啡厅/酒店/便利店的 Wi-Fi 时,同网段任何人都能看到它的 8765 端口。

在被控端 agent.json 里限制来源:

{
  "host": "0.0.0.0",
  "port": 8765,
  "allow_from": ["192.168.0.0/16", "10.0.0.0/8"]
}
  • 支持 CIDR、单 IP、以及 *(放行全部)
  • 未配置 = 放行全部(保持向后兼容);nodeagent info 会提示尚未收敛
  • 拒绝发生在业务握手之前(不浪费 TLS/挑战应答开销),并记 net.deny 审计
  • 判定按前缀比特而非字符串相等 —— Node 在 IPv6 监听上把 IPv4 客户端报成 ::ffff:a.b.c.d,已做归一化

4.5 发现广播最小化(v22)

UDP 广播是未认证的:默认配置下同网段任何人都能看到 「哪台机器可接管 / IP / 端口 / 用什么认证 / 键鼠是否开着」。

"discovery": { "detail": "minimal", "secret": "<与控制端相同的密钥>" }
  • detail: "minimal":顶层只留 service / version / node_id / ts, 真实信息收进 detail{}(同网段看不到地址、端口、认证方式、键鼠开关)
  • 配了 secret:detail 带 HMAC 签名(node_id|ts|JSON(detail)), 控制端用同一密钥验签 → 列表里显示 🔒 已认证;没配/验签失败显示 ⚠️ 明文(仅作参考)
  • 控制端:nodeagent discover --discover-secret <同一密钥>
  • 未配置时 agent 启动会打印醒目告警,提示收敛

仍会暴露「这里有一台可被接管的机器」(存在性无法隐藏,除非关广播或改用 Hub)。 真正的身份确认始终发生在握手之后(auth_ok 带证书指纹,控制端严格比对)。

4.6 每客户端一把 PSK(v22,psk 模式下的身份绑定)

psk 模式里 client_id 由客户端自报,所以一把共享 key 意味着持有者可冒充任意 client_id, ACL 的身份维度形同虚设。v22 起支持按 client_id 分配独立钥匙:

# 控制端:为某个 client_id 生成专属 PSK
nodeagent keygen --psk --id mac_02
// 被控端 agent.json(可并列多个 client_id)
{ "keys": { "mac_02": "<那把密钥>" } }

效果(已实测):

场景 结果
alice 用自己的 key ✅ 通过
alice 的 key 冒用 bob 身份 ❌ E_AUTH_FAILED
bob 用自己的 key ✅ 通过
未登记的 client_id 用共享 key ✅ 通过(旧部署行为不变)
已登记的 client_id 用共享 key ❌ 拒绝(专属钥匙优先,不被共享 key 绕过)

5. 还没做的(后续批次)

项 说明
发现报文最小化 ✅ v22 已做(见 §4.5)
ed25519 默认化 新装仍默认 psk;v22 提供了更轻量的替代:keys{} 每客户端一把 PSK(见 §4.6)
握手失败封禁 目前只计数 + 审计,无按 IP 的临时拉黑
审计链签名 哈希链可被「有 root 的攻击者」整链重写,缺外部锚定
会话上限 无最大连接数与空闲断开
安装期密钥传递 install.cmd 用命令行 -Key 传参,同机其他用户 ps 可见

6. 应急处置

场景 处置
怀疑钥匙泄漏 换钥匙(被控端删 agent.json 的 key 后重启 → 首启重新生成并打印),或改用 ed25519
证书被换 / 重装后连不上 nodeagent connect … --forget-cert 重钉指纹
怀疑被中间人 用 install.ps1 打印的指纹做带外核对;不一致立即断网排查
机器要带去不可信网络 事先配 allow_from,或直接不接入陌生网络