对应审查建议的 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 项、应急处置表。
4.5 KiB
4.5 KiB
安全模型与加固指南
面向使用者与运维者。协议细节见
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 接受任意证书 —— 于是同网段的中间人可以
透明中继(拿不到钥匙,但能读到截屏/剪贴板/键入内容)。现在:
- 被控端在
install.ps1输出里打印 TLS 证书指纹(sha256 of DER) - 控制端首次连接时把它钉进
~/.nodeagent/config.json(nodes.<名>.cert_sha256) - 之后每次连接严格比对,不一致直接拒绝:
✗ 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,或直接不接入陌生网络 |