Files
nodeagent/docs/SECURITY.md
T
jkooe 0d59d6032b feat(v21): 第一批安全加固 —— 证书指纹钉住 + 来源网段白名单
对应审查建议的 P0 两条:这两项是**真实可利用的攻击路径**,其余(发现最小化/失败封禁等)
留待后续批次。

## ① 证书指纹钉住(TOFU)—— 挡同网段中间人
自签证书没有 CA 链,历史上客户端只能 `insecure` 接受任意证书 → 攻击者可**透明中继**:
拿不到钥匙(挑战-应答挡得住冒名),但能读到截屏/剪贴板/键入/exec 输出。

- 被控端:`install.ps1` 打印证书指纹(sha256 of DER)供**带外核对**;
  `system.info.build.cert_sha256` 与 `auth_ok.build.cert_sha256` 上报
- 控制端:首次连接自动钉进 `nodes.<名>.cert_sha256`;之后**严格比对**,
  不一致即 `E_CERT_MISMATCH`(-32422) 拒连,并提示 `--forget-cert` 重钉
- 额外做**交叉确认**:实际握手证书 vs auth_ok 自报,不一致同样拒绝(防自报被篡改)
- 指纹基于 **DER**(不是 PEM 字节),保证与 Windows .NET `GetRawCertData()` 同值

**踩坑记录(写进代码注释,避免后人重走)**:
1. `ws` 客户端的 `upgrade` 事件**只传 IncomingMessage**,拿不到 socket
2. socket 要在 `open` 之后从 `ws._socket` 取(TLSSocket)
3. Node ≥15 的 `getPeerCertificate()` 返回**对象**,DER 在 `.raw` 上(老版本才是 Buffer)
   —— 现已双兼容

## ② 来源网段白名单 `allow_from` —— 收敛暴露面
被控端默认监听 `0.0.0.0`(远程接入所需),但**换到不可信网络就自动暴露**。

- 支持 CIDR / 单 IP / `*`;**未配置 = 放行全部**(保持向后兼容,不打断现有部署)
- 拒绝发生在**业务握手之前**(不做 TLS/挑战应答,省资源),并记 `net.deny` 审计
- 判定按**前缀比特**而非字符串相等:Node 在 IPv6 监听上把 IPv4 客户端报成
  `::ffff:a.b.c.d`,已归一化(单测覆盖)
- `system.info.network.allow_from` 上报状态;`nodeagent info` 一眼看出是否已收敛

## 验证(本地真跑,非纸面)
- 白名单不含本机 → 连接被拒(agent 侧 WARN + 审计留痕)✓
- 白名单含本机 → 连上,并**自动钉住**指纹 ✓
- **换掉被控端证书后重连 → `E_CERT_MISMATCH` 拒绝**(期望 2ed4957b… / 实际 ca8dc20c…)✓
- 新增 e2e 用例(自起 TLS agent)覆盖上述四种情形 + agent 自报与实际证书一致性 ✓
- 新增单测 9 例(CIDR 前缀匹配 / IPv4-mapped 归一 / IPv6 zone / PEM≡DER / 篡改即变)
- 全量:单测 **159**(150→159)· e2e **29**(28→29)全绿

## 文档
新增 `docs/SECURITY.md`:信任模型(钥匙=root)、认证现状与**已知限制**(ACL 身份维度仅在
ed25519 有效)、本次两项加固的用法、尚未收敛的 6 项、应急处置表。
2026-10-05 02:25:19 +08:00

4.5 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,已做归一化

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

项 说明
发现报文最小化 UDP 广播目前明文告知 IP/端口/认证方式/键鼠是否开启(可被伪造,仅作发现用)
ed25519 默认化 新装仍默认 psk,ACL 的身份维度默认无效
握手失败封禁 目前只计数 + 审计,无按 IP 的临时拉黑
审计链签名 哈希链可被「有 root 的攻击者」整链重写,缺外部锚定
会话上限 无最大连接数与空闲断开
安装期密钥传递 install.cmd 用命令行 -Key 传参,同机其他用户 ps 可见

6. 应急处置

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