Align architecture, protocol, API, storage, security and deployment documentation with default mutual measurements and the Agent link board.
199 lines
19 KiB
Markdown
199 lines
19 KiB
Markdown
# 零信任设计
|
||
|
||
> 实现状态:协议方向声明表和卡口、拨号方 自己生成密钥、预握手敲门、操作员集合与创世固定、服务器身份和安全链路、属主固定都已实现;测量数据的产生者签名、入库配额、拨号方 拨号地址限制等还没做(第 3 节有逐项状态)。
|
||
|
||
Agent、Backend、拨号方 之间互相不信任。这份文档回答三个问题:现在谁听谁的、被攻破之后最坏会怎样、怎么用密码学把信任从 Backend 身上拿走。账户、能力、passkey 和入网信任链的具体做法见 [access.md](access.md);节点能力边界、本地硬上限和签名更新见 [architecture.md](../design/architecture.md) 第 5 节,这里不重复。
|
||
|
||
## 1. 范围
|
||
|
||
要防的
|
||
|
||
- Backend(或 Backend 前面的 Cloudflare Tunnel、frp、Caddy 等中间环节)被攻破或被劫持之后,给节点下发恶意内容。
|
||
- Agent 或 拨号方 自己被入侵之后,去影响 Backend 或对端。
|
||
|
||
有意不做的
|
||
|
||
- 进程沙箱(seccomp、pids 和内存上限、出站白名单)。Agent 的定位是 1 核 1 GB 的小机器,不能吃资源。
|
||
- 供应链的多签、冷却期和可复现构建。代码风险靠 PR 评审降一个等级;发布仍然是单把发布密钥签名(见 [deployment.md](../ops/deployment.md))。
|
||
- 顺带记一条:Agent 的单元挂了 `/:/host/root:ro`、`/proc`、`/sys`,被攻破的 Agent 能遍历宿主机。它们多半是不必要的(宿主网络模式下容器自己的 `/proc` 就是宿主的,磁盘用量可以对已挂载的 `/etc/os-release` 做 `statvfs`),属于沙箱范畴,先不动。
|
||
|
||
## 2. 威胁模型
|
||
|
||
| 编号 | 假设被攻破的是 | 现实来源 |
|
||
| --- | --- | --- |
|
||
| T1 | Backend,或它前面的中间环节 | 链路经过 Cloudflare Tunnel、frp、Caddy,它们都能看到 wss 的明文 |
|
||
| T2 | 某个 Agent(在 VPS 上,旁边还有别的服务) | VPS 被入侵 |
|
||
| T3 | 某个 拨号方(在客户内网里) | 客户机器被入侵 |
|
||
| T4 | 管理员的会话或令牌 | 令牌泄露 |
|
||
| T5 | 管理员的浏览器 | XSS,或者节点上报的字符串被渲染 |
|
||
|
||
## 3. 缺口与状态
|
||
|
||
```text
|
||
CI 发布私钥(单把)
|
||
│ 签名清单
|
||
管理员 ──令牌/会话──▶ ┌──────────┐ ◀── 原始块、事件、主机数字
|
||
│ Backend │
|
||
└──┬───┬───┘
|
||
任务集、对端公钥、 │ │ Agent 名单(host:port,拨号方 全部照拨)
|
||
重启计划、更新清单 ▼ ▼
|
||
┌──────┐ ┌──────┐
|
||
│Agent │◀─▶│拨号方 │ 拨号方 私钥由节点自己生成
|
||
└──────┘ └──────┘
|
||
```
|
||
|
||
| # | 缺口 | 状态 |
|
||
| --- | --- | --- |
|
||
| G1 | 节点不验证 Backend 的身份,只靠 TLS 加公共 CA | 已解决:操作员集合里登记服务器密钥,节点要求服务器在认证挑战里签名,链路用应用层加密([access.md](access.md) 第 8 节)。对 Backend 自己被攻破没有帮助 |
|
||
| G2 | 拨号方 的私钥由 Backend 生成 | 已解决:拨号方 自己生成密钥,用加入令牌注册,私钥只在节点上(破坏性:旧 `agent.json` 失效) |
|
||
| G3 | 拨号方 会拨 Backend 给的任意 `host:port` | 未解决:被攻破的 Backend 能让客户内网里的 拨号方 去探测内网;本地 `limits` 只有速率、pps 和 `allowed_target_cidrs` |
|
||
| G4 | Agent 的 TCP 端口对任何人完成 TLS 握手并出示证书 | 已解决:预握手敲门,见第 8 节(破坏性:旧 拨号方 被拒绝) |
|
||
| G5 | Backend 入库只限制了单块的大小,没有按节点的速率或配额 | 未解决:恶意节点可以灌满磁盘 |
|
||
| G6 | 对端公钥、Agent 地址全由 Backend 决定 | 未解决:操作员集合不包含成员名册,被攻破的 Backend 能把恶意 拨号方 加进 Agent 的白名单,或把 拨号方 指向恶意 Agent |
|
||
| G7 | 方法的方向和角色没有统一声明 | 已解决:声明表和卡口,见第 6 节 |
|
||
| G8 | 测量数据没有产生者签名(块只有 CRC32C) | 未解决:中间环节可以改写测量数据;被攻破的 Agent 可以伪造 拨号方 侧的时间戳 |
|
||
| G9 | 入库只核对了 `agent_id`,不核对块里的 `series`(`line:<dialer_id>`) | 未解决:被攻破的 Agent 可以为从没连过它的 拨号方 灌入假线路数据 |
|
||
| G10 | 没有合理性检查(`EchoOps` 不检查回显时间戳的一致性) | 未解决:恶意 Agent 可以谎报 `t2`、`t3` |
|
||
| G11 | 拨号方 的一把密钥多处使用 | 已解决:面向 Backend 和面向对等面的是独立的密钥,用途前缀做域隔离 |
|
||
|
||
没有问题的部分:UDP 端口对未认证的包静默丢弃,不回应(`DropInvalid`,有测试);拨号方 本身没有 TCP 监听;任务有本地硬上限;更新清单有签名和 `seq` 防回滚。
|
||
|
||
## 4. 信任锚点
|
||
|
||
改变信任的内容由 Backend 之外的密钥授权,Backend 只搬运、计算和存储:
|
||
|
||
```text
|
||
① 发布密钥(CI) ──▶ 「跑什么代码」 清单签名、seq 防重放
|
||
② 操作员密钥(管理员 passkey)──▶ 「谁是操作员、哪个是合法的 Backend」 操作员集合,Backend 持有不了
|
||
③ 节点自己的密钥 ──▶ 「我是我」 Agent 都自己生成
|
||
|
||
被攻破的 Backend 能做的:拒绝服务、看数据、改成员名册和任务(G3、G6)。
|
||
不能做的:改操作员集合、冒充节点、塞恶意更新(发布清单要 CI 签名,节点的更新器还会再验)。
|
||
```
|
||
|
||
## 5. 入网信任链
|
||
|
||
详细做法见 [access.md](access.md),这里只留结论:
|
||
|
||
- 节点安装时固定创世指纹和属主;节点注册时先验操作员集合链(创世指纹、passkey 签名、属主在集合里),验不过就不交出密钥。之后只认从这个创世出发的新版本,节点持久化见过的最大版本,旧版本不可回放。
|
||
- 操作员集合里还登记了 Backend 的服务器密钥(G1)。
|
||
- 加节点不需要 passkey:创建加入码是普通操作,命令里只有公开的值;自动化用带 `fleet.enroll` 的 API key。信任来自节点注册时固定的创世指纹和属主。
|
||
- 没有采用逐节点的「成员证明」链(Grant、Ticket、Binding):被攻破的 Backend 往名册里塞节点,拿不到比 Backend 已经有的更多的东西。所以名册(G6)仍然由 Backend 决定。
|
||
|
||
## 6. 协议的方向与平面
|
||
|
||
### 6.1 三个平面,各认各的凭证
|
||
|
||
```text
|
||
外部面(人、浏览器、脚本) /api/public(匿名只读,SSE) /api/admin(Cookie + CSRF,或 API 令牌)
|
||
/install /openapi /healthz /metrics
|
||
节点面(节点主动拨入) /api/agent/v1/enroll(加入令牌,一次性) /api/agent/v1/rpc(wss,节点签名认证)
|
||
对等面(拨号方 拨 Agent) TLS 控制 7443/tcp、数据 7443/udp、7444/tcp(先过敲门)
|
||
```
|
||
|
||
- 每个平面只认自己的凭证:节点面不接受令牌和 Cookie;外部面不接受节点签名;对等面只认敲门通过的成员。
|
||
- 连接方向固定:节点面永远是节点拨入 Backend,Backend 从不主动连节点,所谓「下发」只是在节点拨入的连接上发下行消息;对等面永远是 拨号方 拨 Agent,Agent 不拨 拨号方。
|
||
- 外部面不能直通节点面:管理端的操作(例如发起测试)只会触发 Backend 里一个强类型的下行方法,管理端不能指定方法名,也不能把请求体原样转给节点(`IAgentGateway` 里全是强类型方法)。
|
||
- 三个平面共用同一个监听端口和同一条 ASP.NET 管线,靠路径和凭证类型区分。把节点面和外部面做成可以分别配置监听地址(默认仍同一个),这样管理面可以只对内网或 VPN 开放,是待定的目标。
|
||
|
||
### 6.2 每个方法声明方向、类型和角色
|
||
|
||
同一个方法名只允许一个发起方,不存在「双向方法」。一个方法声明:发起方(`B→A`、`B→P`、`A→B`、`P→B`、`P→A`、`A→P`)、类型(请求有响应、通知无响应)、角色(`A`、`P`、`A+P`)、授权(无,或为 extension 预留的 `operator`)。方向总表见 [protocol.md](../design/protocol.md),以代码里的声明为准。
|
||
|
||
强制方式:
|
||
|
||
1. 一份声明表,单一真源:框架的 `fleet.*` 在 `Fleet.Rpc/MethodTable.cs` 的 `FleetMethodTable`,`moshiro.*` 和两条二进制通道在 `MoShiro.Protocol` 的 `MoShiroMethodTable`;角色是 `NodeRoles`(服务端按节点 ID 前缀判断)。声明表按(平面,名字)查找,同名方法在两个平面里可以有不同的发起方(`fleet.state.sync` 在节点面是 Backend→节点,在对等面是 Agent→拨号方)。
|
||
2. 卡口在 `RpcRouter`:创建时必须给出本侧(`RpcSide`)和声明表。注册处理函数时核对方向、类型、角色,不符就在启动时失败;分发时拒绝没有声明、方向不对、请求和通知用反了、角色不允许的消息,回 `fleet_protocol_violation`;发送前核对本侧有没有资格发起(违反是程序缺陷,抛异常)。
|
||
3. 违规处理:`RpcSession` 和服务端的 `NodeConnection` 对违规计数,超过上限(`MaxViolations`,默认 20)就断开连接;二进制通道也在服务端按声明核对。
|
||
4. 响应匹配:只接受对应未决请求 ID 的响应,不接受主动「响应」。
|
||
5. 对账测试:`Fleet.Tests/MethodDirectionTests` 覆盖卡口本身;`MoShiro.Backend.Tests/ProtocolDeclarationTests` 核对「每个方法常量都声明了」「文档里写了每个方法」「各角色该处理的方法都注册了处理函数」「每条声明的通道都有收件箱」。
|
||
6. 下行消息是建议:节点按本地硬上限执行或拒绝;任何带副作用的敏感能力都不走核心的下行消息,见第 9 节。
|
||
|
||
### 6.3 各类下行消息怎么处置
|
||
|
||
| 下行内容 | 授权 | 被攻破的 Backend 最多能做什么 |
|
||
| --- | --- | --- |
|
||
| 操作员集合新版本 | 操作员 passkey 签名,节点自己验 | 什么都改不了;能转发旧的合法版本,版本号递增防回放 |
|
||
| 更新清单 | 发布密钥(CI) | 不能伪造;能转发旧的合法清单,`seq` 防回滚;节点的更新器还会用本机公钥再验 |
|
||
| 成员名册、对端公钥、Agent 地址 | Backend(G3、G6 未解决) | 能把恶意 拨号方 加进 Agent 的白名单,或把 拨号方 指向恶意 Agent |
|
||
| 任务集、测试请求 | Backend 计算;节点本地硬上限兜底 | 只能在本地速率和端口上限内发测量流量 |
|
||
| 重启计划(`moshiro.schedule`) | Backend;节点本地启动后 10 分钟内不触发 | 最多让节点每 10 分钟重启一次 |
|
||
| 未来的 `extension`(exec 等,尚未实现) | 属主 passkey 签名的一次性能力令牌,见第 9 节 | 设计目标是没有属主签名就无法调用 |
|
||
|
||
## 7. 节点自己被攻破时(设计,未实现)
|
||
|
||
### 7.1 测量数据的可信(G8、G9、G10)
|
||
|
||
测量数据是这个项目的产出,不能只靠传输层可信:链路有 Cloudflare 这样的中间环节,Agent 又要转发 拨号方 的数据。
|
||
|
||
- 产生者签名:每个列块由 拨号方 签名,每个原始块由上传它的节点签名;Agent 转发 拨号方 的块时原样保留签名。签名覆盖块的摘要(SHA-256),签名串带用途前缀 `chunk` 和节点 ID。Backend 入库时逐个验签,不通过的丢弃并计数。每 10 秒一块,一次 ECDSA 验签的开销可以忽略。
|
||
- 归属校验:入库时核对块里的 拨号方 是和这个 Agent 有线路的成员,不是就丢弃并计数。
|
||
- 合理性检查:`t3−t2` 必须小于 `t4−t1`,各时间戳单调;违反的样本不计入指标,记为异常并产生事件。
|
||
- 边界:这样中间环节改不了数据,被攻破的 Agent 改不了 拨号方 一侧的数据;但它仍然可以谎报自己一侧的 `t2`、`t3`,合理性检查只能限制它的幅度,因为这一侧的真值只有它自己知道。
|
||
|
||
### 7.2 对 Backend 的资源和输入
|
||
|
||
- 按节点的配额:每个节点的字节、块数和速率上限,只是计数,不占资源;全局再限制同时解码的块数(单块解码上限是 64 MiB)。
|
||
- 节点上报的字符串:`hello` 里的 `platform`、事件的 `detail` 等一律当不可信数据,入库前统一校验长度、字符集和换行,不依赖前端转义;管理端不渲染成 HTML。
|
||
- 匿名公开接口:登录已经有限流,但热力图、分布这类重查询只有范围和指标数的上限,要加每源限速和结果缓存。
|
||
- webhook:实现之前先定约束:只允许 HTTPS、禁止私网和回环地址、不跟随重定向、限制超时和响应大小、解析之后再检查地址。
|
||
|
||
### 7.3 对对端和身份
|
||
|
||
- 被攻破的 拨号方 只能拿到 Backend 授权的会话,Agent 的速率上限由本地兜底;被攻破的 Agent 发来的数据只会影响它自己的线路。
|
||
- 拨号方 只拨公网地址(G3):固定 SPKI 只能防止建立会话,防不了探测本身;要在 拨号方 本地拒绝私网、回环、链路本地地址。
|
||
- 名册里的主机名解析之后再检查地址:签名保护的是字符串,不是解析结果;默认只放行公网地址,内网需要节点本地显式允许。
|
||
- QUIC 会话迁移要做路径验证:协议允许会话在 NAT 映射变化后跟随新的源地址,路径上的攻击者可以借此把会话牵到自己的地址。迁移前先做一次挑战应答,验证前限制发送量(不超过收到字节数的 3 倍),由 QUIC path validation 提供。
|
||
- 不协商、不降级:现在固定 ECDSA P-256 和固定的签名串格式,没有算法协商,保持这一点。
|
||
- Agent 的 `max_total_mbps` 默认是 1200,对 1 核 1 GB 的小机器没有意义,却是被攻破的 Backend 能让 Agent 向某个 拨号方 打出去的上限。默认值应该保守,需要的人再调高;`allowed_target_cidrs` 默认为空是对的。
|
||
|
||
## 8. 监听隐身:预握手敲门
|
||
|
||
目标:Agent 的 TCP 端口对没有完成挑战的人不写出任何字节,包括 TLS 的 ServerHello,扫描器拿不到证书和 TLS 指纹(G4)。UDP 端口本来就是静默的。
|
||
|
||
```text
|
||
拨号方 Agent TCP 7443 / 7444
|
||
│── TCP 连接 ───────────────────────────────────▶ accept(内核已完成三次握手)
|
||
│── 敲门包 32 字节:nonce(16) │ tag(16) ─────────▶ 读 32 字节(限时 3 秒)
|
||
│ tag = HMAC(K_pair, 时间窗 ‖ nonce) 用名册里每个 拨号方 的 K_pair 逐个核对
|
||
│ ├ 合法:继续 TLS 握手 → 现有的签名认证
|
||
│◀──────────── TLS ServerHello ─────────────────── └ 不合法或超时:什么都不发,直接断开
|
||
```
|
||
|
||
- `K_pair = HKDF(ECDH(拨号方 身份私钥, Agent 身份公钥), "moshiro-knock-v1")`:只有名册里的成员算得出;时间窗 30 秒,接受前后各一个窗;`nonce` 在窗内去重,防重放;每个来源地址限速(默认每来源每 10 秒 300 次)。
|
||
- 实现:`Fleet.Identity.Knock`(构造与核对)和 `KnockGate`(读 32 字节、名册、nonce 去重、按来源限速);控制端口(`PeerListener`)和数据端口(`TcpAcceptor`)共用同一个关卡,拨号方 一侧 `PeerConnector` 和 `TcpLink` 在 TCP 连接建立后、TLS 之前发敲门包。名册是期望状态里的 `peers`(拨号方 的对等面公钥);`dial_targets` 下发的是 Agent 的 `public_key`(Agent 在 hello 里上报)。陌生连接得到的是被关闭的连接,没有任何字节。
|
||
|
||
局限:TCP 的 SYN-ACK 由内核发出,用户态进程拦不住,所以端口在扫描器眼里仍是「开着,但不说话」,不是「没监听」。真正的没监听有两条路:等级二的 UDP 单包授权(拨号方 先发签名的 UDP 敲门包,Agent 才临时开 TCP 监听几秒,只放行敲门者的源地址;代价是多一个往返、UDP 丢包重试、多出口源地址可能对不上),或宿主防火墙规则(需要 root,不适合小机器,不做)。先做等级一,等级二可选。
|
||
|
||
## 9. extension:敏感能力的归宿
|
||
|
||
当前 Agent 的核心只包含测量与上报。敏感能力(exec、web ssh 和终端、文件读写、抓包等)计划做成 extension,由用户选择安装,默认不启用,目前都没有实现。
|
||
|
||
- 独立进程:单独的二进制和镜像,和核心之间没有进程内接口,核心不加载外部代码、不用反射和动态加载,核心的 NativeAOT 和构建期限制不受影响。
|
||
- 授权只认节点的属主:每次调用的能力令牌必须由这个节点绑定的属主 passkey 签名,账户角色不赋予权力;管理员只能删除节点或卸载 extension。交互式会话端到端加密,Backend 只转发密文。属主和会话密钥的设计见 [access.md](access.md) 第 9 节。
|
||
- 独立身份和发布:extension 有自己的密钥、自己向 Backend 注册,在名册里是单独的成员(记录它依附的节点 ID);独立的镜像和清单(`component` 不同,例如 `ext-ssh`),装不装、升不升级都不影响核心。
|
||
- 独立命名空间:extension 的方法只能是 `ext.<名字>.*`,同样进方向声明表;核心的声明表里没有 `ext.*`,核心收到会拒绝。
|
||
- 本地显式启用:extension 的本地配置里有 `enabled`、允许的操作员凭证、允许的命令集或目标范围,默认关闭。
|
||
- 设计约束:不在核心里加入 exec、文件写或加载 extension 的能力;extension 不能绕过核心的本地硬上限。
|
||
|
||
## 10. 密码学原语
|
||
|
||
| 用途 | 现状 |
|
||
| --- | --- |
|
||
| 身份签名 | ECDSA P-256 加 SHA-256(BCL) |
|
||
| 数据面逐包认证 | HMAC-SHA256 截断到 16 字节(`MeasurementDatagram`、`TcpWire`) |
|
||
| 会话密钥派生 | HKDF-SHA256(`SessionKeys`) |
|
||
| 控制链路加密 | AES-256-GCM(每个方向一把密钥,严格递增的计数器) |
|
||
| 令牌哈希、SPKI 指纹 | SHA-256 |
|
||
| 原始块完整性 | CRC32C,不是密码学校验 |
|
||
|
||
BLAKE3 的评估(已定:不采用):ECDSA P-256 本身用 SHA-256,令牌哈希和 SPKI 指纹也继续用 SHA-256,所以 BLAKE3 是多一个哈希族,不是换掉一个;BCL 里没有,Agent 不引用 NuGet 包,只能自己实现几百行的便携版本加官方测试向量,这是要长期维护的密码学代码;托管实现没有 SHA-NI 加速,在 x86 上未必更快。若以后在目标硬件上实测发现逐包 MAC 是瓶颈,再单独评估,届时只替换 `Fleet.Identity` 里 `Mac` 和 `Kdf` 两个静态函数,调用方不动。
|
||
|
||
## 11. 还没定的事
|
||
|
||
1. 敲门等级二(UDP 单包授权)要不要做。
|
||
2. 节点面和外部面是否拆成两个监听端口:拆的话部署文档、Caddy 和隧道的配置都要跟着改。
|
||
3. extension 的第一个具体实现做不做 web ssh,放在哪个阶段;静态数据私钥来源(WebAuthn PRF 还是浏览器里不可导出的密钥)和会话 AEAD 的选择。
|
||
4. 测量数据签名放进块格式的哪个位置,是否影响现有 `MSRC` 容器版本。
|
||
5. Agent 的 `max_total_mbps` 默认值调到多少(比如 100)。
|