feat: bind each node to the binary it runs at its first handshake; refuse changes that are not in a CI-signed manifest [CI]
ci/woodpecker/push/main-amd64-build Pipeline failed

This commit is contained in:
justme committed 2026-10-08 05:39:13 +08:00
1 parent 117d34a2af
commit 4841f7b15b
40 files changed
+1011 -21

No files matched your search

+26 -1
View File
@@ -32,7 +32,9 @@
**发布密钥可以换**:见 `docs/deployment.md` 的「换发布密钥」;回滚已经由清单的 `seq` 防住(更新器只收比已安装更大的)。
**还没有的**:吊销表;任务和配置内容的操作员签名;节点自证跑的是官方构建;extension 本身和属主的 `controllers`、接收者的实际加密(只是被固定在节点本地,没有行为)。
**节点二进制绑定**(第 16 节):节点第一次握手成功的那一刻,Backend 把它正在运行的二进制的 SHA-256 绑定到这个节点;之后哈希对不上、又不在 CI 签名的更新清单里,就拒绝连接。
**还没有的**:吊销表;任务和配置内容的操作员签名;节点向别人**证明**自己没有撒谎(二进制绑定抓的是文件被换、降级、起了不对的二进制,抓不到故意照抄哈希的恶意二进制,见第 16 节);extension 本身和属主的 `controllers`、接收者的实际加密(只是被固定在节点本地,没有行为)。
## 1. 名词
@@ -480,3 +482,26 @@ curl -fsSL https://panel/install -o /tmp/moshiro-install.sh && echo "<sha256> /
10. 安装脚本的哈希在哪里公布(Gitea 发布页之外还要不要放 DNS TXT 或另一个独立渠道)。
11. 节点预算的数字(常驻内存 ≤ 1 MB、二进制 ≤ 150 KB、每个成员验证 ≤ 100 毫秒、成员上限 256)是否合适;超出预算时优先砍什么(比如先去掉操作员集合的 64 个凭证上限换成更小的)。
12. webhook 和数据拉取的配额默认值(每分钟请求数、每天行数)和事件清单。
## 16. 节点二进制绑定
目的:被换掉的、版本被改的、降级的二进制,不能继续当这个节点运行。
```text
CI 构建:算出镜像里节点二进制的 SHA-256,写进签名的清单(binaries)
宿主机更新器:把验过的清单和签名写进节点只读挂载的 update-status/(installed.manifest、installed.sig)
节点握手(auth 里):binary = 正在运行的二进制的哈希(Linux 读 /proc/self/exe,是进程启动时映射的那个映像)
release、release_sig = 上面那份清单和签名
Backend:
① 第一次握手成功 → 绑定 (节点, 哈希, 清单 seq)
② 以后每次握手:哈希和绑定一致 → 放行
③ 不一致 → 必须是一份 CI 签名清单(用内嵌的发布公钥验)里列出的官方二进制,并且清单 seq 不低于绑定时的(合法升级,改绑);
否则拒绝连接,节点页标红「不符」。不许降级。
```
- **清单是节点带来的**,Backend 自己验签,所以不依赖面板有没有收到过这次发布;被攻破的 Backend 也加不进「官方」哈希(没有 CI 私钥)。
- **旧节点**(不上报哈希):没绑定过就放行(显示未绑定);绑定过的不许退回不上报。
- **没有更新器的节点**(二进制安装)没有清单,首次握手按上报的哈希绑定(显示「首次绑定」);手工升级之后哈希变了,会被拒绝,管理员在「系统 → 版本」点「接受当前二进制」清除绑定,下一次连接重新绑定。
- **策略** `MoShiro__BinaryPolicy`:`enforce`(默认)拒绝;`warn` 只记录不拒绝;`off` 不检查。
- **边界**:哈希是节点自己报的。一个被换进来、故意撒谎的恶意二进制可以照抄原来的哈希,Backend 分辨不出——上报者就是被怀疑的对象。要堵这一点,必须让容器之外的东西背书:宿主机更新器用安装时绑定的密钥对「当前运行的镜像 digest + 挑战」签名,由 agent 转发;容器里的进程被换掉也伪造不了。这一步还没做。
+1
View File
@@ -150,6 +150,7 @@
| POST | `/api/release/offer` | CI 登记发布清单的入口,**不需要令牌**:`{manifest, signature}` 的签名必须能用 Backend 内嵌的发布公钥验过(ECDSA P-256 / SHA-256,DER),否则 403;`seq` 必须比已登记的新,否则 409。登记不依赖面板里的任何凭证,清库、重装、还没有 passkey 时照常工作 |
| GET | `/api/admin/links` | 每一对(Probe,Agent)的线路开关:`linked`(最终结果)、`override`(手动设置,`null` 跟随自动连接)、`connected`,以及 `auto_link` |
| PUT | `/api/admin/links/{probe_id}/{agent_id}` | `{enabled: true|false|null}`:手动强制连或不连,`null` 取消手动设置;改完立即重新下发 |
| DELETE | `/api/admin/nodes/{id}/binary` | 接受节点当前的二进制:清除它的二进制绑定,下一次握手重新绑定(需要 `fleet.write`;没有绑定也返回 204)。`GET /api/admin/build-info` 的每个节点带 `binary`(`sha256`、`signed`、`seq`、`bound_at`、`violation`) |
| POST | `/api/admin/nodes/{id}/update` | 请求一个在线的 Agent 或 Probe 更新到它收到的清单(节点不在线返回 409) |
| GET/POST/PATCH | `/api/admin/users` | 首次启动时通过命令行或环境变量创建初始管理员 |