docs: reorganize into design, security, api, ops and dev; drop stale designs; refresh the diagrams [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
ci/woodpecker/push/main-amd64-build Pipeline was successful
- Group docs by category with an index (docs/README.md); fix every link, code comment and the protocol conformance test that read docs/protocol.md. - access.md and security.md rewritten to what is implemented: removed the unadopted member-proof chain, roles and recovery key that do not exist, the old implementation order and the budgets; gaps now carry a status. - enrollment, deployment, api: the install flow is the static, hash-checked script with environment variables; no default admin. - README and architecture: new topology and update-flow diagrams (operator passkey, CI signing, registry feed, auth and trust modules).
This commit is contained in:
1 parent
636a784043
commit
e014a19554
45 files changed
+581
-955
No files matched your search
@@ -38,6 +38,6 @@ Follow the user's requested validation scope. Do not start a full tag/release bu
|
||||
|
||||
## CI and releases
|
||||
|
||||
CI triggers and image tags can change; inspect `.woodpecker/*.yml` and `docs/deployment.md` before changing or describing them. In particular, check the distinction between `[CI]` main builds, `[SKIP TESTS]`, and version-tag releases in the current branch.
|
||||
CI triggers and image tags can change; inspect `.woodpecker/*.yml` and `docs/ops/deployment.md` before changing or describing them. In particular, check the distinction between `[CI]` main builds, `[SKIP TESTS]`, and version-tag releases in the current branch.
|
||||
|
||||
A version tag `vMAJOR.MINOR.PATCH` runs the full cross-platform release pipeline. Development image builds and releases must remain isolated in their respective Zot repository paths.
|
||||
@@ -20,37 +20,46 @@ MoShiro 是一个自托管的**网络线路质量观测工具**。它在若干 P
|
||||
## 信任关系
|
||||
|
||||
```text
|
||||
管理员 ── 账号密码(Cookie 会话 + CSRF)或 API 令牌(按能力授权)──▶ Backend
|
||||
管理员 ── 密码或 passkey(Cookie 会话 + CSRF),或 API 令牌(按能力授权)──▶ Backend
|
||||
访客 ── 匿名只读,站点设为公开时才可用(没有 IP、城市、ASN)──────────────▶ Backend 公开接口
|
||||
|
||||
操作员 ── passkey 给「操作员集合」签名 ──▶ Backend 只转发 ──▶ 节点自己验链
|
||||
(创世指纹和属主在安装时固定在节点上;集合里还登记了 Backend 的服务器密钥,节点据此认 Backend、加密链路)
|
||||
节点 ── 加入令牌一次性注册,之后用自己的 ECDSA P-256 密钥签名认证 ───────▶ Backend(只存公钥)
|
||||
Probe ── 用同一把密钥签名建立测量会话 ───────────────────────────────────▶ Agent(只放行面板名单里的 Probe 公钥)
|
||||
CI ── 发布私钥对「镜像 digest 清单」签名 ──▶ Backend 原样转发 ──▶ 节点宿主机的更新器用本机公钥验签
|
||||
Probe ── 先敲门,再用自己的密钥签名建立测量会话 ───────────────────────────▶ Agent(只放行面板名单里的 Probe 公钥)
|
||||
CI ── 发布私钥对「镜像 digest 清单」签名 ──▶ 镜像仓库 / Backend ──▶ 节点宿主机的更新器用本机公钥验签
|
||||
```
|
||||
|
||||
- **Backend 不信任节点上报的内容**:数据按线路隔离存放;节点的身份靠签名,数据库泄露也无法冒充节点。
|
||||
- **节点不信任 Backend 的下发**:任务超出节点本机的硬上限(目标网段、码率、包速率、监听端口)就被拒绝;节点不执行任何「执行类」指令。
|
||||
- **更新不依赖面板的诚实**:清单必须由发布私钥签名;节点宿主机的更新器用本机公钥验签、只认本机配置的镜像仓库、按 digest 拉取、拒绝旧的 `seq`。面板被攻破也推不出恶意镜像。
|
||||
- **节点不信任 Backend 的下发**:任务超出节点本机的硬上限(目标网段、码率、包速率、监听端口)就被拒绝;节点不执行任何「执行类」指令;改信任(操作员集合)要操作员 passkey 签名,Backend 伪造不了。
|
||||
- **更新不依赖面板的诚实**:清单必须由发布私钥签名(面板登记时验一次,节点宿主机的更新器用本机公钥再验一次)、只认本机配置的镜像仓库、按 digest 拉取、拒绝旧的 `seq`。面板被攻破也推不出恶意镜像。可信的发布密钥由部署的人决定:官方 CI 密钥始终信任,也可以另外信任自己的公钥。
|
||||
- **没有默认账户**:第一次打开管理端进认领向导,用后端日志里的认领码创建第一个管理员、注册 passkey、建立信任根。每个节点都必须有属主(passkey),由节点自己强制。
|
||||
- 运行中的二进制不做校验(节点自报的哈希不可信);「资产是我们的」靠安装时的签名清单和签名的 `SHA256SUMS`。详见 [`docs/security/`](docs/security/security.md)。
|
||||
|
||||
## 架构
|
||||
|
||||
```text
|
||||
Web(展示端 · 管理端)
|
||||
▲ REST + SSE
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ Backend(面板) │
|
||||
│ Api · Registry · Planner · Links │
|
||||
│ Releases · Analysis · LiveHub │
|
||||
操作员 passkey ──签名「操作员集合」──┐ CI 发布私钥 ──签名「镜像 digest 清单」──▶ 镜像仓库(清单 + 镜像)
|
||||
▼ ▲
|
||||
Web(展示端 · 管理端) │ 定时取清单
|
||||
▲ REST + SSE │
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Backend(面板) │──────────────────────────────┘
|
||||
│ Api · Auth(会话 · passkey · API 令牌 · 审计) │
|
||||
│ Trust(操作员集合 · 服务器身份)· Registry · Planner · Links │
|
||||
│ Releases(发布清单 · 可信发布密钥) │
|
||||
│ Ingest · Analysis · LiveHub │
|
||||
│ Store(段文件 + SQLite) │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
▲ ▲
|
||||
│ wss 长连接(节点发起) │ wss 长连接(节点发起)
|
||||
│ wss 长连接(节点发起,应用层加密) │ wss 长连接(节点发起,应用层加密)
|
||||
│ 任务、数据、状态、更新通知 │ 身份、Agent 列表、状态、更新通知
|
||||
┌────────────────────┐ ┌──────────────────────┐
|
||||
│ Agent × 10+ │ ◀── 测量会话 ── │ Probe × N │
|
||||
│ 有公网 IP,监听 │ │ 家宽等出口,NAT 后 │
|
||||
│ 有公网 IP,监听 │ 先敲门再 TLS │ 家宽等出口,NAT 后 │
|
||||
└────────────────────┘ └──────────────────────┘
|
||||
测量会话:TLS 控制通道 + UDP/TCP 数据通道,由 Probe 主动拨出
|
||||
宿主机更新器(容器节点):用本机公钥验签 → 按 digest 拉镜像 → 重启,失败回滚
|
||||
```
|
||||
|
||||
两种连接:**Probe 和 Agent 都用长连接连 Backend**(身份、任务、状态、更新通知),**测量只走 Probe → Agent**(Probe 主动拨出,Agent 不需要连 Probe,家宽也不需要入站端口)。哪些 Probe 连哪些 Agent 由 Backend 决定:默认自动全连,也可以在管理端手动选择。
|
||||
@@ -60,7 +69,7 @@ MoShiro 是一个自托管的**网络线路质量观测工具**。它在若干 P
|
||||
Probe / Agent 积木 ──▶ 原始列块(10 秒一块)──▶ Backend 段文件 ──▶ 指标与服务模型 ──▶ REST / SSE ──▶ Web
|
||||
```
|
||||
|
||||
Backend 用段文件保存原始数据,用 SQLite 保存索引、指标与配置。**完整的架构、数据链路、安全边界和设计决策见 [`docs/architecture.md`](docs/architecture.md)。**
|
||||
Backend 用段文件保存原始数据,用 SQLite 保存索引、指标与配置。**完整的架构、数据链路、安全边界和设计决策见 [`docs/design/architecture.md`](docs/design/architecture.md)。**
|
||||
|
||||
## 协议
|
||||
|
||||
@@ -69,7 +78,7 @@ Backend 用段文件保存原始数据,用 SQLite 保存索引、指标与配
|
||||
- **测量数据面**:Probe 到 Agent 的 TLS 控制通道、UDP 和 TCP 数据通道(默认 `7443/tcp`、`7443/udp`、`7444/tcp`);会话用 `session_id` 和 MAC 认证、不绑定五元组,NAT 映射变化后自动跟随。
|
||||
- **REST + SSE**:`/api/public`(展示,可匿名)、`/api/admin`(管理);OpenAPI 在 `backend/openapi/v1.json`,前端据此生成类型。
|
||||
|
||||
细节见 [`docs/protocol.md`](docs/protocol.md)、[`backend/fleet/README.md`](backend/fleet/README.md)、[`docs/api.md`](docs/api.md)、[`docs/enrollment.md`](docs/enrollment.md)。
|
||||
细节见 [`docs/design/protocol.md`](docs/design/protocol.md)、[`backend/fleet/README.md`](backend/fleet/README.md)、[`docs/api/api.md`](docs/api/api.md)、[`docs/security/enrollment.md`](docs/security/enrollment.md)。
|
||||
|
||||
## 如何构建
|
||||
|
||||
@@ -80,7 +89,7 @@ dotnet build MoShiro.slnx -c Release # Backend、Agent、Probe、Fleet
|
||||
npm ci --prefix web && npm run build --prefix web # 展示端和管理端,产物在 web/display/dist、web/admin/dist
|
||||
```
|
||||
|
||||
本地运行:先在 `backend/` 里 `dotnet run --project src/MoShiro.Backend -- --urls http://localhost:5000`,再 `npm run dev --prefix web`,展示端是 <http://localhost:5173/>,管理端是 <http://localhost:5173/admin/>。
|
||||
本地运行:先在 `backend/` 里 `dotnet run --project src/MoShiro.Backend -- --urls http://localhost:5000`,再 `npm run dev --prefix web`,展示端是 <http://localhost:5173/>,管理端是 <http://localhost:5173/admin/>。第一次打开管理端会进认领向导,认领码在后端日志里(或用 `MoShiro__ClaimCode` 指定)。
|
||||
|
||||
测试和检查:
|
||||
|
||||
@@ -90,22 +99,19 @@ npm run lint:css --prefix web # 前端样式(颜色只能写
|
||||
npm run gen:api --prefix web # 后端改了接口之后重新生成前端类型
|
||||
```
|
||||
|
||||
Backend、Agent、Probe 以 OCI 镜像发布(Linux amd64、arm64),Agent 和 Probe 另有 Linux glibc/musl、macOS、Windows 的二进制包。NativeAOT 的构建细节、CI 触发规则(`[CI]`、`[SKIP TESTS]`、版本 tag)、镜像路径、容器节点的运行和更新见 [`docs/deployment.md`](docs/deployment.md);前端见 [`web/README.md`](web/README.md),后端见 [`backend/README.md`](backend/README.md)。
|
||||
Backend、Agent、Probe 以 OCI 镜像发布(Linux amd64、arm64),Agent 和 Probe 另有 Linux glibc/musl、macOS、Windows 的二进制包。NativeAOT 的构建细节、CI 触发规则(`[CI]`、`[SKIP TESTS]`、版本 tag)、镜像路径、容器节点的运行和更新见 [`docs/ops/deployment.md`](docs/ops/deployment.md);前端见 [`web/README.md`](web/README.md),后端见 [`backend/README.md`](backend/README.md)。
|
||||
|
||||
## 文档
|
||||
|
||||
| 文档 | 内容 |
|
||||
| --- | --- |
|
||||
| [`docs/architecture.md`](docs/architecture.md) | 架构、数据链路、安全边界、设计决策 |
|
||||
| [`docs/security.md`](docs/security.md) | 零信任设计稿(部分已实现):威胁模型、入网信任链、监听隐身、extension 授权 |
|
||||
| [`docs/access.md`](docs/access.md) | 访问控制与入网信任链设计(尚未实现):账户、角色、能力、API key、提权窗口、passkey 到节点的签名链 |
|
||||
| [`docs/protocol.md`](docs/protocol.md)、[`backend/fleet/README.md`](backend/fleet/README.md) | 线协议、Fleet 节点框架(身份、注册、连接) |
|
||||
| [`docs/enrollment.md`](docs/enrollment.md) | 注册、身份、Probe 与 Agent 的接入 |
|
||||
| [`docs/measurement.md`](docs/measurement.md)、[`docs/analysis.md`](docs/analysis.md) | 采集引擎与任务模型、分析层与指标 |
|
||||
| [`docs/storage.md`](docs/storage.md)、[`docs/query.md`](docs/query.md) | 存储、展示数据查询 |
|
||||
| [`docs/api.md`](docs/api.md)、[`docs/api-guidelines.md`](docs/api-guidelines.md) | API 规范与约束 |
|
||||
| [`docs/deployment.md`](docs/deployment.md) | 构建、发布、CI、部署与更新 |
|
||||
| [`docs/project-layout.md`](docs/project-layout.md)、[`docs/coding-style.md`](docs/coding-style.md) | 仓库结构与依赖规则、代码风格 |
|
||||
| [`docs/README.md`](docs/README.md) | 文档索引,按类别组织 |
|
||||
| [`docs/design/`](docs/design/architecture.md) | 设计:架构与决策、线协议、存储与查询、采集与分析 |
|
||||
| [`docs/security/`](docs/security/security.md) | 安全:零信任与威胁模型、访问控制与入网信任链、注册与身份 |
|
||||
| [`docs/api/`](docs/api/api.md) | 接口:端点说明、API 规范 |
|
||||
| [`docs/ops/deployment.md`](docs/ops/deployment.md) | 运维:构建、发布、CI、部署与更新 |
|
||||
| [`docs/dev/`](docs/dev/project-layout.md) | 开发:仓库结构与依赖规则、代码风格 |
|
||||
| [`backend/fleet/README.md`](backend/fleet/README.md) | Fleet 节点框架(身份、注册、连接) |
|
||||
|
||||
代码目录:`backend/`(Backend、`fleet/` 节点框架、`shared/` 测量引擎和协议、`agent/`、`probe/`、`tests/`)、`web/`(`display` 展示端、`admin` 管理端、`shared`)、`packaging/` 和 `scripts/`(部署与维护)、`ci/` 和 `.woodpecker/`(流水线)。
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
## 任务与面板
|
||||
|
||||
- 单个 Agent 的任务(目前是线路模板加按 Agent 的覆盖,没有只属于某一个 Agent 的独立任务)
|
||||
- 原生 webhook 投递;告警(基于 webhook)。实现之前先定约束(只允许 HTTPS、禁止私网和回环、不跟随重定向、限制超时和响应大小、解析后再检查地址),见 [`docs/security.md`](docs/security.md) 第 9.2 节
|
||||
- 原生 webhook 投递;告警(基于 webhook)。实现之前先定约束(只允许 HTTPS、禁止私网和回环、不跟随重定向、限制超时和响应大小、解析后再检查地址),见 [`docs/security/security.md`](docs/security/security.md) 第 9.2 节
|
||||
|
||||
## 部署
|
||||
|
||||
- 后端自动下载前端资源:用 Podman、Docker 部署时,默认拉取最新的前端资源(现在前端要自己构建,镜像里没有)。
|
||||
[security.md](docs/security.md)
|
||||
[security.md](docs/security/security.md)
|
||||
## 访客页面
|
||||
|
||||
- 人机验证:访客页面要通过验证才能拉数据,前端访问时静默触发。候选 tiago2/cap 和 Cloudflare Turnstile。
|
||||
@@ -34,11 +34,11 @@
|
||||
- 流程:下载 → 校验 → 解包到 `versions/<v>/` → 原子切换 `current`(Linux 符号链接,Windows 目录联结)→ 重启服务 → 等健康信标确认 → 失败切回旧版本并把该版本标记为坏版本。
|
||||
- 权限分离:更新进程只能写安装目录和重启被管理的服务,读不到节点身份私钥。
|
||||
- OpenWrt:`keep_versions = 1`,下载前检查闪存剩余空间。
|
||||
- 容器节点已经实现的那一套(清单签名、`seq` 防重放、按 digest 拉取、回滚)见 [`docs/deployment.md`](docs/deployment.md)。
|
||||
- 容器节点已经实现的那一套(清单签名、`seq` 防重放、按 digest 拉取、回滚)见 [`docs/ops/deployment.md`](docs/ops/deployment.md)。
|
||||
|
||||
## 安全
|
||||
|
||||
- **零信任**:设计稿在 [`docs/security.md`](docs/security.md),按其中第 12 节的顺序实现(阶段零「协议方向声明表」已完成):Probe 自己生成密钥、Agent 预握手敲门、操作员凭证和名册、测量数据的签名和归属校验、入库配额和管理端 CSP、Backend 应用层身份、extension 框架(敏感能力都放进可选安装的独立 extension,核心保持 NativeAOT);可选的 UDP 单包授权、UDP 会话迁移的路径验证。
|
||||
- **零信任**:设计和各缺口的状态在 [`docs/security/security.md`](docs/security/security.md)。已完成:协议方向声明表、Probe 自己生成密钥、Agent 预握手敲门、操作员集合与创世固定、服务器身份和安全链路、属主固定。还没做:测量数据的产生者签名和归属校验、入库配额、Probe 只拨公网地址、管理端 CSP 之外的输入校验、extension 框架(敏感能力都放进可选安装的独立 extension,核心保持 NativeAOT)、吊销表;可选的 UDP 单包授权、UDP 会话迁移的路径验证。
|
||||
- **extension**(Web SSH、TTY、更新遥测,敏感 operator):设计时必须保住「节点是纯采集器」这条边界,见 [`README.md`](README.md) 的项目边界。
|
||||
- 对 UDP 包、控制帧、RawChunk、JSON-RPC 做模糊测试;伪造签名和重放的专项测试。
|
||||
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ dotnet test --solution MoShiro.slnx
|
||||
dotnet publish src/MoShiro.Backend -c Release -r <RID> -p:PublishAot=true
|
||||
```
|
||||
|
||||
例如 `<RID>` 可为 `win-x64`、`linux-x64` 或 `linux-arm64`。NativeAOT 发布需要对应平台的 C/C++ 工具链。仓库根目录的 `scripts/publish.sh backend <RID>` 可构建单个平台的 Backend 可执行文件;容器发布由 Woodpecker tag 流水线完成,详见 [`../docs/deployment.md`](../docs/deployment.md)。
|
||||
例如 `<RID>` 可为 `win-x64`、`linux-x64` 或 `linux-arm64`。NativeAOT 发布需要对应平台的 C/C++ 工具链。仓库根目录的 `scripts/publish.sh backend <RID>` 可构建单个平台的 Backend 可执行文件;容器发布由 Woodpecker tag 流水线完成,详见 [`../docs/ops/deployment.md`](../docs/ops/deployment.md)。
|
||||
|
||||
正式发布通过 Woodpecker tag 流水线生成 Linux/amd64 与 Linux/arm64 OCI 镜像,容器内监听 `8080`,数据写入挂载的 `/data` 卷。Agent 与 Probe 的多平台二进制包单独发布,不包含在 Backend 镜像中。
|
||||
|
||||
@@ -49,6 +49,6 @@ cd backend
|
||||
dotnet run --project src/MoShiro.Backend -- --urls http://localhost:5000
|
||||
```
|
||||
|
||||
没有默认管理员:第一次打开面板时按页面引导自己创建管理员、注册 passkey、建立信任根,页面要的认领码写在后端日志里(或用 `MoShiro__ClaimCode` 指定)。`MoShiro__InitialAdminUser` / `MoShiro__InitialAdminPassword` 只给自动化用。数据目录、公开地址、Cookie 和 CORS 等设置通过 `MoShiro` 配置节传入;完整配置说明见 [`../docs/deployment.md`](../docs/deployment.md)。站点默认需要登录,可在管理端修改公开设置。
|
||||
没有默认管理员:第一次打开面板时按页面引导自己创建管理员、注册 passkey、建立信任根,页面要的认领码写在后端日志里(或用 `MoShiro__ClaimCode` 指定)。`MoShiro__InitialAdminUser` / `MoShiro__InitialAdminPassword` 只给自动化用。数据目录、公开地址、Cookie 和 CORS 等设置通过 `MoShiro` 配置节传入;完整配置说明见 [`../docs/ops/deployment.md`](../docs/ops/deployment.md)。站点默认需要登录,可在管理端修改公开设置。
|
||||
|
||||
OpenAPI 文档由构建生成到 `openapi/v1.json`。API 设计和端点说明见 [`../docs/api.md`](../docs/api.md);项目总览见仓库根目录的 [`README.md`](../README.md)。
|
||||
OpenAPI 文档由构建生成到 `openapi/v1.json`。API 设计和端点说明见 [`../docs/api/api.md`](../docs/api/api.md);项目总览见仓库根目录的 [`README.md`](../README.md)。
|
||||
@@ -29,7 +29,7 @@ dotnet publish agent/src/MoShiro.Agent -c Release -r <RID> -p:PublishAot=true
|
||||
dotnet publish fleet/tools/fleet-enroll -c Release -r <RID> -p:PublishAot=true
|
||||
```
|
||||
|
||||
也可在仓库根目录运行 `bash scripts/publish.sh agent <RID>` 和 `bash scripts/publish.sh fleet-enroll <RID>`。部署包和平台说明见 [`../../docs/deployment.md`](../../docs/deployment.md)。
|
||||
也可在仓库根目录运行 `bash scripts/publish.sh agent <RID>` 和 `bash scripts/publish.sh fleet-enroll <RID>`。部署包和平台说明见 [`../../docs/ops/deployment.md`](../../docs/ops/deployment.md)。
|
||||
|
||||
## 注册与运行
|
||||
|
||||
@@ -47,7 +47,7 @@ fleet-enroll --server https://<backend> --join <token> --name <agent-name> --ide
|
||||
moshiro-agent --config /etc/moshiro/agent.json
|
||||
```
|
||||
|
||||
容器节点的运行方式(podman + systemd)和宿主机更新器见 [`../../docs/deployment.md`](../../docs/deployment.md)。也可使用 OCI 镜像 `registry.oeo.one/moshiro/moshiro-agent:<version>`,自动选择 Linux amd64 或 arm64 镜像。镜像同时包含 `fleet-enroll`;可通过 `--entrypoint /usr/local/bin/fleet-enroll` 执行首次注册。容器默认读取命名 volume 中的 `/etc/moshiro/agent.json` 与身份文件;建议使用宿主网络,Linux ICMP 优先使用普通用户 ping socket,内核 ping_group_range 需允许映射后的 GID。镜像以 UID/GID 1654 运行,新命名 volume 由引擎按镜像目录初始化权限。
|
||||
容器节点的运行方式(podman + systemd)和宿主机更新器见 [`../../docs/ops/deployment.md`](../../docs/ops/deployment.md)。也可使用 OCI 镜像 `registry.oeo.one/moshiro/moshiro-agent:<version>`,自动选择 Linux amd64 或 arm64 镜像。镜像同时包含 `fleet-enroll`;可通过 `--entrypoint /usr/local/bin/fleet-enroll` 执行首次注册。容器默认读取命名 volume 中的 `/etc/moshiro/agent.json` 与身份文件;建议使用宿主网络,Linux ICMP 优先使用普通用户 ping socket,内核 ping_group_range 需允许映射后的 GID。镜像以 UID/GID 1654 运行,新命名 volume 由引擎按镜像目录初始化权限。
|
||||
|
||||
不传 `--config` 时默认读取 `/etc/moshiro/agent.json`。Agent 默认监听控制端口 `7443/tcp`,数据端口 `7443/udp` 和 `7444/tcp`;防火墙和面板中的节点端口配置需与本地配置一致。`reporting.host_stats` 可关闭主机状态上报;启用时只上报资源使用等汇总数据,不包含主机名、进程或 IP。
|
||||
|
||||
@@ -58,6 +58,6 @@ IPv6 线路需要 Agent 实际绑定 IPv6。新配置的默认 `listen.bind` 为
|
||||
- 每个 Probe 使用独立的 TLS 会话和数据密钥;Probe 断开时对应的会话资源会释放。
|
||||
- 数据面会校验会话认证、重放窗口和速率限制;无效数据包会静默丢弃。
|
||||
- 测量引擎和协议由 `backend/shared/` 提供;节点身份、注册和连接由 `backend/fleet/` 提供。
|
||||
- 详细安全边界与拓扑见 [`../../docs/architecture.md`](../../docs/architecture.md),协议说明见 [`../../docs/protocol.md`](../../docs/protocol.md),部署见 [`../../docs/deployment.md`](../../docs/deployment.md)。
|
||||
- 详细安全边界与拓扑见 [`../../docs/design/architecture.md`](../../docs/design/architecture.md),协议说明见 [`../../docs/design/protocol.md`](../../docs/design/protocol.md),部署见 [`../../docs/ops/deployment.md`](../../docs/ops/deployment.md)。
|
||||
|
||||
Docker 与 rootless/rootful Podman 共用 `packaging/compose/agent/compose.yaml`,配置、身份和缓存放在命名 volume 中,无需宿主配置目录、keep-id 或手动 chown。首次导入和自动更新见部署文档。
|
||||
@@ -23,7 +23,7 @@
|
||||
2. **安全默认值**:Fleet 永远不提供 exec、shell、文件读写、远程下发代码这类方法;节点只响应本地注册过的方法(本地策略 `allow_methods` 还能收窄);调用对方不支持的方法在本地直接返回 `fleet_unsupported`。
|
||||
3. **权限分离**:注册(`fleet-enroll`,一次性)和常驻业务进程是两个进程;常驻进程不含注册代码,也不使用 `HttpClient`。
|
||||
4. **NativeAOT**:节点侧只依赖 BCL,泛型扩展点通过产品传入的 `JsonTypeInfo<T>` 序列化,不使用反射。
|
||||
5. **更新不在 Fleet 里**:节点进程没有自更新能力。容器节点的签名更新由宿主机上的更新器完成,见 [`docs/deployment.md`](../../docs/deployment.md);纯二进制节点的更新是待办,见 [`TODO.md`](../../TODO.md)。
|
||||
5. **更新不在 Fleet 里**:节点进程没有自更新能力。容器节点的签名更新由宿主机上的更新器完成,见 [`docs/ops/deployment.md`](../../docs/ops/deployment.md);纯二进制节点的更新是待办,见 [`TODO.md`](../../TODO.md)。
|
||||
|
||||
## 身份
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
- JSON-RPC 2.0;方法名 `领域.动作`,`fleet.*` 保留给框架,产品用自己的前缀(MoShiro 是 `moshiro.*`)。
|
||||
- 二进制帧 `u8 channel | 正文`:`0`–`15` 框架保留,产品从 `16` 开始。
|
||||
- 错误:`error.data.code` 为 snake_case,框架的以 `fleet_` 开头,产品可以扩展。
|
||||
- **方法必须声明**:每个方法在声明表(`MethodTable`)里写明发起方、请求还是通知、哪些角色可以参与;不存在双向方法,同一个方法名在一个平面里只有一个发起方。`RpcRouter` 在注册、分发和发送三处核对,违规回 `fleet_protocol_violation`,超过上限断开连接。详见 [`docs/security.md`](../../docs/security.md) 第 8 节。
|
||||
- **方法必须声明**:每个方法在声明表(`MethodTable`)里写明发起方、请求还是通知、哪些角色可以参与;不存在双向方法,同一个方法名在一个平面里只有一个发起方。`RpcRouter` 在注册、分发和发送三处核对,违规回 `fleet_protocol_violation`,超过上限断开连接。详见 [`docs/security/security.md`](../../docs/security/security.md) 第 8 节。
|
||||
- 与传输无关:同一套路由跑在 wss 上,也跑在 TLS 帧(`u32 长度 | u8 类型 | 正文`)上。
|
||||
|
||||
| 保留方法 | 方向 | 说明 |
|
||||
|
||||
@@ -25,7 +25,7 @@ public sealed partial class TrustFileContext : JsonSerializerContext;
|
||||
/// <summary>
|
||||
/// 节点的信任根:安装命令里带来的创世指纹和属主,在注册时固定到本地,之后 Backend 改不了。
|
||||
/// 操作员集合的新版本随期望状态下发,节点只接受从固定的创世出发、签名完整、版本更新的链。
|
||||
/// 属主只是被固定和保存:节点现在没有 extension,等做 extension 时由节点本地强制(见 docs/access.md 第 8 节)。
|
||||
/// 属主只是被固定和保存:节点现在没有 extension,等做 extension 时由节点本地强制(见 docs/security/access.md 第 9 节)。
|
||||
/// </summary>
|
||||
public sealed class TrustAnchor : IServerVerifier
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed record OperatorKey(string CredentialId, byte[] PublicKeySpki);
|
||||
public sealed record OperatorSet(int Version, string Previous, string RpId, IReadOnlyList<string> Origins, IReadOnlyList<OperatorKey> Keys, IReadOnlyList<byte[]>? Servers = null);
|
||||
|
||||
/// <summary>
|
||||
/// 信任根:操作员(passkey)→ 操作员集合 → 节点本地固定的属主。设计见 docs/access.md 第 7 节。
|
||||
/// 信任根:操作员(passkey)→ 操作员集合 → 节点本地固定的属主。设计见 docs/security/access.md 第 8 节。
|
||||
/// 安装命令里带着创世指纹和属主(passkey 的凭证 id):节点把它们固定在本地,之后只认从创世出发、由操作员 passkey 签过名的集合版本,
|
||||
/// 属主必须是集合里的凭证。Backend 能转发集合,但签不出新的版本,也改不了节点已经固定的东西。
|
||||
/// 全是纯函数,只用 BCL,节点和 Backend 共用。语句是 <c>key=value</c> 的文本,第一行是带版本的标题;签名覆盖整段文本。
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Fleet.Identity;
|
||||
|
||||
/// <summary>
|
||||
/// 验证一次 WebAuthn 断言(<c>navigator.credentials.get</c> 的结果)。只支持 ES256(P-256 的 ECDSA),纯函数,只用 BCL:
|
||||
/// 面板用它验证 passkey 登录,节点以后用它验证 passkey 签出的委托(docs/access.md 第 7 节)。
|
||||
/// 面板用它验证 passkey 登录,节点验证操作员集合的签名也用它(docs/security/access.md 第 8 节)。
|
||||
/// 不解析 CBOR——注册时的 attestation 解析只在 Backend 里。
|
||||
/// </summary>
|
||||
public static class WebAuthnAssertion
|
||||
|
||||
@@ -13,7 +13,7 @@ Backend ──任务──> Agent <──测量会话──> Probe
|
||||
|
||||
Probe 用 `probe.json` 读取面板地址(`fleet.server`)、身份目录(`fleet.identity_dir`,由 `fleet-enroll` 注册时生成)和本地测试限制。连上面板之后,面板下发要连接的 Agent 列表;Probe 将列表缓存到同目录的 `agents.json`,面板暂时连不上时靠它继续测量。身份目录里有私钥,请限制访问权限,不要提交到版本控制。
|
||||
|
||||
容器节点的运行方式(podman + systemd)和宿主机更新器见 [`../../docs/deployment.md`](../../docs/deployment.md);Probe 也可以用 OCI 镜像 `registry.oeo.one/moshiro/moshiro-probe:<version>`,容器默认读取 `/etc/moshiro/probe.json`,命名 volume 保存身份配置和缓存,镜像以 UID/GID 1654 运行。
|
||||
容器节点的运行方式(podman + systemd)和宿主机更新器见 [`../../docs/ops/deployment.md`](../../docs/ops/deployment.md);Probe 也可以用 OCI 镜像 `registry.oeo.one/moshiro/moshiro-probe:<version>`,容器默认读取 `/etc/moshiro/probe.json`,命名 volume 保存身份配置和缓存,镜像以 UID/GID 1654 运行。
|
||||
|
||||
## 构建
|
||||
|
||||
@@ -30,7 +30,7 @@ dotnet build probe/src/MoShiro.Probe -c Release
|
||||
dotnet publish probe/src/MoShiro.Probe -c Release -r <RID> -p:PublishAot=true
|
||||
```
|
||||
|
||||
也可在仓库根目录运行 `bash scripts/publish.sh probe <RID>`,再使用 `scripts/package.sh probe <RID>` 生成部署包。支持平台和安装流程见 [`../../docs/deployment.md`](../../docs/deployment.md)。
|
||||
也可在仓库根目录运行 `bash scripts/publish.sh probe <RID>`,再使用 `scripts/package.sh probe <RID>` 生成部署包。支持平台和安装流程见 [`../../docs/ops/deployment.md`](../../docs/ops/deployment.md)。
|
||||
|
||||
## 配置与运行
|
||||
|
||||
@@ -54,6 +54,6 @@ moshiro-probe --config /etc/moshiro/probe.json
|
||||
|
||||
- Probe 只执行编译进程序的测量模块。它不落盘测量数据,也不能执行命令或自行更新(更新由宿主机上的更新器完成,Probe 只写清单和请求文件)。
|
||||
- 测量模块由 `backend/shared/MoShiro.Measurement/` 与 Agent 共用;Probe 负责发起连接、调度任务和记录本端数据。
|
||||
- 测量模型和协议细节见 [`../../docs/measurement.md`](../../docs/measurement.md) 与 [`../../docs/protocol.md`](../../docs/protocol.md);整体架构见 [`../../docs/architecture.md`](../../docs/architecture.md)。
|
||||
- 测量模型和协议细节见 [`../../docs/design/measurement.md`](../../docs/design/measurement.md) 与 [`../../docs/design/protocol.md`](../../docs/design/protocol.md);整体架构见 [`../../docs/design/architecture.md`](../../docs/design/architecture.md)。
|
||||
|
||||
Docker 与 rootless/rootful Podman 共用 `packaging/compose/probe/compose.yaml`,配置、身份和缓存放在命名 volume 中,无需宿主配置目录、keep-id 或手动 chown。首次导入和自动更新见部署文档。
|
||||
@@ -8,7 +8,7 @@ using MoShiro.Protocol;
|
||||
namespace MoShiro.Backend.Api;
|
||||
|
||||
/// <summary>
|
||||
/// 信任根(docs/access.md 第 7 节):操作员集合由 passkey 签名,节点从安装命令里固定的创世指纹出发验证它;
|
||||
/// 信任根(docs/security/access.md 第 8 节):操作员集合由 passkey 签名,节点从安装命令里固定的创世指纹出发验证它;
|
||||
/// 带操作员委托的加入码也在这里创建——委托是 passkey 对「这个码」签的,面板自己签不出来。
|
||||
/// </summary>
|
||||
public static class TrustEndpoints
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace MoShiro.Backend.Api;
|
||||
|
||||
/// <summary>
|
||||
/// 一行安装脚本:<c>packaging/install/</c> 下的 <c>install.sh</c> 和 <c>install.ps1</c> 作为嵌入资源打进 Backend,<b>原样返回,不替换任何值</b>。
|
||||
/// 静态,所以它的 SHA-256 在一个版本内固定:CI 构建时算出来写进签名的发布清单,面板给的命令先核对哈希再执行(docs/access.md 第 15 节)。
|
||||
/// 静态,所以它的 SHA-256 在一个版本内固定:CI 构建时算出来写进签名的发布清单,面板给的命令先核对哈希再执行(docs/security/access.md 第 10 节)。
|
||||
/// 角色、面板地址、代码都走命令里的环境变量。
|
||||
/// </summary>
|
||||
public static class InstallScripts
|
||||
|
||||
@@ -2,7 +2,7 @@ namespace MoShiro.Backend.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 能力(capability):一项具体的权限。端点声明自己需要哪一项,<see cref="Principal.Has"/> 是唯一的判断入口,
|
||||
/// 会话、API key 都走它。设计见 docs/access.md 第 2 节。
|
||||
/// 会话、API key 都走它。设计见 docs/security/access.md 第 3 节。
|
||||
/// </summary>
|
||||
public static class Capability
|
||||
{
|
||||
@@ -38,7 +38,7 @@ public static class Capability
|
||||
public const string EventsRead = "events.read";
|
||||
public const string OpsMetrics = "ops.metrics";
|
||||
|
||||
// 预留:外部 webhook 回调和数据拉取(docs/access.md 5.4),目录里先存在,实现放后面
|
||||
// 预留:外部 webhook 回调和数据拉取(docs/security/access.md 6.3),目录里先存在,实现放后面
|
||||
public const string DataExport = "data.export";
|
||||
public const string WebhooksRead = "webhooks.read";
|
||||
public const string WebhooksManage = "webhooks.manage";
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed record PasskeyAssertion(string CredentialId, string AuthenticatorD
|
||||
/// <summary>
|
||||
/// passkey 的注册与登录(WebAuthn,ES256,attestation 当 none)。挑战只在内存里,一次有效、5 分钟过期;
|
||||
/// 登录是「可发现凭证」:不需要先输用户名,凭证 id 找到用户。验证断言的纯函数在 <see cref="WebAuthnAssertion"/>(节点以后也用)。
|
||||
/// 设计见 docs/access.md 第 3 节。
|
||||
/// 设计见 docs/security/access.md 第 4 节。
|
||||
/// </summary>
|
||||
public sealed class PasskeyService
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed record TrustState(bool Initialized, string? Genesis, int Version,
|
||||
/// <summary>
|
||||
/// 信任根的 Backend 一侧:保存操作员集合的链、给操作员出下一版的语句、验证他签回来的版本。
|
||||
/// Backend <b>不持有任何签名的能力</b>:每一版都要操作员的 passkey 触碰;节点从安装命令里固定的创世指纹出发自己验链,
|
||||
/// 所以 Backend 在这里的验证只是不收垃圾,不是信任的来源。设计见 docs/access.md 第 7 节。
|
||||
/// 所以 Backend 在这里的验证只是不收垃圾,不是信任的来源。设计见 docs/security/access.md 第 8 节。
|
||||
/// </summary>
|
||||
public sealed class TrustService : IOperatorTrustSource, IDisposable
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ public sealed record RegisteredCredential(byte[] CredentialId, byte[] PublicKeyS
|
||||
|
||||
/// <summary>
|
||||
/// 解析并校验 WebAuthn 注册(<c>navigator.credentials.create</c> 的结果)。只接受 ES256;attestation 一律当 <c>none</c> 处理:
|
||||
/// 不验设备证明,信任浏览器的实现(docs/access.md)。CBOR 解析只在 Backend 里,节点不带。
|
||||
/// 不验设备证明,信任浏览器的实现(docs/security/access.md)。CBOR 解析只在 Backend 里,节点不带。
|
||||
/// </summary>
|
||||
public static class WebAuthnRegistration
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Xml.Linq;
|
||||
namespace MoShiro.Backend.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 依赖规则(docs/project-layout.md 第 3 节):由测试在构建期强制。
|
||||
/// 依赖规则(docs/dev/project-layout.md 第 3 节):由测试在构建期强制。
|
||||
/// 读取仓库里所有 .csproj 和源文件,违反规则就失败。
|
||||
/// </summary>
|
||||
public sealed class ArchitectureTests
|
||||
|
||||
@@ -8,7 +8,7 @@ using MoShiro.Backend.Application;
|
||||
|
||||
namespace MoShiro.Backend.Tests;
|
||||
|
||||
/// <summary>能力模型(docs/access.md 第 2 节):每个管理端点声明能力;密钥只能拿到自己拥有能力的子集;写蕴含读。</summary>
|
||||
/// <summary>能力模型(docs/security/access.md 第 3 节):每个管理端点声明能力;密钥只能拿到自己拥有能力的子集;写蕴含读。</summary>
|
||||
[Collection("probe-e2e")]
|
||||
public sealed class CapabilityTests
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.Text.RegularExpressions;
|
||||
namespace MoShiro.Backend.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 对照 docs/api-guidelines.md 检查运行时生成的 OpenAPI 文档:operationId、路径风格、错误响应格式、字段命名。
|
||||
/// 对照 docs/api/api-guidelines.md 检查运行时生成的 OpenAPI 文档:operationId、路径风格、错误响应格式、字段命名。
|
||||
/// 新增端点违反规范时,这里会直接失败(等价于文档里的 <c>eng/check-openapi</c>)。
|
||||
/// </summary>
|
||||
[Collection("backend")]
|
||||
|
||||
@@ -85,7 +85,7 @@ public sealed class SoftwarePasskey : IDisposable
|
||||
private static byte[] Text(string text) => [.. Head(3, Encoding.UTF8.GetByteCount(text)), .. Encoding.UTF8.GetBytes(text)];
|
||||
}
|
||||
|
||||
/// <summary>passkey 登录:注册、登录、拒绝各种错误的断言、只能管理自己的 passkey。设计见 docs/access.md 第 3 节。</summary>
|
||||
/// <summary>passkey 登录:注册、登录、拒绝各种错误的断言、只能管理自己的 passkey。设计见 docs/security/access.md 第 4 节。</summary>
|
||||
[Collection("probe-e2e")]
|
||||
public sealed class PasskeyTests
|
||||
{
|
||||
|
||||
@@ -39,7 +39,7 @@ public sealed class ProtocolDeclarationTests
|
||||
public void EveryDeclaredMethod_IsDocumented()
|
||||
{
|
||||
string root = FindRoot();
|
||||
string docs = File.ReadAllText(Path.Combine(root, "docs", "protocol.md")) + File.ReadAllText(Path.Combine(root, "backend", "fleet", "README.md"));
|
||||
string docs = File.ReadAllText(Path.Combine(root, "docs", "design", "protocol.md")) + File.ReadAllText(Path.Combine(root, "backend", "fleet", "README.md"));
|
||||
string[] missing = [.. s_table.Methods.Select(m => m.Name).Distinct().Where(n => !docs.Contains($"`{n}`", StringComparison.Ordinal))];
|
||||
Assert.Empty(missing);
|
||||
}
|
||||
@@ -84,7 +84,7 @@ public sealed class ProtocolDeclarationTests
|
||||
private static string FindRoot()
|
||||
{
|
||||
string? dir = AppContext.BaseDirectory;
|
||||
while (dir is not null && !File.Exists(Path.Combine(dir, "docs", "protocol.md")))
|
||||
while (dir is not null && !File.Exists(Path.Combine(dir, "docs", "design", "protocol.md")))
|
||||
{
|
||||
dir = Path.GetDirectoryName(dir);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ using MoShiro.Protocol;
|
||||
|
||||
namespace MoShiro.Backend.Tests;
|
||||
|
||||
/// <summary>时间段、步长与数据源的规划:与 docs/query.md 里的表格逐行对应。</summary>
|
||||
/// <summary>时间段、步长与数据源的规划:与 docs/design/query.md 里的表格逐行对应。</summary>
|
||||
public sealed class QueryPlanningTests
|
||||
{
|
||||
private const long Second = 1000;
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace MoShiro.Backend.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 信任根:操作员(passkey)签操作员集合;安装命令里带着创世指纹和属主(都是公开值),节点注册时验证面板的集合并固定在本地。
|
||||
/// 设计见 docs/access.md 第 7 节。
|
||||
/// 设计见 docs/security/access.md 第 8 节。
|
||||
/// </summary>
|
||||
[Collection("probe-e2e")]
|
||||
public sealed class TrustTests
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# 文档
|
||||
|
||||
按类别组织。每份文档开头写明它是已实现的说明还是设计稿;对不上代码时以代码为准。
|
||||
|
||||
| 类别 | 文档 | 内容 |
|
||||
| --- | --- | --- |
|
||||
| 设计 | [architecture.md](design/architecture.md) | 架构、数据链路、安全边界、设计决策 |
|
||||
| | [protocol.md](design/protocol.md) | 线协议、方法的方向与角色 |
|
||||
| | [storage.md](design/storage.md)、[query.md](design/query.md) | 存储;展示数据查询 |
|
||||
| | [measurement.md](design/measurement.md)、[analysis.md](design/analysis.md) | 采集引擎与任务模型;分析层与指标 |
|
||||
| 安全 | [security.md](security/security.md) | 零信任:威胁模型、缺口与状态、协议卡口、监听隐身、extension |
|
||||
| | [access.md](security/access.md) | 访问控制与入网信任链:能力、API key、passkey、操作员集合、属主、安装的供应链 |
|
||||
| | [enrollment.md](security/enrollment.md) | 注册与身份:Agent、Probe 的接入,节点本地硬上限 |
|
||||
| 接口 | [api.md](api/api.md)、[api-guidelines.md](api/api-guidelines.md) | 端点说明;API 规范与约束 |
|
||||
| 运维 | [deployment.md](ops/deployment.md) | 构建、发布、CI、部署与更新、发布密钥 |
|
||||
| 开发 | [project-layout.md](dev/project-layout.md)、[coding-style.md](dev/coding-style.md) | 仓库结构与依赖规则;代码风格 |
|
||||
|
||||
各子项目的说明在它们自己的目录里:[backend](../backend/README.md)、[fleet](../backend/fleet/README.md)、[agent](../backend/agent/README.md)、[probe](../backend/probe/README.md)、[web](../web/README.md)。
|
||||
-498
@@ -1,498 +0,0 @@
|
||||
# 访问控制与入网信任链
|
||||
|
||||
这份文档定义**谁能做什么**(账户、角色、能力、API key)和**谁能让节点入网**(passkey 到节点的签名链)。它是 [security.md](security.md) 第 5、6 节的展开:security.md 讲为什么(缺口和威胁),这里讲怎么做。**状态:部分实现**(当前实现见第 0 节,完整设计的实施顺序见第 12 节)。
|
||||
|
||||
一句话:登录和授权分开。登录证明「你是谁」,密码、passkey、OIDC 都可以;改变信任(加节点、吊销、改凭证)只认 passkey 的签名,并且这个签名要一路传到节点,Backend 只搬运,伪造不了。
|
||||
|
||||
## 0. 当前实现(以这一节为准)
|
||||
|
||||
下面的第 1~16 节是完整设计,其中「成员证明」那一整套(Grant、Ticket、Binding、每个节点的入网链,第 7.1~7.3 节)**当前没有采用**:评估之后,被攻破的 Backend 往名册里塞节点拿不到比 Backend 已经有的更多的东西(监测数据本来就是明文给 Backend 的;敏感的 extension 靠节点属主,不靠名册),成本却是每个节点一次触碰、一套证明格式和一套自动化委托。现在实现的是它的精简版:
|
||||
|
||||
```text
|
||||
两个根,各管一摊
|
||||
根 A:CI(代码) 发布公钥写在安装脚本、宿主机更新器的本机配置和 Backend 里
|
||||
└─ CI 签名的发布清单 → 镜像 digest → 节点跑的二进制;安装脚本的 SHA-256 也记在清单里,并公布在发布页
|
||||
假设:CI(Woodpecker、Gitea、镜像仓库、发布密钥)可信;CI 被攻破等于能给所有节点签恶意更新
|
||||
根 B:你的 passkey(运维)
|
||||
└─ 操作员集合(passkey 签,版本递增;创世指纹在安装时固定到节点)
|
||||
├─ 操作员的 passkey 公钥
|
||||
├─ 服务器密钥:Backend 的身份密钥(节点据此认服务器,链路用应用层加密)
|
||||
└─ 域名(rp、origins)
|
||||
|
||||
添加节点:创建加入码不需要 passkey、不需要触碰,一个码可以装任意多台。命令里只有公开的值:
|
||||
面板地址、角色、一次性码、创世指纹、属主(passkey 凭证 id)、安装脚本的哈希(命令先核对哈希再执行)
|
||||
节点注册:向面板取操作员集合链,从命令里的创世指纹验起,属主必须是集合里的凭证;任何一步不对就不注册、不交出密钥;
|
||||
通过后写 trust.json(创世指纹、链、属主),之后只认从这个创世出发的新版本
|
||||
节点和 Backend 之间:集合里登记了服务器密钥之后,节点的认证挑战要求服务器用它签名;两边各出一把临时 ECDH 密钥,
|
||||
认证签名覆盖双方的临时公钥,之后控制链路每一帧都是 AES-256-GCM(每个方向一把密钥、严格递增的计数器)。
|
||||
所以 TLS 在 Cloudflare 或被冒充的证书那里终止,中间人没有服务器密钥,既冒充不了服务器,也读不了、改不了这条链路
|
||||
密码登录和 passkey 登录的会话拿同样的能力,授权只看能力;passkey 只用在节点自己会验证的事上(现在是操作员集合的签名,以后是 extension 会话和加密数据)。
|
||||
Backend 自己拦自己没有意义,所以没有提权窗口
|
||||
```
|
||||
|
||||
**发布密钥可以换**:见 `docs/deployment.md` 的「换发布密钥」;回滚已经由清单的 `seq` 防住(更新器只收比已安装更大的)。
|
||||
|
||||
**运行中的二进制不做校验**(第 16 节):节点自报的哈希不可信,来源证明在安装时(签名的清单和 `SHA256SUMS`)。
|
||||
|
||||
**还没有的**:吊销表;任务和配置内容的操作员签名;节点向别人**证明**自己运行的就是官方二进制(需要容器之外的背书,见第 16 节);extension 本身和属主的 `controllers`、接收者的实际加密(只是被固定在节点本地,没有行为)。
|
||||
|
||||
## 1. 名词
|
||||
|
||||
| 名词 | 含义 |
|
||||
| --- | --- |
|
||||
| 账户 | 一个人。有角色、若干登录方式、若干 passkey |
|
||||
| 角色 | `anonymous`、`admin`、`owner`,决定能力的**上限** |
|
||||
| 能力(capability) | 一项具体权限,如 `fleet.write`。端点声明自己需要哪项能力 |
|
||||
| 会话 | 登录得到的状态,只带 L1 能力 |
|
||||
| 操作员集合(OpSet) | 节点固定的、带版本的 passkey 公钥列表,每个带角色和限制 |
|
||||
| 委托(Grant) | passkey 签名授权一把临时密钥,在范围、时间内代表它签名 |
|
||||
| API key | 一串种子;`enroll` 类的 key 创建时由 passkey 签出一份委托 |
|
||||
| 入网券(Ticket) | 委托的密钥签的「允许一个持有 K_code 的节点入网」 |
|
||||
| 属主(controllers) | 一个节点绑定的一组 passkey:只有它们能授权这个节点上的 extension(web ssh 等)。管理员没有这份权力,见第 8 节 |
|
||||
|
||||
## 2. 角色与能力
|
||||
|
||||
### 2.1 能力表
|
||||
|
||||
| 组 | 能力 | 说明 | 信任类 |
|
||||
| --- | --- | --- | --- |
|
||||
| 展示 | `public.read` | 看隐藏线路;站点私有时看展示页 | |
|
||||
| 节点 | `fleet.read` | 看 Agent、Probe、线路、加入代码 | |
|
||||
| | `fleet.write` | 改名、标签、可见性、手填地址、线路开关、断开会话 | |
|
||||
| | `fleet.enroll` | 生成加入代码(签入网券);可以吊销**自己加的**节点 | 是 |
|
||||
| | `fleet.revoke` | 吊销或删除任意节点 | 是 |
|
||||
| 任务 | `tasks.read` · `tasks.write` | 任务模板、覆盖、发起测试 | |
|
||||
| 数据 | `data.read` · `data.write` | 原始块、导出;清理 | |
|
||||
| 设置 | `settings.read` · `settings.write` | 站点设置、保留策略、重启计划 | |
|
||||
| 更新 | `update.read` · `update.confirm` | 看 Backend 更新、确认更新 | |
|
||||
| 令牌 | `keys.read` · `keys.manage` | 列出、创建、撤销 API key | |
|
||||
| 账户 | `accounts.manage` | 账户、passkey、操作员集合 | 是 |
|
||||
| 运维 | `audit.read` · `ops.metrics` | 审计日志;`/metrics` | |
|
||||
| 集成(预留) | `events.read` · `data.export` | 拉取事件流和批量数据,见 5.4 | |
|
||||
| | `webhooks.read` · `webhooks.manage` | 外部回调订阅,见 5.4 | |
|
||||
|
||||
- **Backend 更新确认是 L1**:更新清单由 CI 的发布密钥签名,`seq` 防回滚,真正的门是发布密钥,不是确认这一步。
|
||||
- **一个能力只在一个地方声明**:`Capability` 常量(`MoShiro.Protocol`),端点用 `.Requires(Capability.X)`;`Principal.Has(capability)` 是唯一的判断入口,会话、API key、委托都走它。
|
||||
- **对账测试**:所有 `/api/admin` 端点必须声明能力,没声明测试就失败;OpenAPI 的每个操作带 `x-moshiro-capability`。
|
||||
|
||||
### 2.2 角色
|
||||
|
||||
| 角色 | 能力 |
|
||||
| --- | --- |
|
||||
| `anonymous` | 只有 `public.read`,且站点公开时才有。不是账户 |
|
||||
| `admin` | 除 `accounts.manage` 外的全部 |
|
||||
| `owner` | 全部;`accounts.manage` 每次都要当场触控 |
|
||||
|
||||
- 角色是上限:能力是否可用取决于角色。
|
||||
- **不能越权**:创建 API key 时勾选的能力必须是创建者当前拥有能力的子集;带信任类能力的 key 创建时要 passkey 签一次。
|
||||
- 角色写在操作员集合里,**节点自己执行**:admin 凭证签的入网券有效;只有 owner 凭证签的操作员集合更新才有效。被攻破的 Backend 给自己造一个 admin 账户也加不进节点。
|
||||
|
||||
## 3. 登录与会话
|
||||
|
||||
| 登录方式 | 能得到 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| passkey | L1 会话;还能给操作员集合签名(节点验证) | WebAuthn,只支持 ES256,attestation 选 `none` |
|
||||
| 密码 | L1 会话 | 保留用于首次引导和兜底;PBKDF2 |
|
||||
| OIDC | L1 会话 | 账户绑定 `iss + sub`,登录时按它找账户,不自动建账户。放在 passkey 之后做 |
|
||||
|
||||
- 密码和 OIDC 会话可以管理(有能力),但**进不了信任域**:节点验证不了 IdP 的断言,所以它们改不了操作员集合。
|
||||
- **引导(认领向导,没有默认账户)**:面板里还没有任何用户时,打开 `/admin/` 进向导——用后端日志里的认领码(或 `MoShiro__ClaimCode`)创建第一个管理员(用户名和密码自己填),注册第一个 passkey,再用它给操作员集合签名建立信任根。谁先打开页面谁就能当管理员,所以要认领码;创建完码作废、入口关闭(`POST /api/admin/auth/setup` 之后返回 409)。后两步是引导不是门槛,可以稍后补,没有 passkey 也能用密码登录管理。之后新增的 passkey 不会自动成为操作员:要由现有操作员在信任页看清「新增了哪些」再签新版本,节点才认(见第 7 节)。
|
||||
- 建议每个账户至少注册两个 passkey(手机加硬件密钥)。
|
||||
- **离线恢复密钥**:创建 owner 时同时生成一把离线的 ECDSA 恢复密钥,作为 `type=ecdsa`、`recovery=true` 的 owner 凭证写进操作员集合;私钥打印或放进密码管理器,平时不用。节点强制它**只能签操作员集合更新**,不能签委托、入网券和吊销表,所以即使被偷走,也加不了节点;它的用处是 passkey 全部丢失时签一版新的操作员集合、重新加 passkey,节点不用重装。
|
||||
|
||||
### 3.1 passkey 登录的实现(已完成)
|
||||
|
||||
- 流程:登录页「使用 Passkey 登录」→ `POST /api/admin/auth/passkey/options` 取挑战 → 浏览器 `navigator.credentials.get`(可发现凭证,不用输用户名)→ `POST /api/admin/auth/passkey/login` 提交断言 → 建立会话(Cookie,或跨域前端用 `return_session`)。注册:已登录用户在「系统 → 用户 → 我的 Passkey」添加,`POST /api/admin/auth/passkeys/options` 加 `POST /api/admin/auth/passkeys`;每个用户最多 10 个。
|
||||
- 校验:挑战在内存里,一次有效、5 分钟过期,并且区分注册和登录、绑定用户;`rpIdHash`、UP 和 UV 标志、`clientDataJSON` 的 type、challenge、origin、ES256 签名;签名计数器只增不减(两边都是 0 的同步型 passkey 不检查)。登录失败一律 401,不区分原因,和密码登录共用限流。
|
||||
- 验证断言的纯函数是 `Fleet.Identity.WebAuthnAssertion`(只用 BCL,节点以后验证 passkey 签出的委托也用它);注册里的 CBOR 和 attestation 解析在 Backend(`WebAuthnRegistration`),attestation 一律当 `none`,节点不带 CBOR。
|
||||
- 依赖方(RP):`MoShiro__WebAuthnRpId`(缺省取 `PublicUrl` 的主机,没有就取请求的主机)和 `MoShiro__WebAuthnOrigins`(缺省取 `PublicUrl` 的来源)。**passkey 绑定在这个域名上,改了会让已注册的 passkey 失效。**只有和后端同站的页面能用 passkey 登录,管理端在「添加其他后端」(跨域)时不显示这个入口。
|
||||
- 会话记录登录方式(`password` 或 `passkey`,`sessions.auth_method`),只用于审计和显示;API key 不是用户,不能注册 passkey。
|
||||
- 测试:`SoftwarePasskey` 在测试里模拟认证器走完整的注册和登录,覆盖错误的挑战、来源、RP、签名、缺少 UV、挑战重放、计数器回退、只能管理自己的 passkey;管理端用 Chromium 的 WebAuthn 虚拟认证器验证过注册和登录。
|
||||
|
||||
## 4. passkey 用在哪里(没有提权窗口)
|
||||
|
||||
原则:**passkey 不是 Backend 用来拦操作的一道门**(Backend 被攻破时,它自己做的拦截也会被一起绕过),而是节点或密钥持有者需要它才能验证、解密的东西。
|
||||
|
||||
| 事 | 谁验证 | 要 passkey |
|
||||
| --- | --- | --- |
|
||||
| 建立、更新操作员集合(信任根) | **节点**验链 | 是(现在唯一做了的) |
|
||||
| extension 会话(web ssh 等) | **节点**验 passkey 签名的临时密钥 | 是(还没做) |
|
||||
| 读加密存储的数据 | 私钥持有者 | 是(还没做) |
|
||||
| 吊销或删除节点、账号、API key、加入码、设置 | 只有 Backend | 否,只看能力 |
|
||||
|
||||
- 这些「只有 Backend」的事靠能力限权:API key 只带需要的能力,登录会话按角色。密码泄露的后果是这些能力被滥用,改不了信任根,也用不了以后的 extension。
|
||||
- 唯一的风险是往账号里塞一把 passkey 再等人签名:信任页的提案会列出**新增和移除的凭证**,签名之前要看一眼是不是自己加的;签了才算授权。
|
||||
|
||||
## 5. API key
|
||||
|
||||
### 5.1 类型
|
||||
|
||||
| 类型 | 创建时要不要 passkey | 能力 |
|
||||
| --- | --- | --- |
|
||||
| 普通 key | 不要 | 勾选的非信任类能力(只读、运维、CI 发布等),只能是创建者能力的子集 |
|
||||
| `enroll` key | **要,签一次** | 勾选的非信任类能力 + `fleet.enroll`(可吊销自己加的节点) |
|
||||
|
||||
- 现有令牌迁移(破坏性):`read` → 全部 `*.read`;`admin` → 全部非信任类能力;`release` 令牌已取消(CI 登记发布清单不需要令牌,见 `POST /api/release/offer`)。迁移后,旧的 admin 令牌不能再生成加入代码,需要换成 `enroll` key。
|
||||
- 每把 key 可设:有效期、可选的 IP 白名单(CIDR,**待确认**)、`enroll` 类的数量上限和单券 TTL 上限;预留:资源范围、速率和数据配额(见 5.4)。
|
||||
|
||||
### 5.2 `enroll` key:自动化添加节点
|
||||
|
||||
**当前实现:它就是一把带 `fleet.enroll` 的普通 API key**,用 `POST /api/admin/join-tokens` 创建加入码,响应里直接给出安装命令(带创世指纹、属主和脚本哈希,没有任何私钥)。因为创建码不是信任动作,所以不需要 passkey;CI 和脚本照常工作。信任在两处:命令里的创世指纹和属主由面板生成,节点注册时固定;以后改属主和集合要操作员 passkey 签名。
|
||||
|
||||
早先设计过的「委托密钥」(passkey 签一次委托,API key 自己签入网券,节点验证整条链)属于第 7.1~7.3 节的完整设计,当前没有采用,见第 0 节的取舍。
|
||||
|
||||
### 5.3 创建界面
|
||||
|
||||
- 名称、有效期、可选 IP 白名单;`enroll` 类多出「最多加几个节点」「单券有效期」。
|
||||
- 能力用分组复选框:全局**全选**(带半选状态)、**组内全选**、**反选**、搜索;预设按钮一键套用:只读、运维、接入、CI 发布、全部;超出创建者能力的项灰色不可选,需要 passkey 的项带标记(⚿),勾选后创建时提示。
|
||||
- 创建后只显示一次 key,同时列出它拥有的能力和过期时间。
|
||||
- 列表:API key、加入代码、节点、审计日志都支持多选和全选做批量操作(撤销、吊销、删除、筛选)。API key 列表显示能力数、最后使用时间、过期时间。
|
||||
|
||||
### 5.4 预留:外部 webhook 回调与数据拉取
|
||||
|
||||
后面要让外部系统能**订阅事件、触发回调、拉数据**。接口和权限现在先留,实现放后面;全部发生在 Backend 里,**节点零开销**。
|
||||
|
||||
- **能力(已列入能力表,标预留)**:
|
||||
- `events.read`:拉取事件流,游标分页(`GET /api/admin/events?after=<cursor>`),适合外部系统轮询。
|
||||
- `data.export`:按线路和时间范围批量拉数据,有限速和配额;`data.read` 仍然是交互式查询。
|
||||
- `webhooks.read` · `webhooks.manage`:创建、修改、删除外部回调订阅,以及手动触发一次测试回调(`POST /api/admin/webhooks/{id}/test`)。
|
||||
能力在目录里先存在,旧 key 以后可以直接带上它们,不需要再迁移。
|
||||
- **key 上预留的字段**:
|
||||
- `resource_filter {agent_ids?, labels?}`:限制这把 key 能看到哪些节点、线路;事件和 webhook 同样受它限制。
|
||||
- `rate_limit`(每分钟请求数)和 `quota`(每天拉取的行数或字节数):超出返回 429 带 `Retry-After`,计入审计。
|
||||
- **webhook 的形状**:订阅 `{id, url, events[], secret, 创建者(key 或账户), enabled}`。事件:节点上线或下线、线路告警阈值、安全事件(节点准入、吊销、key 创建)等。投递是 `POST` JSON,头带 `X-MoShiro-Signature: sha256=<HMAC(secret, 时间戳 ‖ body)>`、时间戳和投递 id;至少一次投递、指数退避重试、接收方按投递 id 幂等。
|
||||
- **订阅受创建者能力约束**:只能订阅创建者自己读得到的事件,不能借 webhook 越权。
|
||||
- **SSRF 约束**(TODO 里已有):URL 解析后检查地址,只放行公网地址;不跟随重定向到内网;连接和响应有超时和大小上限;`secret` 只在创建时显示一次。
|
||||
- 这些都属于 L1:改的是 Backend 的集成配置,不改变节点信任;但每次增删订阅和改 URL 都进审计日志。
|
||||
|
||||
## 6. 归属与审计
|
||||
|
||||
- 账户表:`id`、名字、角色、登录方式、passkey;加入代码记 `created_by`,节点记 `added_by`;管理页的 Agent、Probe 详情显示「由 xxx 于 xxx 添加」。
|
||||
- 审计日志 `(时间, 账户, 凭证 id, 动作, 对象, 详情)`:创建或撤销 key、加入代码、吊销、删除、手填地址、确认更新、因能力不足被拒绝的请求。保留 365 天(同事件表)。
|
||||
- **可验证的一层**:委托里写进签名它的 passkey 凭证 id,入网券和节点绑定带着它,节点在 hello 里报告「由哪个凭证准入」。「谁加的」不只是 Backend 的一面之词。
|
||||
|
||||
## 7. 入网信任链(完整设计,当前没有采用;当前实现见第 0 节)
|
||||
|
||||
> 第 7.1~7.3 节描述的 Grant、Ticket、Binding、成员证明和节点验证在代码里**不存在**。要不要叠加回来,取决于你要不要防「被攻破的 Backend 往名册里塞节点」;操作员集合和创世固定已经在,叠加时不用重装节点。
|
||||
|
||||
### 7.1 文档
|
||||
|
||||
```text
|
||||
① 操作员集合 OpSet {version, creds:[{id, type=webauthn|ecdsa, 公钥, 角色, 账户, recovery?, max_ticket_ttl, 作用域}]}
|
||||
签名人:owner 的 passkey;创世版本由安装时固定的第一个 owner 公钥自证
|
||||
② 委托 Grant {grant_id, 委托公钥 D 或 P, 作用域, 额度, not_before, not_after, 签发凭证 id}
|
||||
签名人:passkey(WebAuthn 断言,challenge = SHA256(Grant))
|
||||
③ 入网券 Ticket {ticket_id, K_code 公钥, 角色, controllers, issued_at, expires_at, grant_id}
|
||||
签名人:委托私钥。生成加入代码时签,用户那时不用在场
|
||||
④ 节点绑定 Binding {node_id, 角色, 节点密钥公钥, 对等面公钥, ticket_id} (controllers 随 Ticket 一起被签名,节点绑定引用它)
|
||||
签名人:K_code 私钥。它就在安装命令里,节点注册时用一次
|
||||
⑤ 吊销表 RL {version, issued_at, 被吊销的节点公钥指纹[]}
|
||||
签名人:passkey 当场签;`enroll` 委托的密钥只能签针对「自己加的节点」的条目(节点校验条目指向的 Binding 属于这份委托)
|
||||
```
|
||||
|
||||
- 安装命令里的代码就是 `K_code` 的种子;Backend 的一次性加入令牌由它派生,所以命令仍然是一行。
|
||||
- 所有签名带用途前缀(域隔离),规范化编码;格式不协商、不降级。
|
||||
|
||||
### 7.2 节点验证(本地,Backend 不参与信任)
|
||||
|
||||
节点固定创世 owner 公钥。链随期望状态下发(Agent 的 `peers`、Probe 的 `probe_agents` 里每个成员带着自己的链),节点在**状态同步时**验,每个成员验一次、结果进成员表;对等握手(敲门加现有的对端认证)完全不变,没有额外字节和运算。Agent 接纳 Probe、Probe 接纳 Agent,各自验同一条链:
|
||||
|
||||
1. OpSet:从创世公钥起,每个版本由 owner 凭证签、版本递增;节点持久化见过的最大版本。
|
||||
2. Grant:签发凭证在 OpSet 里且角色够;WebAuthn 断言有效(`rpIdHash`、UP/UV 标志、`clientData.type = webauthn.get`、`challenge = SHA256(Grant)`、ES256 验签);当前时间在有效期内。
|
||||
3. Ticket:由 Grant 的委托公钥签;`issued_at` 落在 Grant 窗口内;`expires_at − issued_at ≤ max_ticket_ttl`(来自 OpSet 里签发凭证的限制);节点当前时间 ≤ `expires_at`。
|
||||
4. Binding:由 Ticket 的 `K_code` 签;节点密钥和对等面公钥与这个成员条目里的一致;角色匹配(Probe 只能是 probe)。握手时对端证明自己持有这把对等面密钥,用的是现有的认证。
|
||||
5. RL:指纹不在最新吊销表里(持久化见过的最大版本)。
|
||||
|
||||
通过后记进本地成员表(持久化,离线也认),同时进入敲门的名册(`KnockGate` 的 `peers`);没通过的成员条目直接丢弃并计数,不回详细原因。Probe 只拨公网地址(拒绝私网、回环、链路本地),解决「被攻破的 Backend 让 Probe 探测客户内网」。
|
||||
|
||||
### 7.3 能强制什么,不能强制什么
|
||||
|
||||
- 节点能强制:谁是成员(链)、时间(Grant 窗口、Ticket 过期、单券 TTL 上限)、角色、吊销。
|
||||
- 节点强制不了:数量上限(它不知道别的节点)。由签名人执行(浏览器或 Backend 的 key 路径)。
|
||||
|
||||
### 7.4 现状的缺口:节点没有任何东西可以用来识别 Backend
|
||||
|
||||
今天的链路(已核对代码):
|
||||
|
||||
```text
|
||||
安装 curl https://<面板>/install?code=jt_… | bash 信任 HTTPS 证书,第一次联系即信任(TOFU)
|
||||
注册 Agent 生成 node.key 和 tls.pfx,用 jt_ 码把公钥交给 Backend
|
||||
连接 Backend 发挑战,Agent 用 node.key 签名 只有「Agent 向 Backend 证明自己」
|
||||
反方向 Agent 验证 Backend?没有。只靠 wss 的 WebPKI 证书;节点本地只存 node_id、面板地址、自己的证书指纹
|
||||
```
|
||||
|
||||
后果:Backend 的身份就是「能让 Agent 连上的、证书有效的服务器」。DNS 或前置代理被攻破、被签发了该域名的证书、或 Backend 机器被拿下,攻击者都等于 Backend,可以改 Agent 的 `peers` 名单(把自己的 Probe 公钥放进去)、下发任务、读全部数据。「换一个 Backend 公钥配自己的私钥」在这里甚至不需要,因为节点从来没有存过 Backend 的公钥。
|
||||
|
||||
要求:信任根必须是操作员的 passkey 公钥,在节点**第一次安装时固定**,之后 Backend 无法替换:
|
||||
|
||||
- 节点收到的所有「改信任」的内容(成员名单、授权、吊销表)都必须带操作员的签名,节点自己用固定下来的创世公钥验证。Backend 只能转发,签不出新的。
|
||||
- Backend 被冒充也只能转发操作员签过的东西:已有的授权可以重放(受有效期、吊销表约束),新授权签不出来。
|
||||
- 弱点:创世公钥指纹放在安装命令里,命令本身要从可信渠道拿到(管理端复制)。改了命令的人可以换指纹;缓解见第 15 节(静态签名脚本、按 digest 拉镜像),并让管理端展示指纹供核对。
|
||||
|
||||
### 7.5 第一个落地切片(重启、改配置的票和 webssh 先不做,只留接口)
|
||||
|
||||
| 步 | 内容 | 验证 |
|
||||
|---|---|---|
|
||||
| a | 创世:第一次用 passkey 登记操作员公钥(OpSet v1,自证);安装命令带上指纹 | 命令里的指纹和管理端展示一致 |
|
||||
| b | Grant:生成加入码时由 passkey 签(`challenge = SHA256(Grant)`);Agent 和 Probe 注册时验签,通过才保存操作员公钥 | 没有签名或签名不对的码,节点拒绝注册 |
|
||||
| c | 成员名单带签名:Agent 的 `peers`、Probe 的 `probe_agents` 每项带自己的链,节点在状态同步时验,不过就丢弃并计数 | 冒充的 Backend 往名单里加 Probe 公钥,节点不接纳 |
|
||||
| d | 数据加密(PRF)、`enroll` key、Ticket 作为后续,接口先留在 Grant 里(`controllers`、数据公钥字段) | |
|
||||
|
||||
不在这个切片里:重启和改配置不需要签名的票;webssh 等 extension 见第 8 节。
|
||||
|
||||
### 7.6 当前实现要点
|
||||
|
||||
- 语句是 `key=value` 文本,第一行是带版本的标题,签名覆盖整段文本;WebAuthn 的挑战是语句的 SHA-256。库在 `Fleet.Identity` 的 `OperatorTrust`(操作员集合的编码、解析、链验证)和 `SecureChannel`(服务器证明和链路加密),都是只用 BCL 的纯函数,节点和 Backend 共用。
|
||||
- **创世**:第一次建立信任根时,把账号下所有 passkey 和这个面板的服务器密钥登记成一个操作员集合,passkey 对它签名;创世语句的 SHA-256 就是命令里的 `MOSHIRO_GENESIS`。之后加删 passkey 或换服务器密钥要签新版本(上一版里的凭证签),页面会提示;签名更新不会让节点重装。
|
||||
- **操作员集合版本**随期望状态(`trust_chain`)下发,节点从固定的创世验起,版本比现有的新才采用并保存。
|
||||
- **节点注册**:`fleet-enroll --genesis … --controllers …`:先 `POST /api/agent/v1/enroll/trust` 取链,验过(创世指纹、签名、属主在集合里)才把密钥交出去;之后写 `trust.json`。没有命令里的指纹(`--genesis`)就不固定,节点按旧方式工作。
|
||||
- **服务器身份和安全链路**:`ChallengeParams` 带服务器的临时公钥和签名(`server_eph`、`server_sig`),`AuthParams` 带节点的临时公钥(`node_eph`),节点签名的用途是 `auth2`,覆盖双方的临时公钥;通过后两边用 HKDF 派生每个方向一把 AES-256-GCM 密钥,每帧 `计数器(8) ‖ 密文 ‖ 标签(16)`,计数器严格递增,明文帧、改动、重放、乱序一律断开。集合里没有服务器密钥的节点(旧节点)保持明文,不会被降级成放行。
|
||||
- **服务器密钥**存在 Backend 的设置里(`server.identity_key`):它防的是面板被冒充,不是面板被攻破;面板被攻破时它也泄露,那时要由 passkey 签一个换掉它的新集合版本。集合里没有这个面板的服务器密钥的版本,Backend 不收(否则固定了它的节点就认不出这个面板了)。
|
||||
- **安装脚本是静态的**:不替换任何值,所以 SHA-256 固定;CI 把哈希写进签名的发布清单和发布页,面板的信任页显示它和清单是否一致;命令先下载、核对哈希,一致才执行。
|
||||
- **密码登录和 passkey 登录的会话能力相同**;创建加入码需要 `fleet.enroll`,没有提权窗口。passkey 只用在节点自己会验证的事上。
|
||||
- **现状的缺口**:没有吊销表;删除一把操作员 passkey 只是让它不能再签新版本;任务和配置没有操作员签名;节点没有自证构建;服务器密钥换新要靠签一个新集合版本,忘了签会让固定了集合的节点认不出面板(fail closed)。
|
||||
|
||||
## 8. 节点属主与 extension 授权(预留)
|
||||
|
||||
后面会做 extension(web ssh、文件、抓包,见 [security.md](security.md) 第 11 节)。这类能力**连管理员都不信任**:管理员能删,不能读,也不能用。所以入网链现在就要给每个节点留出「属主」这个概念,即使 extension 还没做,字段也先固定下来,之后不用重装节点。
|
||||
|
||||
```text
|
||||
属主 controllers = 一组 passkey 凭证 [{credential_id, sign_pubkey(ES256), enc_pubkey?}]
|
||||
默认:添加者(签入网券的那个账户)的全部 passkey
|
||||
添加节点时可以从已有的 passkey 里选(含全选):允许为该节点绑定一组 passkey,而不是只绑一个人的一把
|
||||
写在哪:入网券里(由添加者的委托签名),节点绑定引用它;节点本地持久化,随成员表一起保存
|
||||
|
||||
extension 的授权(每次调用)
|
||||
操作员签名的一次性能力令牌 {节点 ID, extension, 命令哈希, not_before, not_after, nonce}
|
||||
签名必须是属主里某把 passkey 的 WebAuthn 断言(challenge = 令牌哈希),节点用自己存的属主公钥验
|
||||
—— 不是「操作员集合里任意 admin/owner 的 passkey」,而是「这个节点的属主」
|
||||
|
||||
谁能做什么
|
||||
属主 启用或使用 extension;增删属主(需要现有属主中的一把签名的 ControllerUpdate)
|
||||
admin / owner 只能删除节点或卸载 extension(吊销表,照常当场 passkey);读不到、用不了
|
||||
Backend 只搬运。交互式会话的内容端到端加密(浏览器 ↔ extension),Backend 看不到明文
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- **删除和使用分开**:吊销表没有「使用」的含义;账户角色(owner、admin)不赋予任何 extension 权力。owner 想用某个节点的 web ssh,也得是那个节点的属主。
|
||||
- **改属主不经过 Backend 的账户系统**:`ControllerUpdate {node_id, version, controllers}` 由现有属主的 passkey 签名,节点持久化见过的最大版本;账户被删除、角色被改都改不了某个节点的属主。
|
||||
- **属主是 passkey 凭证,不是账户**:账户只是 Backend 里给人看的标签;节点只认公钥。一个人丢了全部属主 passkey,这个节点上的 extension 就没人能用了,要用就得重装节点(和第 3 节的恢复一致),所以添加时建议绑两把以上。
|
||||
- 属主字段在**步骤 4(入网链)**就写进 Ticket;extension 本身、ControllerUpdate 和能力令牌放在阶段六。
|
||||
|
||||
### 8.0 范围:这一节先固定协议和要求,extension 本身先不做
|
||||
|
||||
- **web ssh、文件、抓包等 extension 现在都不实现**,Agent 和 Probe 一样(Probe 是节点,同样要在本地执行下面的要求)。这里只把「谁来强制」和「怎么证明」定下来,以后实现时不改信任链,也不用重装节点。
|
||||
- **已经固定在节点本地的字段**:`controllers`(允许授权本节点 extension 会话的 passkey 凭证 id,必须是操作员集合里的凭证)由安装命令带来,节点注册时保存在 `trust.json`,Backend 之后改不了;从集合里去掉的凭证不再有效。**每个 Agent 和 Probe 都必须有属主,至少一把——而且这是节点自己的要求,不是 Backend 的要求**:节点启动时检查本地固定的属主,没有(或者属主的 passkey 已经不在操作员集合里)就不运行(Agent 和 Probe 共用 `Fleet.Hosting` 里的 `OwnerGuard`;配置 `fleet.require_owner`,`MOSHIRO_ALLOW_NO_OWNER=1` 放行)。面板被攻破也改不了节点本地的属主。属主是节点敏感内容(extension 会话、加密数据)绑定的人——这些内容只有属主的 passkey 能授权、解密,没有属主的节点没有人能用它的敏感能力,也就不该存在。接收者名单(`recipients`)等做加密时再加。节点还没有 extension,所以现在只是被固定和保存,不起别的作用。
|
||||
|
||||
### 8.1 添加节点时选属主
|
||||
|
||||
- API:`POST /api/admin/join-tokens` 的 `controllers: [credential_id…]`:不给就是操作员集合里的全部凭证;给了就必须非空,且每个 id 都是操作员集合里的凭证。还没有建立信任根、或者一把属主都没有时,面板**提前拒绝**创建加入码(400,字段 `controllers`)——这只是尽早失败,免得装出一个起不来的节点;真正的强制在节点(上面)。`MoShiro__RequireOwner=false` 可以关掉这个提前检查(只给没有信任根的老部署)。
|
||||
- **逆推出绑定关系**:节点握手时把本地固定的属主(passkey 凭证 id)报给面板(hello 的 `owners`),面板顺着凭证 id 反查出它绑定在**哪个账户的哪把 passkey** 上,显示在「系统 → 版本」的「属主」列(对不上任何已注册 passkey 的显示「未知凭证」)。上报只是自述,真正的授权由节点自己校验 passkey 签名;以后给节点更多的接口和操作(extension 会话、加密内容)时,据此决定谁有资格用,不会越权到别人的节点。
|
||||
- 界面:「添加 Agent / Probe」对话框里一个「属主(passkey)」多选列表,**必选**(一把都不选时创建按钮不可用);只是选,不用触碰。选定的写进安装命令,节点装上时固定。
|
||||
- 安装端也强制:`install.sh`、`install.ps1`、容器入口缺了 `MOSHIRO_GENESIS` 或 `MOSHIRO_CONTROLLERS` 就不装(只有没有信任根的老面板才用 `MOSHIRO_ALLOW_NO_OWNER=1`);`fleet-enroll --genesis` 不带 `--controllers` 也拒绝。
|
||||
- 设计意图(还没做):属主的 passkey 是敏感内容的钥匙。节点和 Probe 这一侧要在宿主侧拿到属主 passkey 的证明,才可以解密、才可以开 extension 会话;Backend 在这里只是搬运,见第 8.2、8.4 节。
|
||||
- 以后改属主要现有属主的 passkey 签名(`ControllerUpdate`,没有实现);管理员改不了。
|
||||
|
||||
### 8.2 谁来强制:节点本地,不是 Backend
|
||||
|
||||
web ssh 这类会话要不要加密、要不要验证,**由 Agent(和 Probe)自己要求**,不是 Backend。Backend 是被防的一方:让它决定,被攻破的 Backend 一句「这次不用加密」或自己充当会话的一端就绕过去了。所以要求落在节点本地,Backend 只能转发:
|
||||
|
||||
| 节点本地强制的要求 | 为什么不能交给 Backend |
|
||||
|---|---|
|
||||
| 没有操作员的现场证明就不启动会话 | Backend 说「已经授权过了」不算数 |
|
||||
| 会话密钥只和证明里的那把密钥协商,不和 Backend 转来的任何公钥协商 | 否则 Backend 换一把自己的公钥就成了中间人 |
|
||||
| 加密字段缺失就拒绝,没有明文降级 | 防降级 |
|
||||
| 能开哪些 extension、哪些 passkey 能授权,只看本节点属主(passkey 签的 `controllers`) | Backend 改不了属主,账户角色也不赋予任何权力 |
|
||||
| 这些要求写在节点本地(编进二进制或本地 `trust.json`),Backend 下发的配置、任务、更新清单都覆盖不了 | 配置是 Backend 能碰的东西 |
|
||||
|
||||
**要求证明的方向是反的:节点向请求者要证明,不信任 Backend 的转述。** 一次会话由节点先出题(nonce 加它的临时公钥,用节点身份密钥签名),浏览器把这个 nonce 和自己的临时公钥一起放进 passkey 断言的挑战里(见 8.3 的流程);节点验:断言来自固定的操作员集合里的凭证、要求用户验证、挑战里的节点 id、extension、过期时间都对得上、这把凭证在本节点的 `controllers` 里、nonce 是它自己刚出的且没用过。任何一条不满足,节点直接拒绝,不看 Backend 的任何说法。
|
||||
|
||||
**Backend 在这件事里仍然管**:谁有资格发起请求(一项 `extensions.use` 能力)、审计、限流、转发密文。这些只挡「不该问的人」;真正的放行始终是节点本地验过证明之后。
|
||||
|
||||
**交互式会话和静态数据用不同的密钥**:
|
||||
- 交互式会话(终端、ssh):现场签名的临时密钥协商,**不依赖 PRF**,passkey 只用来签名授权,不参与解密,所以不支持 PRF 的 passkey 也能用,还有前向保密。
|
||||
- 静态数据(抓包文件、会话录像):需要事后解密,才用接收者名单里的公钥加密(`recipients`,来自 WebAuthn PRF 或浏览器里的不可导出密钥,见 8.3)。
|
||||
|
||||
### 8.3 数据用属主的私钥解密:Backend 解不开,也改不了
|
||||
|
||||
目标:交互式会话(web ssh、终端)、以及以后 extension 产生的数据(文件、抓包),**只有属主 passkey 对应的私钥能解开**。Backend 被篡改或被攻破,拿到的只有密文,且改动密文会被发现。
|
||||
|
||||
```text
|
||||
浏览器(属主) Backend(只搬运) 节点上的 extension
|
||||
⓪ 节点先出题:nonce 和它的临时公钥 n,签名后经 Backend 转给浏览器(见 8.2:要求证明的是节点)
|
||||
① 生成会话临时 ECDH 密钥 e
|
||||
② passkey 对「e 的公钥 ‖ 节点 ID ‖ 命令哈希 ‖ 节点出的 nonce ‖ not_after」做 WebAuthn 断言(能力令牌)
|
||||
──────────── 能力令牌 + e 公钥 ───────▶ 转发 ─────────────────────────▶ ③ 验:断言来自本节点属主
|
||||
+ 令牌新鲜、nonce 未用
|
||||
◀── 节点的会话公钥(节点身份密钥签名)── 转发 ◀────────────────────────── ④ 节点生成临时密钥 n,签名后返回
|
||||
⑤ 双方 ECDH(e, n) → HKDF(绑定两边公钥、令牌哈希、用途标签)→ 会话密钥
|
||||
⑥ 之后双向都是 AEAD 密文(ChaCha20-Poly1305 或 AES-GCM,BCL 自带),每帧带序号,
|
||||
序号进 AAD,重放、乱序、丢帧、篡改都会解密失败
|
||||
```
|
||||
|
||||
- 终端输出等**从节点流向属主的数据**,只能被拿着会话私钥 e 的浏览器解开;Backend 没有 e,也没有节点的 n,算不出会话密钥。
|
||||
- 临时密钥带来前向保密:长期的 passkey 只用来**签名授权**,不参与解密,所以以后 passkey 泄露也解不开历史会话。
|
||||
- Backend 篡改密文、重放、乱序:AEAD 校验失败,会话立即断开并计数。Backend 仍然能做的:拒绝服务、延迟、丢包,以及看到元数据(谁连哪台节点、什么时候、流量大小)。
|
||||
- **静态数据(以后的抓包文件、会话录像)**需要事后解密,不能只靠临时密钥:属主记录里的 `enc_pubkey`(P-256 ECDH 公钥)就是为此预留的,数据用它封装(ECIES:临时密钥 + HKDF + AEAD)。私钥有两种来源,按设备能力选:
|
||||
1. **WebAuthn PRF 扩展**:由 passkey 派生私钥种子(浏览器用 `prf` 取 32 字节,经 HKDF 得到 P-256 私钥);私钥不落盘,每次要解密时再触控一次。设备不支持 PRF 就退到下一种。
|
||||
2. **浏览器里的不可导出密钥**:添加属主时在这台浏览器生成,只能在这台浏览器用,丢了就解不开。
|
||||
属主记录里没有 `enc_pubkey` 的节点只支持实时会话,不能存加密的静态数据。
|
||||
- 加密发生在属主这一侧的浏览器和节点上的 extension 之间,不经过 Backend 的任何账户、角色、会话机制,所以改 Backend 的数据库或代码也无从下手。
|
||||
|
||||
### 8.4 数据边界:哪些明文,哪些加密,谁能解
|
||||
|
||||
| 数据 | 形态 | 谁能读 |
|
||||
|---|---|---|
|
||||
| **监测数据**(延迟、丢包、吞吐曲线、事件、主机资源汇总) | **明文**:Backend 要做分析、聚合和展示页 | 有 `data.read` 等能力的人和 key |
|
||||
| **extension 会话**(web ssh、文件、抓包等,以后)、原始 payload | **端到端加密**:节点加密给接收者名单里的公钥,Backend 只存和转发密文 | 只有接收者私钥的持有者 |
|
||||
|
||||
- **读取权不是能力,是私钥**:Backend 不持有任何接收者私钥,所以 `data.read`、`data.export` 对加密流只返回密文。API key 解不开,除非你登记过读者密钥。
|
||||
- **接收者名单由操作员固定**(做加密时和 `controllers` 一样,写进安装命令并固定在节点本地;现在还没有加密功能):
|
||||
- 默认只有操作员自己的 passkey(经 WebAuthn PRF 派生的公钥)。
|
||||
- 接入密钥签出来的码,名单必须和操作员签的接入委托完全一致:它能让节点加入,不能让节点把数据加密给自己。
|
||||
- **读者密钥**(预留):自动化想读明文,要操作员用 passkey 显式登记一把读者公钥(范围、有效期),私钥由读者自己保管;吊销只管之后的数据,已经加密给它的历史数据它仍然能解。
|
||||
- 管和读互不相通:passkey 或接入密钥能让节点加入和批准,但不能解密;解密靠私钥,Backend 被攻破也拿不到。
|
||||
|
||||
## 9. 流程
|
||||
|
||||
| 流程 | 步骤 |
|
||||
| --- | --- |
|
||||
| 引导 | 初始 owner 密码登录 → 注册第一个 passkey → 一行安装命令带创世公钥 → 节点首次注册时固定(和加入代码同一信任通道) |
|
||||
| 加节点(人) | 接入窗口内点「添加」→ 浏览器用 D 签入网券 → 生成命令(含 K_code 种子)→ 用户一行安装 → 节点用 K_code 签 Binding → Backend 存好并随 `peers`、`probe_agents` 下发。人不在场也行(券在有效期内);过期就重新生成 |
|
||||
| 加节点(自动化) | `enroll` key 调 `POST /api/admin/join-tokens` → Backend 用种子现算签名密钥签券 → 其余同上 |
|
||||
| 吊销 | 吊销窗口内当场 passkey 签 RL → 全部节点持久化最大版本 → 被吊销的节点连不进来,旧表复活不了它。`enroll` key 可以吊销自己加的节点 |
|
||||
| 增删 passkey、换角色 | owner 当场触控签新 OpSet 版本 → 节点自己同步,不用重装 |
|
||||
| 恢复 | passkey 全丢:用离线恢复密钥签新的操作员集合,重新加 passkey(节点不用重装)。恢复密钥也丢了才重装节点并换创世公钥(部署脚本 `--operator-key`) |
|
||||
| 弱信任(开发、实验室、升级过渡) | 节点安装时显式选择**不固定操作员**,行为回到「信任 Backend」;管理页标「弱信任」。这是显式降级,不会静默发生 |
|
||||
|
||||
## 10. 存储
|
||||
|
||||
- Backend:`accounts`、`credentials`(passkey 公钥、计数器)、`sessions`、`api_keys`(认证值哈希、能力、委托、IP 白名单、有效期、最后使用)、`grants`、`tickets`、`opsets`、`revocations`、`audit_log`;`join_tokens`、`agents`、`probes` 加归属列和 Binding。
|
||||
- 节点本地(`ConfigStore`):创世公钥、见过的最大 OpSet 和 RL 版本、成员表(含准入凭证 id)。
|
||||
|
||||
## 11. 落点(不新增项目)
|
||||
|
||||
| 机制 | 落点 |
|
||||
| --- | --- |
|
||||
| WebAuthn 断言、OpSet、Grant、Ticket、Binding、RL 的编码和验签(纯函数,只用 BCL 的 ECDSA) | `Fleet.Identity` |
|
||||
| `trust` 状态类型(OpSet + RL)、成员条目带链、同步时验证、成员表持久化 | `Fleet.Link` 的 `StateHandler`(对等握手不变) |
|
||||
| 能力常量、`Principal.Has`、端点 `.Requires`、对账测试 | `MoShiro.Protocol`、`Backend.Api` |
|
||||
| 账户、passkey 注册与登录、审计、API key | `Backend.Application`、`Storage.Sqlite`、`Backend.Api` |
|
||||
| 管理端:passkey 登录与注册、信任页(列出新增和移除的凭证)、API key 创建界面(预设、全选)、审计页 | `web/admin` |
|
||||
| 安装命令带创世公钥和 `K_code`;`fleet-enroll` 签 Binding | `packaging/install`、`Fleet.Enrollment.Client` |
|
||||
|
||||
## 12. 实施顺序(每步可独立验证和推送)
|
||||
|
||||
1. **能力模型与守卫**:`Capability`、`Principal.Has`、端点声明、对账测试、API key 创建界面(预设、全选、能力子集校验)。不改节点。**已实现**(破坏性:旧令牌作废,数据库迁移 v11 重建 `api_tokens`)。登录会话在步骤 2 之前拥有全部能力(包括信任类),信任类的 passkey 要求从步骤 2 起强制。IP 白名单推迟:本部署经 Cloudflare、frp 和 Caddy,后端看到的来源地址是代理,需要先有「可信代理」配置,才做得对。`events.read` 对应现有的 `GET /api/admin/events`,不是预留。
|
||||
2. **账户、passkey、会话分级、审计**:WebAuthn 注册与登录、审计日志、归属列和详情页显示。**已实现**:passkey 登录(第 3.1 节)、审计日志(`GET /api/admin/audit`,写操作和登录自动记录,保留 365 天)、归属(加入代码的创建者、节点的添加者)。
|
||||
3. **操作员集合与创世公钥固定**(落地切片见第 7.5 节):`fleet-enroll` 和安装命令改造、弱信任开关、离线恢复密钥的创建与导出。
|
||||
4. **入网链**:Grant、Ticket(含属主 `controllers`)、Binding 的签发和节点验证(验证一次、缓存结果);成员条目随期望状态下发、节点在状态同步时验证;Probe 只拨公网地址;`enroll` key;节点信任状态上报和管理页显示(第 14 节)。添加节点的界面可以选属主(默认添加者的全部 passkey)。
|
||||
5. **吊销表与操作员集合更新**;自动化夹具(测试里的软件 ECDSA 凭证);**安装脚本的供应链**(第 15 节:静态签名脚本、按 digest 拉镜像)。
|
||||
|
||||
实现状态:能力模型、passkey 登录、审计、操作员集合与创世固定、服务器身份和安全链路、静态安装脚本和哈希核对、属主固定都已实现(第 0 节);成员证明(第 7.1~7.3 节)没有采用;吊销表、任务和配置的操作员签名、节点自证构建、extension 属主的实际强制还没有做。
|
||||
|
||||
## 13. 局限
|
||||
|
||||
- 数量上限只能由签名人执行,节点只强制时间;窗口过期后设备被攻破的人,最多还能多签有效期内的入网券(受节点时钟约束)。
|
||||
- 节点的时钟被攻破或严重偏差会影响 Ticket 过期判断。
|
||||
- WebAuthn 只验 ES256 和 attestation = `none`,不验设备证明。
|
||||
- passkey 和恢复密钥都丢失才需要重装节点;某个节点的属主 passkey 全部丢失,这个节点上的 extension 就没人能授权。
|
||||
- `enroll` key 是持有者凭证:谁拿到种子 S,谁就能在它的范围和有效期内加节点,必须按密码对待。
|
||||
|
||||
## 14. 节点信任状态上报
|
||||
|
||||
Backend 被篡改时,你需要一个**不经过 Backend 账户系统**的信号。节点把自己的信任状态报给面板,面板显示对比:
|
||||
|
||||
- 节点在 `hello`、状态变化时和每 5 分钟(不进每次心跳)上报:创世公钥指纹(前 8 字节)、见过的操作员集合版本和吊销表版本、成员数、是否弱信任、各原因的拒绝计数。每次不到 100 字节。
|
||||
- 面板的节点列表显示一个信任状态:**正常**、**版本落后**(节点的操作员集合或吊销表版本低于面板持有的)、**弱信任**、**时钟偏差过大**(超过入网券 TTL 的一半,或超过 60 秒;面板用 `hello` 的 `NowUtcMs` 算)、**拒绝计数上升**、**超过 10 分钟没有上报**。
|
||||
- 这些是节点经认证的链路上报的,被攻破的 Backend 伪造不了,但可以压住不显示——所以「长时间没有上报」本身也是红色,面板同时显示 Backend 自己持有的版本和节点上报的版本,对不上就是信号。
|
||||
- 被攻破的节点可以说谎,但它说不了别的节点的状态。
|
||||
|
||||
## 15. 安装脚本的供应链
|
||||
|
||||
一行安装要执行 Backend 吐出的脚本、拉仓库里的镜像。Backend 或仓库被攻破,就能在新节点上执行任意命令。收束办法,**优先级最高的是第一跳的哈希核对**:
|
||||
|
||||
**① 脚本是静态产物,哈希固定(优先做)**
|
||||
- 不在服务端做字符串替换。CI 构建时算出 `install.sh`、`install.ps1` 的 SHA-256,写进发布清单(发布密钥签名),同时公布在 Gitea 发布页——**这条渠道独立于 Backend**,这是哈希核对有意义的前提:哈希若和脚本从同一处来,攻击者两个一起换就行。
|
||||
- 参数(代码、面板地址)通过命令行传入,代码也因此不再出现在 URL 和反向代理日志里。
|
||||
- 管理端给的命令把「下载、核对、执行」合成一行,哈希核对不通过就不执行:
|
||||
|
||||
```text
|
||||
curl -fsSL https://panel/install -o /tmp/moshiro-install.sh && echo "<sha256> /tmp/moshiro-install.sh" | sha256sum -c - && sh /tmp/moshiro-install.sh '<code>'
|
||||
```
|
||||
|
||||
macOS 用 `shasum -a 256 -c`,Windows 用 `Get-FileHash` 比较,不通过就 `throw`。命令里的 `<sha256>` 由 Backend 填入,所以管理端同时标出「与 Gitea 发布页的 SHA-256 一致」并提供链接;严格的人对照一次即可(脚本是静态的,哈希在一个版本内不变,只需要核对一次)。
|
||||
- 进一步:操作员确认这个版本的发布时(`update.confirm`,要 passkey),把脚本哈希一起签进去,Backend 被攻破也改不了命令里的哈希;节点和安装命令里的创世公钥指纹(第 7.5 节)来自同一次签名。
|
||||
|
||||
**② 镜像按 digest 拉取,二进制按 SHA-256 校验**
|
||||
- 脚本向面板取已签名的清单(和更新用的是同一份,发布密钥签名、`seq` 防回滚),用**脚本里内置的发布公钥**验签,再按清单里的 digest 拉镜像(`repo@sha256:…`);原生二进制则核对清单里的 SHA-256 再安装。Backend 改不了 digest,最多让节点装到一个旧的合法版本。
|
||||
|
||||
**③ 其余**
|
||||
- 角色(Agent 还是 Probe)、镜像仓库地址仍由代码决定,但镜像身份由签名清单决定。
|
||||
- Windows 版同理,另外可以加 Authenticode 签名。
|
||||
- 今天的现状:一行安装不装更新器,也不带发布公钥,节点自己不验任何签名;这一节是要补的缺口。
|
||||
|
||||
## 16. 效率(运行时开销与日常摩擦)
|
||||
|
||||
目标机器是 **1 核 1 GB 的小鸡**,甚至路由器。原则:**重活留在 Backend 和浏览器,节点只做最小的验证**;节点不增加线程、定时器、依赖,数据路径上不增加任何东西。
|
||||
|
||||
**节点的预算(实现时当成验收标准)**
|
||||
|
||||
| 项 | 预算 | 怎么做到 |
|
||||
| --- | --- | --- |
|
||||
| 数据路径 | **零增量**:UDP 数据面、测量、对等握手、敲门都不变 | 链随期望状态下发,在状态同步时验,不在握手里验(见 7.2) |
|
||||
| 常驻内存 | 增量 ≤ 1 MB | 成员表上限 256 条(每条几百字节)、操作员集合上限 64 个凭证;不缓存原始链,只存验过的结果和哈希 |
|
||||
| 二进制 | 增量 ≤ 150 KB | 只用 BCL 的 ECDSA、HKDF、SHA-256;WebAuthn 只验断言(固定结构的 `authenticatorData`、对 `clientDataJSON` 只比三个字段),**不带通用 CBOR 解析**;注册和 attestation 解析只在 Backend 和浏览器里 |
|
||||
| CPU | 每个成员一次完整验证(约 4–6 次 ECDSA 验签),小核心(Cortex-A53、MIPS 级)上 ≤ 100 毫秒;不重复验 | 只在状态同步时做;文档版本或成员链哈希没变就跳过;操作员集合或吊销表版本变化时,只重验受影响的成员 |
|
||||
| 线程和定时器 | **不新增** | 信任状态上报搭现有心跳计数(每 N 次心跳带一次,约 5 分钟);验证在已有的状态处理里串行完成 |
|
||||
| 流量 | 链约 2 KB,只在成员首次出现或变化时随期望状态下发;上报每次 < 100 字节 | 同步时带成员标识(哈希),未变化的不重发 |
|
||||
| 输入上限 | 单条链 ≤ 8 KB、成员 ≤ 256、凭证 ≤ 64、单次同步最多验 N 个成员 | 超限在做任何密码学运算**之前**拒绝;Backend 也只下发它自己预先验过的链(挡掉垃圾,节点仍然独立验证) |
|
||||
|
||||
**把预算变成测试**
|
||||
|
||||
- `Fleet.Tests`:链验证耗时断言(CI 机器上每个成员 ≤ 20 毫秒)、输入上限的拒绝测试、同一份状态重复同步不重复验证(计数断言)。
|
||||
- 现有 `ArchitectureTests` 继续禁止节点引用新包;加一条检查:节点项目不引用 WebAuthn/CBOR 之类的库。
|
||||
- CI 记录节点二进制大小和启动后的常驻内存,增量超出预算就失败。
|
||||
|
||||
**Backend 侧**
|
||||
|
||||
- WebAuthn 验证只发生在登录;API 请求认证是一次哈希查表,主体缓存 30 秒并在撤销时主动失效。
|
||||
- `enroll` key 签一张券是一次 HKDF 加一次 ECDSA 签名(不到 1 毫秒)。
|
||||
- 审计日志的写入并进 SQLite 单写入者的批量提交,不拖慢摄取路径。
|
||||
- webhook 投递、数据拉取都在 Backend 里完成(见 5.4),对节点零开销。
|
||||
|
||||
**日常摩擦**
|
||||
|
||||
- 进页面不要求 passkey,只有给操作员集合签名时才触碰。
|
||||
- 批量操作一次触控:批量吊销是一张吊销表一次签名,批量添加是窗口内连续签券;列表都支持全选。
|
||||
- 自动化走 `enroll` key,不需要人在场;其余自动化走普通 key。
|
||||
- 离线恢复密钥(见第 3 节)避免因为 passkey 丢失而重装节点。
|
||||
|
||||
## 17. 待确认
|
||||
|
||||
2. API key 的 IP 白名单要不要做(倾向做,放在第 1 步)。
|
||||
3. 迁移后,现有 admin 令牌不能生成加入代码,需要触控创建 `enroll` key 来替换。
|
||||
4. `owner` 与 `admin` 之外要不要加只读的认证角色(目前用 `read` 能力的 API key 满足)。
|
||||
5. 属主的默认值:添加者的全部 passkey,还是只有签这次入网券的那一把;添加时最多允许绑定几把。
|
||||
6. `ControllerUpdate` 要几把属主签名(一把还是多数)。
|
||||
7. 静态数据的私钥来源:优先 WebAuthn PRF,不支持时是否接受「浏览器里的不可导出密钥」(只在这台浏览器能用)。
|
||||
8. 会话 AEAD 选 ChaCha20-Poly1305 还是 AES-GCM(两者 BCL 都有;小机器没有 AES 硬件指令时前者更快)。
|
||||
9. 信任状态上报的阈值:时钟偏差红线(入网券 TTL 的一半与 60 秒取小)、「长时间没有上报」是 10 分钟,是否合适。
|
||||
10. 安装脚本的哈希在哪里公布(Gitea 发布页之外还要不要放 DNS TXT 或另一个独立渠道)。
|
||||
11. 节点预算的数字(常驻内存 ≤ 1 MB、二进制 ≤ 150 KB、每个成员验证 ≤ 100 毫秒、成员上限 256)是否合适;超出预算时优先砍什么(比如先去掉操作员集合的 64 个凭证上限换成更小的)。
|
||||
12. webhook 和数据拉取的配额默认值(每分钟请求数、每天行数)和事件清单。
|
||||
|
||||
## 16. 运行中的二进制不做校验
|
||||
|
||||
曾经做过「节点二进制绑定」:节点握手时自报正在运行的二进制的 SHA-256,Backend 在第一次握手时绑定,之后对不上就标红或拒绝。**已经移除。**
|
||||
|
||||
原因:哈希和清单都是节点自己报的。一个被换进来、故意撒谎的二进制可以照抄原来的哈希和清单,Backend 分辨不出——上报者就是被怀疑的对象。这一层只能抓到「文件被换了、但程序本身没撒谎」的情形,却让人误以为有保障,还要求运维宿主机更新器、状态卷和每个节点的清单。得不偿失。
|
||||
|
||||
「这个资产是我们的」靠**安装时的来源证明**,那里的哈希来自另一个信道,伪造不了:
|
||||
|
||||
- 安装脚本(SHA-256 记在签名的清单里,并公布在发布页),按清单里的 digest 拉镜像(`repo@sha256:…`);
|
||||
- 下载压缩包的人用签名的 `SHA256SUMS` 核对(验签用 `packaging/update/release-key.pub.pem`);
|
||||
- 节点的更新由宿主机更新器用本机公钥验签,只认本机配置的镜像仓库。
|
||||
|
||||
要让运行中的二进制真的无法伪造,必须有容器之外的东西背书:宿主机更新器持有一把节点进程读不到的密钥,对「当前运行的镜像 digest + Backend 的挑战」签名,由节点转发;容器里的进程被换掉也伪造不了,除非攻破宿主机。这一步没有做(一键容器安装的节点也没有宿主机更新器)。
|
||||
@@ -1,6 +1,6 @@
|
||||
# API 设计约束
|
||||
|
||||
本文是所有 Backend REST 端点都必须遵守的**规则**;具体端点见 [api.md](api.md)。规则参考了 [Microsoft Azure REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md) 和 RFC 9457,并根据本项目做了取舍。内部协议(Probe↔Agent、Agent↔Backend)的兼容规则见 [protocol.md](protocol.md) 第 4 节。
|
||||
本文是所有 Backend REST 端点都必须遵守的**规则**;具体端点见 [api.md](api.md)。规则参考了 [Microsoft Azure REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md) 和 RFC 9457,并根据本项目做了取舍。内部协议(Probe↔Agent、Agent↔Backend)的兼容规则见 [protocol.md](../design/protocol.md) 第 4 节。
|
||||
|
||||
## 1. 契约生成链路
|
||||
|
||||
@@ -57,7 +57,7 @@ web:openapi-typescript 生成类型,调用方的类型错误会直接暴露
|
||||
|
||||
| 类别 | 规则 | 示例 |
|
||||
| --- | --- | --- |
|
||||
| 时间点 | RFC 3339 UTC,以 `Z` 结尾,精确到毫秒。**唯一例外**:列式时序响应中的时间轴数组 `t` 使用 Unix 毫秒(见 [query.md](query.md)) | `2026-09-27T10:00:00.000Z` |
|
||||
| 时间点 | RFC 3339 UTC,以 `Z` 结尾,精确到毫秒。**唯一例外**:列式时序响应中的时间轴数组 `t` 使用 Unix 毫秒(见 [query.md](../design/query.md)) | `2026-09-27T10:00:00.000Z` |
|
||||
| 时长 | 整数 + 单位后缀 | `interval_ms: 1000` |
|
||||
| 延迟 | 带小数的毫秒值(指标字典中的单位见目录) | `rtt_p95_ms: 58.93` |
|
||||
| 百分比 | 0–100 的数字 + `_pct` | `loss_pct: 0.5` |
|
||||
@@ -15,7 +15,7 @@
|
||||
```text
|
||||
/api/public/* 只读。站点设为公开时匿名可访问,设为私有时需要登录或只读令牌;
|
||||
public_visible = false 的 Agent 对匿名访客隐藏
|
||||
/api/admin/* 管理。需要 Cookie 会话(含 CSRF 校验)或带有对应**能力**的 API 令牌;每个端点声明自己需要的一项能力(OpenAPI 里的 `x-moshiro-capability`),完整目录见 `GET /api/admin/capabilities`,设计见 [access.md](access.md)
|
||||
/api/admin/* 管理。需要 Cookie 会话(含 CSRF 校验)或带有对应**能力**的 API 令牌;每个端点声明自己需要的一项能力(OpenAPI 里的 `x-moshiro-capability`),完整目录见 `GET /api/admin/capabilities`,设计见 [access.md](../security/access.md)
|
||||
/api/agent/v1/* 节点专用:注册(使用加入令牌)和 RPC(使用签名认证)
|
||||
/healthz /metrics /openapi/v1.json /install /install.ps1
|
||||
```
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
## 2. Public API
|
||||
|
||||
> 展示数据的查询规则(时间段、步长、分层、指标分组、列式响应、实时跟随、缓存)以 [query.md](query.md) 为准。本节只列出端点。
|
||||
> 展示数据的查询规则(时间段、步长、分层、指标分组、列式响应、实时跟随、缓存)以 [query.md](../design/query.md) 为准。本节只列出端点。
|
||||
|
||||
### 2.1 首屏排行
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
- 返回每条线路在该时间段内的精确汇总,以及在线状态、会话信息(源地址变化次数)、最近一次测试的摘要;
|
||||
- `range` 默认 `5m`,也可以写 `from/to`;
|
||||
- 汇总字段的格式见 [query.md](query.md) 第 6 节。
|
||||
- 汇总字段的格式见 [query.md](../design/query.md) 第 6 节。
|
||||
|
||||
### 2.2 实时推送(SSE)
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
| `GET /api/public/lines/{line_id}/distribution?metric=rtt&range=15m` | 该时间段内的直方图和 CDF |
|
||||
| `GET /api/public/lines/{line_id}/heatmap?metric=rtt&range=6h` | 时间 × 延迟桶的热力图 |
|
||||
|
||||
参数、响应示例和限额见 [query.md](query.md)。
|
||||
参数、响应示例和限额见 [query.md](../design/query.md)。
|
||||
|
||||
线路详情 `GET /api/public/lines/{line_id}` 带 `data_since`:这条线路最早的数据是什么时候(没有数据是 null),前端据此只提供「有数据」的时间范围。**展示端的响应里没有 Probe 的版本和来源地址**(那些只在管理端)。
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
| GET | `/api/admin/auth/me` | |
|
||||
| GET | `/api/admin/agents` | 全部 Agent(包括 pending/disabled)、版本、本地硬上限、心跳计数、任务同步状态 |
|
||||
| PATCH | `/api/admin/agents/{id}` | `name`、`labels`、`public_host`、`ipv6_host`、`public_visible`;`public_host_auto` / `ipv6_host_auto` 为 true 时撤销手填、回到自动识别。手填就是手动,Agent 上报的地址不再覆盖;DTO 的 `public_host_auto` / `ipv6_host_auto` 表示当前是不是自动 |
|
||||
| POST | `/api/admin/agents/{id}/approve` `/disable` `/enable` `/takeover` | 见 [enrollment.md](enrollment.md) |
|
||||
| POST | `/api/admin/agents/{id}/approve` `/disable` `/enable` `/takeover` | 见 [enrollment.md](../security/enrollment.md) |
|
||||
| DELETE | `/api/admin/agents/{id}` | 删除;历史数据按保留策略自然过期 |
|
||||
| GET/POST | `/api/admin/join-tokens` | 创建时返回令牌明文和安装命令(仅此一次);`role` 是 `agent`(默认)或 `probe`,决定用它注册出来的是哪种节点 |
|
||||
| DELETE | `/api/admin/join-tokens/{id}` | 吊销 |
|
||||
@@ -165,8 +165,8 @@
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| POST | `/api/agent/v1/enroll` | 由 `MapFleetEnrollment` 提供(使用加入令牌认证),见 [../backend/fleet/README.md](../backend/fleet/README.md) 的「注册」一节 |
|
||||
| GET | `/api/agent/v1/rpc` | 由 `MapFleetLink` 提供,升级为 WebSocket;MoShiro 的方法见 [protocol.md](protocol.md) |
|
||||
| POST | `/api/agent/v1/enroll` | 由 `MapFleetEnrollment` 提供(使用加入令牌认证),见 [../backend/fleet/README.md](../../backend/fleet/README.md) 的「注册」一节 |
|
||||
| GET | `/api/agent/v1/rpc` | 由 `MapFleetLink` 提供,升级为 WebSocket;MoShiro 的方法见 [protocol.md](../design/protocol.md) |
|
||||
|
||||
## 5. 运维端点
|
||||
|
||||
@@ -175,7 +175,8 @@
|
||||
| `GET /healthz` | 无 | SQLite 与原始数据目录都可写 |
|
||||
| `GET /metrics` | admin 令牌 | Prometheus 格式:在线数、块入库速率、分析队列长度、重算进度、磁盘占用、SSE 连接数 |
|
||||
| `GET /openapi/v1.json` | 无 | |
|
||||
| `GET /install?code=…`(也接受 `/install.sh`)、`GET /install.ps1?code=…` | 无(凭一次性代码) | 一行安装脚本:Agent 和 Probe 共用同一份,角色、面板地址和镜像由代码决定,用 Docker 或 Podman 起容器;代码不能用就是 404 |
|
||||
| `GET /install`(也接受 `/install.sh`)、`GET /install.ps1` | 无 | 静态安装脚本:Agent 和 Probe 共用同一份,原样返回、不替换任何值,所以哈希在一个版本内固定;角色、面板地址、代码和属主走环境变量 |
|
||||
| `GET /install/manifest?code=…`、`GET /install/manifest.sig?code=…` | 无(凭一次性代码) | 该角色已签名的发布清单(原样)和签名,安装脚本据此验签、按 digest 拉镜像;代码不能用就是 404 |
|
||||
|
||||
## 6. 错误码(节选,完整列表见共享的 `ErrorCode`)
|
||||
|
||||
File renamed without changes.
@@ -11,25 +11,30 @@
|
||||
## 2. 拓扑
|
||||
|
||||
```text
|
||||
Web(展示端 · 管理端)
|
||||
▲ REST + SSE
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ Backend(面板) │
|
||||
│ Api · Registry · Planner · Links │
|
||||
│ Releases · Analysis · LiveHub │
|
||||
操作员 passkey ──签名「操作员集合」──┐ CI 发布私钥 ──签名「镜像 digest 清单」──▶ 镜像仓库(清单 + 镜像)
|
||||
▼ ▲
|
||||
Web(展示端 · 管理端) │ 定时取清单
|
||||
▲ REST + SSE │
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Backend(面板) │──────────────────────────────┘
|
||||
│ Api · Auth(会话 · passkey · API 令牌 · 审计) │
|
||||
│ Trust(操作员集合 · 服务器身份)· Registry · Planner · Links │
|
||||
│ Releases(发布清单 · 可信发布密钥) │
|
||||
│ Ingest · Analysis · LiveHub │
|
||||
│ Store(段文件 + SQLite) │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
▲ ▲
|
||||
│ wss 长连接(节点发起) │ wss 长连接(节点发起)
|
||||
│ wss 长连接(节点发起,应用层加密) │ wss 长连接(节点发起,应用层加密)
|
||||
│ 任务、数据、状态、更新通知 │ 身份、Agent 列表、状态、更新通知
|
||||
┌────────────────────┐ ┌──────────────────────┐
|
||||
│ Agent × 10+ │ ◀── 测量会话 ── │ Probe × N │
|
||||
│ 有公网 IP,监听 │ │ 家宽等出口,NAT 后 │
|
||||
│ 有公网 IP,监听 │ 先敲门再 TLS │ 家宽等出口,NAT 后 │
|
||||
└────────────────────┘ └──────────────────────┘
|
||||
测量会话:TLS 控制通道 + UDP/TCP 数据通道,由 Probe 主动拨出
|
||||
宿主机更新器(容器节点):用本机公钥验签 → 按 digest 拉镜像 → 重启,失败回滚
|
||||
```
|
||||
|
||||
- **线路(line)** = 一对(Probe,Agent)。可以有多个 Probe(身份由 Backend 生成,可以吊销、删除),每个 Probe 连接多个 Agent,每个 Agent 同时服务多个 Probe;一条线路的数据按 `line:<probe_id>` 单独存放,互不混合。对外的线路 id 是 `<agent_id>~<probe_id>`。
|
||||
- **线路(line)** = 一对(Probe,Agent)。可以有多个 Probe(身份由 Probe 自己生成并用加入令牌注册,可以吊销、删除),每个 Probe 连接多个 Agent,每个 Agent 同时服务多个 Probe;一条线路的数据按 `line:<probe_id>` 单独存放,互不混合。对外的线路 id 是 `<agent_id>~<probe_id>`。
|
||||
- **目标任务(target)** = 某个 Agent → 指定的 IP:端口(ICMP、TCP/TLS 建连),只能访问 Agent 本地允许的网段。
|
||||
|
||||
## 3. 数据链路
|
||||
@@ -53,30 +58,34 @@
|
||||
|
||||
| 主题 | 要点 | 文档 |
|
||||
| --- | --- | --- |
|
||||
| 节点框架 | 注册、身份、连接、RPC、可靠上报、状态同步、宿主都由与业务无关的 **Fleet** 提供;MoShiro 只写业务,其他产品也能复用 | [../backend/fleet/README.md](../backend/fleet/README.md) |
|
||||
| 注册与身份 | 使用 `fleet-enroll` 和加入令牌自动注册,之后用签名认证;Probe 用自己的身份直连面板,Agent 列表由面板下发 | [enrollment.md](enrollment.md) |
|
||||
| 更新 | 只接受发布密钥签名的清单,更新策略由节点本地决定,失败自动回滚。容器节点(Agent、Probe)已实现:CI 对镜像 digest 签名,宿主机上的 `moshiro-update` 验签、按 digest 拉取、重启;纯二进制节点的更新是待办([TODO.md](../TODO.md)) | [deployment.md](deployment.md) |
|
||||
| 节点框架 | 注册、身份、连接、RPC、可靠上报、状态同步、宿主都由与业务无关的 **Fleet** 提供;MoShiro 只写业务,其他产品也能复用 | [../backend/fleet/README.md](../../backend/fleet/README.md) |
|
||||
| 注册与身份 | 使用 `fleet-enroll` 和加入令牌自动注册,之后用签名认证;Probe 用自己的身份直连面板,Agent 列表由面板下发 | [enrollment.md](../security/enrollment.md) |
|
||||
| 更新 | 只接受发布密钥签名的清单,更新策略由节点本地决定,失败自动回滚。容器节点(Agent、Probe)已实现:CI 对镜像 digest 签名,宿主机上的 `moshiro-update` 验签、按 digest 拉取、重启;纯二进制节点的更新是待办([TODO.md](../../TODO.md)) | [deployment.md](../ops/deployment.md) |
|
||||
| 任务模型 | `Task = {kind, target, params, schedule}`;持续采集、按需测试、目标任务统一建模;下发“期望任务集”,由节点自行调和 | [measurement.md](measurement.md) |
|
||||
| 采集扩展 | Kind 由积木组合声明;采集密度靠参数调整;原始块自描述 | [measurement.md](measurement.md) |
|
||||
| 统计扩展 | 指标注册制:少数几种累加器 + 统计算子;一行 `MetricDef` 就是一个新指标 | [analysis.md](analysis.md) |
|
||||
| 协议 | 控制链路使用 Fleet(JSON-RPC 2.0 + 二进制通道);MoShiro 只定义 `moshiro.*` 方法、`TaskSet` 状态文档和 UDP/TCP 数据面 | [protocol.md](protocol.md) |
|
||||
| 存储 | 原始块追加写入段文件,SQLite 记录偏移量;指标窗口通用存储并降采样 | [storage.md](storage.md) |
|
||||
| 展示查询 | 任意时间段(相对或绝对);自动步长(每条曲线最多约 720 个点);1s 内存热层(1 小时)加上 10s/1m/5m/1h 各层;累加器现场合并,汇总精确;列式响应;SSE 实时跟随 | [query.md](query.md) |
|
||||
| API | `/api/public`(只读,可设为公开)、`/api/admin`(登录)、`/api/agent`(注册与 RPC);实时数据用 SSE | [api.md](api.md) |
|
||||
| API | `/api/public`(只读,可设为公开)、`/api/admin`(登录)、`/api/agent`(注册与 RPC);实时数据用 SSE | [api.md](../api/api.md) |
|
||||
|
||||
### 4.0 容器节点的更新
|
||||
|
||||
```text
|
||||
CI ──签名的发布清单(镜像 digest)──▶ Backend ──原样转发(offer)──▶ Agent / Probe 进程
|
||||
│ 只写进数据目录的 update/
|
||||
▼
|
||||
CI ──签名的发布清单(镜像 digest)──▶ 镜像仓库(<仓库>-manifest 小镜像的标签里)
|
||||
│ │
|
||||
Backend 定时取、验签后登记(也接受 CI 直接 POST /api/release/offer)
|
||||
▼ │
|
||||
Backend ──原样转发(offer)──▶ Agent / Probe 进程 │ 宿主机更新器也可以直接读
|
||||
│ 只写进数据目录的 update/ │
|
||||
▼ ▼
|
||||
宿主机 moshiro-update(root):用本机公钥验签 → 核对组件、仓库和 seq → podman pull 仓库@digest → 重启 → 不健康就回滚
|
||||
│ 状态写进只读的 update-status/
|
||||
▼
|
||||
节点随心跳上报更新状态 ──▶ Backend ──▶ 管理端(有新版本时点「更新」)
|
||||
```
|
||||
|
||||
细节见 [deployment.md](deployment.md)「容器节点的更新」。
|
||||
细节见 [deployment.md](../ops/deployment.md)「容器节点的更新」。
|
||||
|
||||
### 4.1 回显的四个时间戳
|
||||
|
||||
@@ -88,7 +97,7 @@ CI ──签名的发布清单(镜像 digest)──▶ Backend ──原样
|
||||
|
||||
## 5. 安全边界:纯采集
|
||||
|
||||
> 这一节是现状。「Backend 被攻破、节点被入侵」之后的更严格设计,包括现有的几处缺口(例如 Probe 会拨 Backend 给的任意地址、Agent 的 TCP 端口会对陌生人完成 TLS 握手),见 [security.md](security.md),尚未实现。
|
||||
> 这一节是现状。「Backend 被攻破、节点被入侵」之后的威胁模型、现有的几处缺口(例如 Probe 会拨 Backend 给的任意地址、测量数据没有产生者签名)和每项的状态,见 [security.md](../security/security.md);预握手敲门、Probe 自己生成密钥、操作员集合与服务器身份已经实现。
|
||||
|
||||
```text
|
||||
Agent / Probe 能做的 执行编译在二进制中的 Kind;监听和访问本地配置允许的端口与网段;受本地硬上限约束
|
||||
@@ -98,11 +107,11 @@ Agent / Probe 能做的 执行编译在二进制中的 Kind;监听和访问
|
||||
```
|
||||
|
||||
- **本地硬上限**写在节点本机的配置文件中,Backend 下发的任务如果超出,节点直接拒绝:允许访问的目标网段(默认为空,即不开放目标任务)、总码率和包速率上限、允许监听的端口。
|
||||
- **构建期强制**:常驻节点进程使用 `BannedSymbols.Node.txt`,禁用 `System.Diagnostics.Process`、`Assembly.Load*`、`NativeLibrary.Load`、`HttpClient`,以及 `ConfigStore` 以外的文件写入 API,详见 [project-layout.md](project-layout.md) 第 7 节。
|
||||
- **权限分离**:注册(`fleet-enroll`,一次性运行)、采集(`moshiro-agent` / `moshiro-probe`)、更新是不同的进程。容器节点里,采集进程在容器内以 UID 1654 运行(只读文件系统、不带额外能力),更新由宿主机上 root 运行的 `moshiro-update` 完成:它只读取节点放进 `update/` 的普通文件、复制到自己的临时目录后验签,状态写在节点只读的 `update-status/`;采集进程改写不了自己的二进制,更新器也读不到节点身份私钥。纯二进制节点的更新是待办(见 [TODO.md](../TODO.md))。
|
||||
- **构建期强制**:常驻节点进程使用 `BannedSymbols.Node.txt`,禁用 `System.Diagnostics.Process`、`Assembly.Load*`、`NativeLibrary.Load`、`HttpClient`,以及 `ConfigStore` 以外的文件写入 API,详见 [project-layout.md](../dev/project-layout.md) 第 7 节。
|
||||
- **权限分离**:注册(`fleet-enroll`,一次性运行)、采集(`moshiro-agent` / `moshiro-probe`)、更新是不同的进程。容器节点里,采集进程在容器内以 UID 1654 运行(只读文件系统、不带额外能力),更新由宿主机上 root 运行的 `moshiro-update` 完成:它只读取节点放进 `update/` 的普通文件、复制到自己的临时目录后验签,状态写在节点只读的 `update-status/`;采集进程改写不了自己的二进制,更新器也读不到节点身份私钥。纯二进制节点的更新是待办(见 [TODO.md](../../TODO.md))。
|
||||
- **容器运行**:Agent、Probe 的镜像基于 Alpine(musl,NativeAOT),宿主机用 rootful podman 加 systemd 运行:UID/GID 1654、只读文件系统、`--cap-drop all`、`no-new-privileges`、`/tmp` 用 tmpfs;配置和身份放在挂载的数据目录里。
|
||||
- **最小权限运行**:非 root 用户;只在需要时授予 `CAP_NET_RAW`(ICMP);默认端口都高于 1024,不需要 `CAP_NET_BIND_SERVICE`,只有在 Agent 本地显式开启 443 时才授予。
|
||||
- **身份**:节点使用本地生成的 ECDSA P-256 密钥对签名认证(Probe 的密钥由面板生成、只返回一次,同一把用来连面板和连 Agent),Backend 只存公钥,数据库泄露也无法冒充节点。
|
||||
- **身份**:节点使用本地生成的 ECDSA P-256 密钥对签名认证(Agent 和 Probe 都自己生成;Probe 有两把独立的密钥,一把连面板、一把连 Agent),Backend 只存公钥,数据库泄露也无法冒充节点。
|
||||
- **防放大**:未认证的包静默丢弃;回复长度与请求相等;每个流按声明速率限速;使用重放窗口。
|
||||
- **容器里的主机指标**:容器里的 Agent 通过只读挂载宿主机的 `/proc`、`/sys`、根目录和 `/etc/os-release`(环境变量 `HOST_PROC`、`HOST_SYS`、`HOST_ROOT`)读取宿主机的资源占用和系统名;读不到的项返回空值,不会退回去读容器自己的数据。Probe 只需要挂载 `/etc/os-release`。
|
||||
- **自身状态**:节点上报进程内的计数(会话数、包速率、队列、丢弃数);Agent 另外每隔几秒用 `moshiro.host` 上报一份主机资源占用的汇总数字(可以在 `agent.json` 里用 `reporting.host_stats = false` 整个关掉),最新值实时推给展示页,Backend 每分钟存一个样本(默认保留 7 天,`retention.host_days`)。
|
||||
@@ -146,14 +155,14 @@ Kind 目录编译在二进制中,节点不接受远程下发代码(安全边
|
||||
| D13 | Probe/Agent 运行时只依赖 BCL;.NET 10 NativeAOT | 轻量 |
|
||||
| D14 | 节点机制抽成与业务无关的 Fleet 框架,放在同一仓库的 `backend/fleet/` 目录,暂不独立(由用户确认;不设拆分时间表,需要时再评估;不使用 PublicApiAnalyzers) | 可复用;不引用任何 `MoShiro.*` 的约束由 `ArchitectureTests` 在构建时检查 |
|
||||
| D15 | 注册独立为 `fleet-enroll` 工具;常驻进程不包含注册代码 | 常驻进程去掉 HttpClient,边界更纯 |
|
||||
| D16 | 更新与业务进程分离:发布密钥签名、本地策略、健康确认与回滚。容器节点已实现(宿主机 `moshiro-update`,清单里是镜像 digest);纯二进制节点的更新见 [TODO.md](../TODO.md) | 面板被攻破时也无法推送恶意二进制 |
|
||||
| D16 | 更新与业务进程分离:发布密钥签名、本地策略、健康确认与回滚。容器节点已实现(宿主机 `moshiro-update`,清单里是镜像 digest);纯二进制节点的更新见 [TODO.md](../../TODO.md) | 面板被攻破时也无法推送恶意二进制 |
|
||||
| D17 | Probe 支持 OpenWrt:正式支持 `linux-musl-x64`、`linux-musl-arm64`;armv7 视工具链验证结果决定;MIPS 不支持,改为在局域网设备上运行 Probe | 由用户确认;NativeAOT 不支持 MIPS |
|
||||
| D18 | 默认测量端口不使用 443:控制 7443/tcp,UDP 数据 7443/udp,TCP 数据 7444/tcp;443 只在 Agent 本地显式开启时才允许使用 | 由用户确认;避免与 VPS 上的真实服务冲突;结果会记录实际端口 |
|
||||
| D19 | 展示查询最细 1s:最近 1 小时来自内存热层,更早的数据来自 10s/1m/5m/1h 各层,现场合并;时间段内的汇总由累加器精确合并得出 | 由用户确认;5 分钟、15 分钟这类视图足够精细且快 |
|
||||
| D20 | 时序响应采用列式格式(`t[]` + `values{}`,时间轴使用 Unix 毫秒) | 由用户确认;体积小,可以直接交给图表库 |
|
||||
| D21 | 国家和运营商只由 Backend 向 IP 归属服务查询(缺省 `https://ip.007995.xyz`,可关闭),只查公网地址;节点不访问任何外部服务;公开接口不返回 IP、城市、ASN | 客户的防火墙常常要审计节点的出站连接;访客拿不到敏感信息(有测试保证) |
|
||||
| D22 | 构建信息(版本、tag、提交、构建时间)由节点自报,访客是否可见由设置 `site.show_build` 控制(默认关);提交号只在管理端 | 展示页可以选择不暴露版本;管理员能看出哪个节点没升级 |
|
||||
| D23 | 零信任方向:信任锚点从 Backend 移到操作员密钥(名册);Probe 自己生成密钥;Agent 的 TCP 端口预握手敲门;敏感能力放进独立 extension,用操作员签名的能力令牌授权。不做进程沙箱(节点是 1 核 1 GB 的小机器),代码风险靠 PR 评审 | 由用户确认;设计稿见 [security.md](security.md),尚未实现 |
|
||||
| D23 | 零信任方向:信任锚点从 Backend 移到操作员 passkey(操作员集合,节点安装时固定创世指纹和属主);Probe 自己生成密钥;Agent 的 TCP 端口预握手敲门;敏感能力以后放进独立 extension,用属主 passkey 签名的能力令牌授权。不做进程沙箱(节点是 1 核 1 GB 的小机器),代码风险靠 PR 评审 | 由用户确认;做法和各缺口的状态见 [security.md](../security/security.md)、[access.md](../security/access.md) |
|
||||
|
||||
## 9. 仍待确认
|
||||
|
||||
File renamed without changes.
@@ -2,8 +2,8 @@
|
||||
|
||||
MoShiro 的两条控制链路都建立在 Fleet 之上:
|
||||
|
||||
- **Agent ↔ Backend**:Fleet 连接服务端(wss),见 [../backend/fleet/README.md](../backend/fleet/README.md);
|
||||
- **Probe ↔ Agent**:Fleet 节点间点对点会话(TLS),见 [../backend/fleet/README.md](../backend/fleet/README.md) 的「点对点会话」一节。
|
||||
- **Agent ↔ Backend**:Fleet 连接服务端(wss),见 [../backend/fleet/README.md](../../backend/fleet/README.md);
|
||||
- **Probe ↔ Agent**:Fleet 节点间点对点会话(TLS),见 [../backend/fleet/README.md](../../backend/fleet/README.md) 的「点对点会话」一节。
|
||||
|
||||
认证、心跳、重连、能力协商、可靠发件箱、期望状态同步、JSON-RPC 信封都由 Fleet 负责。本文只定义 **MoShiro 自己的内容**:`moshiro.*` 方法、状态文档、二进制通道,以及 Fleet 之外的数据面(UDP 包和数据连接)。所有类型都放在 `MoShiro.Protocol` 中。
|
||||
|
||||
@@ -14,7 +14,7 @@ MoShiro 的两条控制链路都建立在 Fleet 之上:
|
||||
| 下发任务 | 期望状态 `fleet.state.sync` | 文档类型 `TaskSet`,包括 `tasks[]`、`peers`(允许连这个 Agent 的 Probe 公钥,已按线路开关过滤) |
|
||||
| 下发 Agent 列表给 Probe | 期望状态 `fleet.state.sync`(Probe 直连面板) | 文档类型 `probe_agents`:`version`、`agents[]`(`agent_id`、`host`、`tcp_port`、`udp_ports`、`public_key`),`public_key` 是 Agent 面向 Probe 的 TLS 证书公钥(SPKI,base64):Probe 用它固定证书并派生敲门密钥;没有上报公钥的 Agent 不会下发。已按线路开关过滤;Agent 集合或线路开关变化时重新推送 |
|
||||
| 定时重启 | 通知 `moshiro.schedule`(B→A、B→P) | `{restart_at}`,形如 `04:30+08:00`,空表示关闭;连上时补发、管理端改了设置就群发。节点本地写了 `restart_at` 或 `MOSHIRO_RESTART_AT` 时以本地为准;节点只是到点自己有序退出,由服务管理器拉起 |
|
||||
| 更新 | 通知 `moshiro.update.offer` / `moshiro.update.apply`(B→A、B→P),心跳附加字段 `update` | `offer` 带 CI 签名的发布清单原文和签名,节点只写进数据目录;`apply` 请求更新;`update` 是宿主机更新器的状态(`state`、`current`、`available`、`error`、`at`),见 [deployment.md](deployment.md) |
|
||||
| 更新 | 通知 `moshiro.update.offer` / `moshiro.update.apply`(B→A、B→P),心跳附加字段 `update` | `offer` 带 CI 签名的发布清单原文和签名,节点只写进数据目录;`apply` 请求更新;`update` 是宿主机更新器的状态(`state`、`current`、`available`、`error`、`at`),见 [deployment.md](../ops/deployment.md) |
|
||||
| 上报原始块 | 发件箱,二进制通道 **16** | `RawChunk`(自描述,见 [measurement.md](measurement.md) 第 4 节),`msg_id = chunk_id` |
|
||||
| Probe 交付列块 | 点对点会话的发件箱,二进制通道 **17** | 列块:`u32 stream_id \| u32 first_seq \| u32 count \| 列数据(Brotli)` |
|
||||
| 节点自身状态 | `fleet.heartbeat` 的附加字段 | Agent:`sessions`、`probes[]`、`pps_in/out`、`dropped_invalid`、`dropped_rate_limited`、`queue_bytes`、`update`;Probe:只有 `update` |
|
||||
@@ -22,7 +22,7 @@ MoShiro 的两条控制链路都建立在 Fleet 之上:
|
||||
|
||||
## 2. `moshiro.*` 方法
|
||||
|
||||
每个方法的发起方、类型(请求或通知)和允许的角色声明在代码里的 `MoShiroMethodTable`(框架的 `fleet.*` 在 `FleetMethodTable`),路由器据此拒绝没有声明、方向不对或角色不符的消息,回 `fleet_protocol_violation`;下面各表的「方向」列由测试和这张声明表对账。设计见 [security.md](security.md) 第 8 节。
|
||||
每个方法的发起方、类型(请求或通知)和允许的角色声明在代码里的 `MoShiroMethodTable`(框架的 `fleet.*` 在 `FleetMethodTable`),路由器据此拒绝没有声明、方向不对或角色不符的消息,回 `fleet_protocol_violation`;下面各表的「方向」列由测试和这张声明表对账。设计见 [security.md](../security/security.md) 第 6 节。
|
||||
|
||||
### Agent ↔ Backend
|
||||
|
||||
@@ -33,7 +33,7 @@ MoShiro 的两条控制链路都建立在 Fleet 之上:
|
||||
| `moshiro.test.cancel` | B→A | 请求 | `test_id` |
|
||||
| `moshiro.test.status` | A→B | 通知 | `test_id`、`status`(`running`、`phase_done`、`succeeded`、`failed`、`aborted`)、`phase?`、`error_code?` |
|
||||
| `moshiro.event` | A→B | 通知 | `kind`(`probe_connected`(`detail` 是 `probe_id`,`probe` 里带版本、远端地址和连接时间)、`probe_disconnected`、`endpoint_changed`、`session_rejected`、`clock_step`、`gap`、`cpu_saturated`(节点进程 CPU 饱和,可能影响测量,由 Probe 或 Agent 上报))、`at_utc`、`detail` |
|
||||
| `moshiro.host` | A→B | 通知 | `HostSnapshot`:主机资源占用的汇总数字(CPU、内存、根分区、网卡累计流量与网速、运行时长;没有型号、主机名、IP),每 `host_interval_s` 秒一次;`host_stats` 为 false 时不发,见 [enrollment.md](enrollment.md) |
|
||||
| `moshiro.host` | A→B | 通知 | `HostSnapshot`:主机资源占用的汇总数字(CPU、内存、根分区、网卡累计流量与网速、运行时长;没有型号、主机名、IP),每 `host_interval_s` 秒一次;`host_stats` 为 false 时不发,见 [enrollment.md](../security/enrollment.md) |
|
||||
|
||||
心跳的附加字段里有 `probes[]`:Agent 当前连着的每个 Probe(`id`、`version`、`remote`、`since_utc_ms`),每次都是完整快照,Backend 靠它纠正错过的连接事件。点对点握手的认证消息里可以带 Probe 的 `version`(只用于展示)。块头里的 `probe_id` 以 Agent 认证过的会话为准,Probe 自报的值会被覆盖。
|
||||
|
||||
@@ -110,7 +110,7 @@ Probe 只记录自己一侧的列,Agent 在数据面记录另一侧,由 Agen
|
||||
|
||||
### 通用规则
|
||||
|
||||
- Fleet 层的兼容规则见 [../backend/fleet/README.md](../backend/fleet/README.md)。
|
||||
- Fleet 层的兼容规则见 [../backend/fleet/README.md](../../backend/fleet/README.md)。
|
||||
- `moshiro.*` 方法:新增方法或可选字段不需要升级版本,接收方忽略未知字段。
|
||||
- RawChunk:块头自描述,新增列或新增 Kind 不需要升级 `container_version`;只有容器格式变化时才升级。
|
||||
- UDP 包和数据连接首帧的布局变化时,必须升级 `protocol_version`,并更新黄金字节测试。
|
||||
@@ -1,6 +1,6 @@
|
||||
# 展示数据查询(前端取数)
|
||||
|
||||
本文定义前端从 Backend 拉取展示数据的方式:时间段、步长、指标选择、响应格式、实时跟随、缓存与限额。端点清单见 [api.md](api.md) 第 2 节;指标与累加器见 [analysis.md](analysis.md)。
|
||||
本文定义前端从 Backend 拉取展示数据的方式:时间段、步长、指标选择、响应格式、实时跟随、缓存与限额。端点清单见 [api.md](../api/api.md) 第 2 节;指标与累加器见 [analysis.md](analysis.md)。
|
||||
|
||||
## 1. 设计目标
|
||||
|
||||
@@ -123,7 +123,7 @@ id 的格式为 `指标.统计量`,详见 [analysis.md](analysis.md) 第 4 节
|
||||
}
|
||||
```
|
||||
|
||||
- 时间轴 `t` 使用 Unix 毫秒,只出现一次;`values` 中每个数组的长度都与 `t` 相同,可以直接交给 uPlot 或 ECharts。**这是 [api-guidelines.md](api-guidelines.md) 中“时间用 RFC 3339”规则的明确例外**,理由是减小体积、方便绘图;其余时间字段仍然使用 RFC 3339。
|
||||
- 时间轴 `t` 使用 Unix 毫秒,只出现一次;`values` 中每个数组的长度都与 `t` 相同,可以直接交给 uPlot 或 ECharts。**这是 [api-guidelines.md](../api/api-guidelines.md) 中“时间用 RFC 3339”规则的明确例外**,理由是减小体积、方便绘图;其余时间字段仍然使用 RFC 3339。
|
||||
- 某个桶没有数据时填 `null`(不填 0),前端据此绘制断线。
|
||||
- `summary` 是**整个时间段**的精确合并结果。`coverage` = 实际数据量 / 期望数据量,用来提示前端“数据是否完整”。
|
||||
- `gaps` 和 `events` 用于在曲线上做标注(断线、端点变化、时钟跳变等)。
|
||||
File renamed without changes.
@@ -108,4 +108,4 @@
|
||||
|
||||
- 提交信息使用 `<范围>: <摘要>` 格式,范围取值为 `probe`、`agent`、`backend`、`shared`、`web`、`docs`、`scripts`。
|
||||
- 修改 `backend/shared/MoShiro.Protocol` 的 PR 必须说明兼容性影响(新增字段、行为变化、需要升级版本)。
|
||||
- 修改 `backend/openapi/v1.json` 的 PR 必须按照 [api-guidelines.md](api-guidelines.md) 第 2 节判断是否属于破坏性变更。
|
||||
- 修改 `backend/openapi/v1.json` 的 PR 必须按照 [api-guidelines.md](../api/api-guidelines.md) 第 2 节判断是否属于破坏性变更。
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
1. **三个独立项目加一个前端**:Probe、Agent、Backend 各自拥有解决方案,可以单独构建、测试和发布;Web 是独立的 Vite 项目(`web/`)。
|
||||
2. **通用能力只写一次**:TCP/UDP/TLS/ICMP 采集积木(类似 iperf3 的能力,但只记录原始数据)放在 `MoShiro.Measurement` 中,Probe 和 Agent 共用;采集方法(Kind)由积木组合声明而成。
|
||||
3. **节点机制通用化**:注册、身份、连接、RPC、可靠上报、状态同步、宿主,由与业务无关的 **Fleet** 框架提供(见 [../backend/fleet/README.md](../backend/fleet/README.md))。MoShiro 只写业务,其他产品也可以复用 Fleet。
|
||||
3. **节点机制通用化**:注册、身份、连接、RPC、可靠上报、状态同步、宿主,由与业务无关的 **Fleet** 框架提供(见 [../backend/fleet/README.md](../../backend/fleet/README.md))。MoShiro 只写业务,其他产品也可以复用 Fleet。
|
||||
4. **改一处,整条链路受益**:协议、原始数据格式、测试方案、指标、错误码、版本号、编译设置、代码风格都只有**一个真源**。改动之后,下游要么自动更新,要么在编译期或 CI 中直接报错。
|
||||
5. **模块化**:Probe/Agent 由可插拔的模块组成;Backend 按层分成多个项目,层之间的依赖在编译期强制。
|
||||
6. **Probe/Agent 轻量**:运行时只依赖 BCL 和仓库内的库,不引用任何 NuGet 包。
|
||||
@@ -49,7 +49,7 @@ moshiro/
|
||||
└─ MoShiro.Backend.slnx
|
||||
```
|
||||
|
||||
节点机器上的发布物是两个独立进程,注册和采集分开:`fleet-enroll`(一次性运行)和 `moshiro-agent`(或 `moshiro-probe`,常驻)。容器节点的更新由宿主机上的 `moshiro-update` 完成,见 [deployment.md](deployment.md)。
|
||||
节点机器上的发布物是两个独立进程,注册和采集分开:`fleet-enroll`(一次性运行)和 `moshiro-agent`(或 `moshiro-probe`,常驻)。容器节点的更新由宿主机上的 `moshiro-update` 完成,见 [deployment.md](../ops/deployment.md)。
|
||||
|
||||
## 3. 依赖关系
|
||||
|
||||
@@ -98,7 +98,7 @@ Web ──> openapi/v1.json
|
||||
|
||||
### 4.1 Fleet(通用节点框架)
|
||||
|
||||
组件、扩展点和协议细节见 [../backend/fleet/README.md](../backend/fleet/README.md)。
|
||||
组件、扩展点和协议细节见 [../backend/fleet/README.md](../../backend/fleet/README.md)。
|
||||
|
||||
### 4.2 MoShiro.Protocol(MoShiro 的契约真源)
|
||||
|
||||
@@ -127,7 +127,7 @@ MoShiro.Protocol/
|
||||
- `TaskReconciler`(实现 Fleet 的 `IStateReconciler<TaskSet>`);
|
||||
- `BudgetGuard`。
|
||||
|
||||
详见 [measurement.md](measurement.md) 第 6 节。
|
||||
详见 [measurement.md](../design/measurement.md) 第 6 节。
|
||||
|
||||
## 5. 模块化
|
||||
|
||||
@@ -241,7 +241,7 @@ M0 阶段先测出基线,之后作为 CI 的回归门槛(超出 10% 即失
|
||||
- **MIPS 不支持**(NativeAOT 没有 MIPS 目标)。使用 MIPS 路由器(例如 MT7621)时,把 Probe 运行在局域网内的其他设备上,比如 NAS、树莓派或 PC,测量结果同样代表这条家宽。
|
||||
- NativeAOT 需要目标平台的原生工具链:musl 目标在 Alpine 容器中构建,arm64 使用交叉编译镜像。CI 矩阵为 Windows、Linux glibc、Linux musl。
|
||||
- `scripts/publish.sh <probe|agent|backend|fleet-enroll> <rid>` 输出到 `backend/artifacts/publish/<project>/<rid>/`。
|
||||
- 容器镜像由 CI 构建并对镜像 digest 签名,节点的宿主机更新器验签后更新,见 [deployment.md](deployment.md);纯二进制节点的更新见 [TODO.md](../TODO.md)。
|
||||
- 容器镜像由 CI 构建并对镜像 digest 签名,节点的宿主机更新器验签后更新,见 [deployment.md](../ops/deployment.md);纯二进制节点的更新见 [TODO.md](../../TODO.md)。
|
||||
- Linux 上 TLS 依赖系统 OpenSSL(`libssl.so.3`)。
|
||||
|
||||
### 8.1 OpenWrt 上的 Probe
|
||||
@@ -256,8 +256,8 @@ M0 阶段先测出基线,之后作为 CI 的回归门槛(超出 10% 即失
|
||||
| 内存 | 预算 ≤ 32 MB,适合 ≥ 256 MB 内存的路由器;在本地配置中限制重负载测试的码率(`limits.max_test_mbps`),防止路由器 CPU 成为瓶颈,反过来影响测量结果 |
|
||||
| 测量偏差 | Probe 运行在网关上时,自己没有 NAT;一旦路由器 CPU 饱和,结果就会失真。因此 Probe 在 `moshiro.event` 中上报自身的 CPU 繁忙标记(仅进程级别),分析层据此标注置信说明 |
|
||||
| 打包 | 提供 `tar.gz` 和 procd 脚本;包格式(ipk 或 apk)按目标 OpenWrt 版本提供 |
|
||||
| 更新 | 纯二进制节点的更新器还没有实现([TODO.md](../TODO.md));实现时 `keep_versions = 1`,下载前检查剩余空间,避免写满闪存 |
|
||||
| 更新 | 纯二进制节点的更新器还没有实现([TODO.md](../../TODO.md));实现时 `keep_versions = 1`,下载前检查剩余空间,避免写满闪存 |
|
||||
|
||||
## 9. Web
|
||||
|
||||
`web/` 是 npm workspaces:`display`(展示端,匿名可读)、`admin`(管理端)、`shared`(API 客户端、图表、组件、主题)。Vite 8、React 19、TypeScript,图表用 uPlot;唯一的契约是 `backend/openapi/v1.json`,用 `openapi-typescript` 生成类型(`npm run gen:api`)。颜色只能写在 `shared/theme/tokens.css`,由 stylelint 强制。详见 [web/README.md](../web/README.md)。
|
||||
`web/` 是 npm workspaces:`display`(展示端,匿名可读)、`admin`(管理端)、`shared`(API 客户端、图表、组件、主题)。Vite 8、React 19、TypeScript,图表用 uPlot;唯一的契约是 `backend/openapi/v1.json`,用 `openapi-typescript` 生成类型(`npm run gen:api`)。颜色只能写在 `shared/theme/tokens.css`,由 stylelint 强制。详见 [web/README.md](../../web/README.md)。
|
||||
@@ -58,11 +58,11 @@ Compose 将容器端口只绑定到宿主机 `127.0.0.1:8080`,适合由同机
|
||||
|
||||
Backend 的 NativeAOT 发布默认启用 `SqliteStaticLink`,支持 Linux glibc/musl、macOS、Windows 的 x64/arm64 RID;跨平台构建使用 `-p:UseAotAnywhere=true`。SQLite 源码版本、下载年份和 SHA-256 统一配置在根目录 `Directory.Packages.props`,构建会校验下载文件并按 RID 缓存原生对象。普通 JIT 开发构建仍使用 NuGet 原生库。发布 Backend 时设置 `-p:OpenApiGenerateDocuments=false`,API 文档在普通开发构建中生成。
|
||||
|
||||
健康检查端点为 `GET /healthz`。容器定义见 [`../packaging/container/Backend.Containerfile`](../packaging/container/Backend.Containerfile)。
|
||||
健康检查端点为 `GET /healthz`。容器定义见 [`../packaging/container/Backend.Containerfile`](../../packaging/container/Backend.Containerfile)。
|
||||
|
||||
## 节点二进制发布
|
||||
|
||||
Woodpecker 在 `vMAJOR.MINOR.PATCH` tag 上,由 Windows runner 使用 AotAnywhere 并行构建 Agent/Probe 的 Linux glibc、Linux musl、macOS 和 Windows 产物。Agent 包还包含一次性注册工具 `fleet-enroll`。Linux 归档带有安装脚本和 systemd/OpenWrt 单元;Windows 产物为 `.zip`,Linux/macOS 产物为 `.tar.gz`。Linux musl 的 Agent 与 Probe 二进制还会打包为 OCI 镜像,基底为 `mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine`,支持 `linux/amd64` 和 `linux/arm64`;所有平台仍提供对应归档。所有平台产物和 OCI 镜像发布成功后,CI 会创建对应的 Gitea Release,并附上压缩包和 SHA-256 校验文件。macOS 产物目前未签名/公证。安装说明见 [`../backend/agent/README.md`](../backend/agent/README.md) 与 [`../backend/probe/README.md`](../backend/probe/README.md)。
|
||||
Woodpecker 在 `vMAJOR.MINOR.PATCH` tag 上,由 Windows runner 使用 AotAnywhere 并行构建 Agent/Probe 的 Linux glibc、Linux musl、macOS 和 Windows 产物。Agent 包还包含一次性注册工具 `fleet-enroll`。Linux 归档带有安装脚本和 systemd/OpenWrt 单元;Windows 产物为 `.zip`,Linux/macOS 产物为 `.tar.gz`。Linux musl 的 Agent 与 Probe 二进制还会打包为 OCI 镜像,基底为 `mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine`,支持 `linux/amd64` 和 `linux/arm64`;所有平台仍提供对应归档。所有平台产物和 OCI 镜像发布成功后,CI 会创建对应的 Gitea Release,并附上压缩包和 SHA-256 校验文件。macOS 产物目前未签名/公证。安装说明见 [`../backend/agent/README.md`](../../backend/agent/README.md) 与 [`../backend/probe/README.md`](../../backend/probe/README.md)。
|
||||
|
||||
## CI 工作流
|
||||
|
||||
@@ -195,7 +195,7 @@ OpenWrt 默认不带 OpenSSL:`opkg install libopenssl3`(较新的快照和 2
|
||||
|
||||
**国家和运营商**:Backend 用 Probe、Agent 的来源地址向 IP 归属服务查一次,展示在国旗和运营商图标上(`MoShiro__GeoLookupUrl`,缺省 `https://ip.007995.xyz`;留空就不查;这个服务只供公益使用,署名和使用条款由部署的人自己遵守)。Probe、Agent 自己不访问外部服务,所以客户的防火墙审计里不会多出出站连接;Backend 发出去的只有公网 IP(内网地址不发)。
|
||||
|
||||
**从别处看这个面板**:本机跑前端,在页面里把数据源指向这个域名,用 API 令牌访问,Backend 设 `MoShiro__CorsOrigins` 放行前端的来源(见 [web/README.md](../web/README.md))。
|
||||
**从别处看这个面板**:本机跑前端,在页面里把数据源指向这个域名,用 API 令牌访问,Backend 设 `MoShiro__CorsOrigins` 放行前端的来源(见 [web/README.md](../../web/README.md))。
|
||||
|
||||
实测(Probe 在广东移动家宽,Agent 在日本 IIJ):
|
||||
|
||||
@@ -215,15 +215,15 @@ OpenWrt 默认不带 OpenSSL:`opkg install libopenssl3`(较新的快照和 2
|
||||
|
||||
镜像和 Compose 配置共用。普通用户执行 Podman 即为 rootless,使用命名 volume 不需要 `keep-id` 或手动 `chown`。Agent 的配置、原有身份和缓存保存在 `moshiro-agent-config`,Probe 保存在 `moshiro-probe-config`,更新状态使用独立的 `moshiro-<role>-status` volume,在业务容器内只读。不会重新生成已有身份。更新/重建容器不删除 volume;不要使用 `compose down -v`,该参数会删除数据。
|
||||
|
||||
**一行安装(Docker 或 Podman,Agent 和 Probe 共用)**:管理端「添加 Agent」「添加 Probe」生成加入令牌之后,给出一行命令,面板地址和一次性代码已经填好,没有要自己替换的占位符;节点名字默认取主机名:
|
||||
**一行安装(Docker 或 Podman,Agent 和 Probe 共用)**:管理端「添加 Agent」「添加 Probe」生成加入令牌之后,给出一行命令,面板地址、一次性代码、信任根指纹和属主都已经填好,没有要自己替换的占位符;节点名字默认取主机名。命令先下载静态安装脚本、核对它的 SHA-256,一致才执行:
|
||||
|
||||
```sh
|
||||
curl -fsSL 'https://panel.example.com/install?code=jt_…' | bash # Linux
|
||||
curl -fsSL 'https://panel.example.com/install?code=jt_…' | zsh # macOS
|
||||
irm 'https://panel.example.com/install.ps1?code=jt_…' | iex # Windows(PowerShell)
|
||||
f=$(mktemp) && curl -fsSL 'https://panel.example.com/install' -o "$f" && echo "<sha256> $f" | sha256sum -c - >/dev/null \
|
||||
&& MOSHIRO_SERVER=… MOSHIRO_ROLE=… MOSHIRO_CODE=jt_… MOSHIRO_GENESIS=… MOSHIRO_CONTROLLERS=… sh "$f" # Linux、macOS
|
||||
# Windows(PowerShell):irm 'https://panel.example.com/install.ps1' 下载后用 Get-FileHash 核对,不一致就 throw,再带同样的环境变量运行
|
||||
```
|
||||
|
||||
- 脚本是同一份(`packaging/install/install.sh` 和 `install.ps1`,作为嵌入资源打进 Backend),由 Backend 在 `/install` 和 `/install.ps1` 按代码填好角色、面板地址和镜像;Agent 还是 Probe 由代码决定。代码不存在、已吊销、已过期或用完了都是 404。
|
||||
- 脚本是静态的同一份(`packaging/install/install.sh` 和 `install.ps1`,作为嵌入资源打进 Backend,原样返回,不替换任何值),所以它的 SHA-256 在一个版本内固定;角色、面板地址、代码和属主走环境变量。脚本向面板取该角色已签名的发布清单(凭一次性代码),用内置的发布公钥验签,再按清单里的 digest 拉镜像;代码不存在、已吊销、已过期或用完了取清单都是 404。缺了 `MOSHIRO_GENESIS` 或 `MOSHIRO_CONTROLLERS` 就不装(只有还没有信任根的老面板才用 `MOSHIRO_ALLOW_NO_OWNER=1`)。
|
||||
- 脚本检测 Docker 或 Podman(连不上就自动用 sudo),拉镜像,起容器 `moshiro-<role>`:只读根文件系统、丢掉全部能力、Linux 上用宿主网络(Agent 对外暴露端口,也靠它识别本机公网地址),macOS 和 Windows 上 Agent 发布 7443、7444 端口;注册成功(日志出现 `link up`)才算装好。
|
||||
- 身份和配置在命名卷 `moshiro-<role>-config` 里(持久化):镜像入口脚本(`packaging/container/entrypoint.sh`)第一次启动用 `MOSHIRO_SERVER`、`MOSHIRO_JOIN`、`MOSHIRO_NAME` 注册并写配置,之后重启、换新镜像重建容器都不需要再给参数。卷名和部署脚本一致,以后可以直接切到脚本方式。
|
||||
- 环境变量:`MOSHIRO_NAME=名字`(想自己定,或者提示名字被占用)、`MOSHIRO_ENGINE=docker|podman`、`MOSHIRO_DRY_RUN=1`(只打印 run 命令)。Podman 会顺便启用 `podman-restart`,容器开机自启。
|
||||
@@ -237,7 +237,8 @@ irm 'https://panel.example.com/install.ps1?code=jt_…' | iex # Windows
|
||||
sudo sh deploy-node.sh agent --image-repo registry.oeo.one/moshiro/moshiro-agent --server https://<backend> --join <token> --name <name>
|
||||
sudo sh deploy-node.sh probe --image-repo registry.oeo.one/moshiro/moshiro-probe --server https://<backend> --join <probe 令牌> --name <name>
|
||||
# Backend(只支持 Podman,更新策略固定为管理端确认;--port 默认 29181,环境文件写在 /etc/moshiro/backend.env,已有的不覆盖)
|
||||
sudo sh deploy-node.sh backend --channel dev --admin-password '<密码>' --public-url https://<面板域名>
|
||||
sudo sh deploy-node.sh backend --channel dev --public-url https://<面板域名>
|
||||
# 没有默认管理员:首次打开面板按页面引导认领(认领码写在后端日志里,或用 --env MoShiro__ClaimCode=… 自己指定)
|
||||
# 其他环境变量用 --env 原样写入(只在首次创建 backend.env 时生效):本地开发前端要连这个面板时加
|
||||
# --env MoShiro__CorsOrigins=http://localhost:5173,http://localhost:5174 --env MoShiro__SecureCookies=false
|
||||
# 面板前面有反向代理(Caddy、frp、Cloudflare)时,登录限流和审计看到的是代理的地址:把代理的地址写进可信代理,才会采信转发头里的客户端地址
|
||||
@@ -324,7 +325,7 @@ sh packaging/update/install-updater.sh agent --user \
|
||||
3. 下一个版本起把 `release_signing_key` 换成新私钥、去掉 `RELEASE_NEXT_KEY`,并把 `packaging/update/release-key.pub.pem`、`install.sh`、`install.ps1` 里的公钥换成新的(安装脚本哈希随清单变化)。
|
||||
4. 还没收到过渡清单的节点(离线很久)会因为签名对不上而停在旧版本,需要手动换 `release-key.pub.pem`;旧密钥泄露时,这些节点是唯一的暴露面。
|
||||
|
||||
一行安装:页面给的命令先下载静态安装脚本(`/install`,所有人拿到的字节都一样)并核对它的 SHA-256,一致才执行;面板地址、角色、一次性码、信任根指纹(`MOSHIRO_GENESIS`)和属主(`MOSHIRO_CONTROLLERS`)走环境变量,都是公开值,没有私钥。脚本向面板取 CI 签名的发布清单(`/install/manifest`、`/install/manifest.sig`),用脚本里内置的发布公钥(`packaging/update/release-key.pub.pem`)验签,再按清单里的 digest 拉镜像(`镜像仓库@sha256:…`);签名对不上、组件不对或 digest 格式不对就拒绝安装;需要本机有 `openssl`(Windows 用 .NET 自带的)。面板还没有这个组件的已签名清单时,设 `MOSHIRO_ALLOW_UNVERIFIED=1` 和 `MOSHIRO_IMAGE=镜像` 才会安装。CI 把脚本的 SHA-256 写进签名清单和发布页,面板的「系统 → 信任」页显示它和清单是否一致。详见 [access.md](access.md) 第 0 节。
|
||||
一行安装:页面给的命令先下载静态安装脚本(`/install`,所有人拿到的字节都一样)并核对它的 SHA-256,一致才执行;面板地址、角色、一次性码、信任根指纹(`MOSHIRO_GENESIS`)和属主(`MOSHIRO_CONTROLLERS`)走环境变量,都是公开值,没有私钥。脚本向面板取 CI 签名的发布清单(`/install/manifest`、`/install/manifest.sig`),用脚本里内置的发布公钥(`packaging/update/release-key.pub.pem`)验签,再按清单里的 digest 拉镜像(`镜像仓库@sha256:…`);签名对不上、组件不对或 digest 格式不对就拒绝安装;需要本机有 `openssl`(Windows 用 .NET 自带的)。面板还没有这个组件的已签名清单时,设 `MOSHIRO_ALLOW_UNVERIFIED=1` 和 `MOSHIRO_IMAGE=镜像` 才会安装。CI 把脚本的 SHA-256 写进签名清单和发布页,面板的「系统 → 信任」页显示它和清单是否一致。详见 [access.md](../security/access.md) 第 1 节。
|
||||
|
||||
## 签名清单放在镜像仓库里(更新和首次部署都不依赖面板)
|
||||
|
||||
@@ -1,312 +0,0 @@
|
||||
# 零信任设计
|
||||
|
||||
> **实现状态:阶段零(协议方向声明表和卡口,第 8 节)已实现,其余是设计稿,尚未实现。** 第 3 节的缺口是对当前代码的核对结果(G7 已解决);第 4 节起是目标设计和实施顺序。第 14 节列出还没定的事。
|
||||
|
||||
Agent、Backend、Probe 之间互相不信任。这份文档只回答三个问题:现在谁听谁的、被攻破之后最坏会怎样、怎么用密码学把信任从 Backend 身上拿走。纯采集的边界、本地硬上限和签名更新见 [architecture.md](architecture.md) 第 5 节,这里不重复。
|
||||
|
||||
## 1. 范围
|
||||
|
||||
**要防的**
|
||||
|
||||
- Backend(或 Backend 前面的 Cloudflare Tunnel、frp、Caddy 等中间环节)被攻破或被劫持之后,给节点下发恶意内容。
|
||||
- Agent 或 Probe 自己被入侵之后,去影响 Backend 或对端。
|
||||
|
||||
**有意不做的**
|
||||
|
||||
- **进程沙箱**(seccomp、pids 和内存上限、出站白名单)。Agent 和 Probe 的定位是 1 核 1 GB 的小机器,不能吃资源。
|
||||
- **供应链的多签、冷却期和可复现构建**。代码风险靠 PR 评审降一个等级;发布仍然是单把发布密钥签名(见 [deployment.md](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 | 某个 Probe(在客户内网里) | 客户机器被入侵 |
|
||||
| T4 | 管理员的会话或令牌 | 令牌泄露 |
|
||||
| T5 | 管理员的浏览器 | XSS,或者节点上报的字符串被渲染 |
|
||||
|
||||
## 3. 现状:节点听谁的,缺口在哪
|
||||
|
||||
```text
|
||||
CI 发布私钥(单把)
|
||||
│ 签名清单
|
||||
管理员 ──令牌/会话──▶ ┌──────────┐ ◀── 原始块、事件、主机数字(没有按节点的配额)
|
||||
│ Backend │
|
||||
└──┬───┬───┘
|
||||
任务集、对端公钥、 │ │ Agent 名单(host:port,Probe 全部照拨)
|
||||
重启计划、更新清单 ▼ ▼
|
||||
┌──────┐ ┌──────┐
|
||||
│Agent │◀─▶│Probe │ Probe 私钥由 Backend 生成
|
||||
└──────┘ └──────┘
|
||||
```
|
||||
|
||||
| # | 缺口 | 依据 | 后果 |
|
||||
| --- | --- | --- | --- |
|
||||
| G1 | 节点不验证 Backend 的身份,只靠 TLS 加公共 CA | `Fleet.Link` 里没有证书固定,也没有应用层验证 | 链路上任一环节被攻破,就能给节点下发任务集、Agent 名单、对端公钥、重启计划(更新清单有签名,不受影响) |
|
||||
| G2 | Probe 的私钥由 Backend 生成 | `ProbeService` 里的 `NodeIdentity.Generate`(创建和轮换) | Backend 接触过 Probe 的私钥,可以冒充它;Agent 的密钥是自己生成的,没有这个问题 |
|
||||
| G3 | Probe 会拨 Backend 给的任意 `host:port` | `AgentDirectory`、`AgentConnection` 不做任何地址检查;本地 `limits` 只有速率和 pps | 被攻破的 Backend 能让客户内网里的 Probe 去探测内网。固定 SPKI 只能防止建立会话,防不了探测本身 |
|
||||
| G4(已修,阶段二) | Agent 的 TCP 端口对任何人完成 TLS 握手并出示证书 | `TcpAcceptor`、`PeerListener` 在 `AcceptTcpClient` 之后直接 `AuthenticateAsServer` | 自签证书、TLS 参数、JARM 都能被扫描器指纹识别,一眼看出这是个服务 |
|
||||
| G5 | Backend 入库只限制了单块的大小 | `MaxDecodedChunkBytes` 等;没有找到按节点的速率或配额 | 恶意节点可以灌满磁盘 |
|
||||
| G6 | 对端公钥、Agent 地址全由 Backend 决定 | `TaskSet.Peers`、`ProbeAgentList` | 被攻破的 Backend 能把恶意 Probe 加进 Agent 的白名单,或把 Probe 指向恶意 Agent |
|
||||
| G7(已解决) | 方法的方向和角色没有统一声明:原来靠「谁注册了处理器」隐式决定 | `NodeConnection.HandleMessageAsync` 只按方法名分发到路由器;文档里的 `B→A`、`A→B` 只存在于 [protocol.md](protocol.md),没有对账 | 新增一个方法时,方向写反、角色写错(例如 Probe 调了只给 Agent 的方法)不会被任何东西发现 |
|
||||
| G8 | **测量数据没有产生者签名** | `Raw/` 的编解码里没有任何签名或 MAC,块只有 CRC32C;Probe 的列块(通道 17)经 Agent 合并进原始块再上传 | 隧道等中间环节可以改写测量数据;被攻破的 Agent 可以伪造 Probe 侧的时间戳(`t1`、`t4`) |
|
||||
| G9 | **入库只核对了 `agent_id`** | `IngestService` 在 `header.AgentId != agent` 之后直接采用块头里的 `series`(形如 `line:<probe_id>`),全文没有出现 Probe | 被攻破的 Agent 可以为从没连过它的 Probe 灌入假线路数据 |
|
||||
| G10 | **没有合理性检查** | `EchoOps` 里没有对回显时间戳做任何一致性检查 | 恶意 Agent 可以谎报 `t2`、`t3`,把自己的处理时延说成任意值,让线路 RTT 好看或难看 |
|
||||
| G11 | **Probe 的一把密钥多处使用** | [architecture.md](architecture.md) 第 5 节写明同一把用来连 Backend 和连 Agent;已有用途前缀(`auth`、`peer`)做域隔离 | 一处泄露,两处都沦陷 |
|
||||
|
||||
没有问题的部分:UDP 端口对未认证的包静默丢弃,不回应(`DropInvalid`,有测试);Probe 本身没有 TCP 监听;任务有本地硬上限;更新清单有签名和 `seq` 防回滚。
|
||||
|
||||
## 4. 核心思路:信任锚点从 Backend 移走
|
||||
|
||||
要防 Backend 被攻破,**下行内容必须由 Backend 之外的密钥授权**。Backend 只搬运、计算和存储,不创造信任:
|
||||
|
||||
```text
|
||||
① 发布密钥(CI) ──▶ 「跑什么代码」 已有:清单签名、seq 防重放
|
||||
② 操作员密钥(管理员本人) ──▶ 「谁是舰队成员、去哪拨」 新增:Backend 持有不了
|
||||
③ 节点自己的密钥 ──▶ 「我是我」 已有;Probe 要改成自己生成
|
||||
|
||||
被攻破的 Backend 能做的:拒绝服务、看数据。
|
||||
不能做的:加入新节点、改地址让 Probe 去探内网、塞恶意更新、冒充节点。
|
||||
```
|
||||
|
||||
## 5. 入网信任链
|
||||
|
||||
下文和其他章节里的「名册」指节点本地验过链的成员表。**详细设计见 [access.md](access.md)**(账户、角色、能力、API key、签名链、流程和实施顺序)。这里只留结论:
|
||||
|
||||
- 名册不再是「每次变更都要操作员签一整张表」,而是一条可离线验证的链:节点固定创世 owner 公钥 → 操作员集合(带版本,owner 签)→ 委托(passkey 签,限定范围和时间)→ 入网券(委托的密钥签)→ 节点绑定(节点注册时用安装命令里的 `K_code` 签)。加上 passkey 当场签的吊销表。
|
||||
- Agent 只接纳链验得过的 Probe,Probe 只拨链验得过的 Agent,并且只拨公网地址(缺口 G3、G6 因此消失,不需要另外的拨号白名单)。
|
||||
- **不可回放**:节点持久化见过的最大操作员集合版本和吊销表版本,Backend 无法拿旧文档复活已吊销的节点。
|
||||
- 加节点不需要 passkey:创建加入码是普通操作,命令里只有公开的值(创世指纹、属主、脚本哈希);自动化用带 `fleet.enroll` 的 API key。信任来自节点注册时固定的创世指纹和属主。
|
||||
|
||||
## 6. 操作员凭证
|
||||
|
||||
操作员凭证就是 passkey(WebAuthn,ES256,attestation 选 `none`)。节点本地固定创世 owner 公钥,其余凭证靠 owner 签名的操作员集合同步,不用重装节点;每条凭证带角色(`owner`、`admin`)和限制(例如单张入网券的最长有效期),由节点自己执行。
|
||||
|
||||
- 登录可以用密码、passkey 或 OIDC,会话能力相同;但**只有 passkey 能给操作员集合签名**:节点验证不了 IdP 的断言,也不该让密码和 IdP 进入信任域。OIDC 只是账户的另一种登录方式(绑定 `iss + sub`)。
|
||||
- 自动化凭证:`enroll` API key 创建时由 passkey 签一份委托;CI 和测试用软件 ECDSA 凭证,写进测试的操作员集合。
|
||||
- 恢复:passkey 全部丢失时,重装节点并换创世公钥;所以每个账户至少注册两个 passkey。
|
||||
|
||||
## 7. 各类下行消息怎么处置
|
||||
|
||||
| 下行内容 | 授权 | 被攻破的 Backend 最多能做什么 |
|
||||
| --- | --- | --- |
|
||||
| 舰队成员、对端公钥 | **操作员签名**的入网链和吊销表 | 什么都改不了;Agent 地址由 Backend 下发,但 Probe 只拨公网地址 |
|
||||
| 更新清单 | 发布密钥(已有) | 不能伪造;能转发旧的合法清单,`seq` 防回滚 |
|
||||
| 任务集、测试请求 | Backend 计算;节点本地硬上限兜底(已有) | 只能在名册成员之间、本地速率和端口上限内发测量流量 |
|
||||
| 重启计划(`moshiro.schedule`) | Backend;节点本地启动后 10 分钟内不触发 | 最多让节点每 10 分钟重启一次 |
|
||||
| 未来的 `extension`(exec 等) | **操作员签名的一次性能力令牌**,见第 11 节 | 什么都做不了 |
|
||||
|
||||
另外 Backend 自己对节点也有一条应用层身份:Backend 有长期签名密钥,节点在注册时固定它,下行消息带签名、节点 ID、序号和有效期。它防的是你的 Cloudflare、frp 链路被劫持(G1);对 Backend 自己被攻破没有帮助,那一类靠名册。
|
||||
|
||||
## 8. 协议的方向与平面
|
||||
|
||||
### 8.1 三个平面,各认各的凭证
|
||||
|
||||
```text
|
||||
外部面(人、浏览器、脚本) /api/public(匿名只读,SSE) /api/admin(Cookie + CSRF,或 API 令牌)
|
||||
/install.sh /openapi /healthz /metrics
|
||||
节点面(节点主动拨入) /api/agent/v1/enroll(加入令牌,一次性) /api/agent/v1/rpc(wss,节点签名认证)
|
||||
对等面(Probe 拨 Agent) TLS 控制 7443/tcp、数据 7443/udp、7444/tcp(名册里的成员才能通过)
|
||||
```
|
||||
|
||||
- **每个平面只认自己的凭证**:节点面不接受令牌和 Cookie;外部面不接受节点签名;对等面只认名册成员。任何一个平面的凭证都换不到另一个平面的权限。
|
||||
- **连接方向固定**:节点面永远是节点拨入 Backend,Backend 从不主动连节点,所谓「下发」只是在节点拨入的连接上发下行消息;对等面永远是 Probe 拨 Agent,Agent 不拨 Probe。
|
||||
- **外部面不能直通节点面**:管理端的操作(例如发起测试)只会触发 Backend 里一个**强类型**的下行方法,管理端不能指定方法名,也不能把请求体原样转给节点。现状已满足(`IAgentGateway` 里全是强类型方法),要写成约束并用测试守住。
|
||||
- 现状:三个平面共用同一个监听端口和同一条 ASP.NET 管线,靠路径和凭证类型区分。目标是把节点面和外部面做成可以分别配置监听地址(默认仍同一个),这样管理面可以只对内网或 VPN 开放,节点面单独放出去。
|
||||
|
||||
### 8.2 每个方法声明方向、类型和角色
|
||||
|
||||
**同一个方法名只允许一个发起方**,不存在「双向方法」。需要两个方向交互时,定义两个方法。一个方法声明四件事:
|
||||
|
||||
| 声明 | 取值 | 含义 |
|
||||
| --- | --- | --- |
|
||||
| 发起方 | `B→A`、`B→P`、`A→B`、`P→B`、`P→A`、`A→P` | 只有它能发出这个方法,对方收到就是违规 |
|
||||
| 类型 | 请求(有响应)、通知(无响应) | 通知不允许回复;响应只对应一个未决的请求 ID,且那个方法必须是请求 |
|
||||
| 角色 | `A`、`P`、`A+P` | 节点角色不对就拒绝(Probe 不能调 Agent 的方法) |
|
||||
| 授权 | 无、`operator`(需要操作员签名的能力令牌) | 为 extension 预留,核心方法都是「无」 |
|
||||
|
||||
方向总表(来自 [protocol.md](protocol.md),实现时以代码里的声明为准):
|
||||
|
||||
| 方法 | 方向 | 类型 | 角色 |
|
||||
| --- | --- | --- | --- |
|
||||
| `fleet.challenge`、`fleet.state.sync`、`fleet.outbox.ack`、`fleet.revoked` | B→节点 | 见 Fleet 说明 | A+P |
|
||||
| `fleet.auth`、`fleet.hello`、`fleet.heartbeat` | 节点→B | 见 Fleet 说明 | A+P |
|
||||
| `moshiro.test.run`、`moshiro.test.cancel`、`moshiro.probe.disconnect` | B→A | 请求 | A |
|
||||
| `moshiro.update.offer`、`moshiro.update.apply`、`moshiro.schedule` | B→节点 | 通知 | A+P |
|
||||
| `moshiro.test.status`、`moshiro.event`、`moshiro.host` | A→B | 通知 | A |
|
||||
| 二进制通道 16(`RawChunk`) | A→B | 发件箱 | A |
|
||||
| 对等面的方法和通道 17(列块) | P→A、A→P,按 [protocol.md](protocol.md) | 按声明 | P、A |
|
||||
|
||||
### 8.3 强制方式
|
||||
|
||||
1. **一份声明表,单一真源**,放在 `MoShiro.Protocol`(Fleet 自己的 `fleet.*` 放在 Fleet);文档里的方向表由它生成或对账。
|
||||
2. **发送前检查**:发错方向是编程错误,直接失败。
|
||||
3. **接收时检查**:收到不该由对方发起的方法、未声明的方法、角色不对的方法,丢弃并计数,连续违规断开连接。
|
||||
4. **响应匹配**:只接受对应未决请求 ID 的响应,不接受主动「响应」。
|
||||
5. **对账测试**(`ArchitectureTests`):声明表和实际注册的处理器逐项比对,新增方法绕不过声明。
|
||||
6. **下行消息只是建议,不是命令**:节点按本地硬上限执行或拒绝;任何带副作用的敏感能力都不走核心的下行消息,见第 11 节。
|
||||
|
||||
**已实现**(阶段零):
|
||||
|
||||
- 声明表:`Fleet.Rpc/MethodTable.cs`(`MethodSpec`、`ChannelSpec`、`MethodTable`);框架的 `fleet.*` 在 `FleetMethodTable`,`moshiro.*` 和两条二进制通道在 `MoShiro.Protocol` 的 `MoShiroMethodTable`;角色是 `NodeRoles`(服务端按节点 ID 前缀判断)。声明表按(平面,名字)查找,同名方法在两个平面里可以有不同的发起方(`fleet.state.sync` 在节点面是 Backend→节点,在对等面是 Agent→Probe)。
|
||||
- 卡口:`RpcRouter` 创建时必须给出本侧(`RpcSide`)和声明表。**注册**处理函数时核对方向、类型、角色,不符就在启动时失败;**分发**时拒绝没有声明、方向不对、请求和通知用反了、角色不允许的消息,回 `fleet_protocol_violation` 并报告违规;**发送**前核对本侧有没有资格发起(`CheckSend`,违反是程序缺陷,抛异常)。
|
||||
- 违规处理:`RpcSession` 和服务端的 `NodeConnection` 对违规计数,超过上限(`MaxViolations`,默认 20)就断开连接;二进制通道也在服务端按声明核对(节点只能往声明过、并且允许它的角色使用的通道发)。
|
||||
- 对账测试:`Fleet.Tests/MethodDirectionTests` 覆盖卡口本身;`MoShiro.Backend.Tests/ProtocolDeclarationTests` 核对「每个方法常量都声明了」「文档里写了每个方法」「Backend、Agent、Probe 两个平面各自该处理的方法都注册了处理函数」「每条声明的通道都有收件箱」。
|
||||
- 这次落地时对账发现了两处不一致并修掉:文档漏了 `fleet.peer.challenge`、`fleet.peer.auth`;Agent 的 `moshiro.schedule` 处理函数原来只在传入重启控制器时才注册。
|
||||
|
||||
## 9. 节点自己被攻破时:数据可信与归属
|
||||
|
||||
### 9.1 测量数据的可信(G8、G9、G10)
|
||||
|
||||
测量数据是这个项目的产出,不能只靠传输层可信:你的链路有 Cloudflare 这样的中间环节,Agent 又要转发 Probe 的数据。
|
||||
|
||||
- **产生者签名**:每个列块由 Probe 签名,每个原始块由上传它的节点签名;Agent 转发 Probe 的块时原样保留 Probe 的签名。签名覆盖块的摘要(见第 13 节),签名串带用途前缀 `chunk` 和节点 ID。Backend 入库时逐个验签,不通过的丢弃并计数。每 10 秒一块,一次 ECDSA 验签的开销可以忽略。
|
||||
- **归属校验**:入库时核对块里的 Probe 是名册里和这个 Agent 有线路的成员,不是就丢弃并计数;线路只由名册里的(Probe,Agent)对产生。
|
||||
- **合理性检查**:`t3−t2` 必须小于 `t4−t1`,各时间戳单调;违反的样本不计入指标,记为异常并产生事件。
|
||||
- 边界:这样中间环节改不了数据,被攻破的 Agent 改不了 Probe 一侧的数据;但它仍然可以谎报自己一侧的 `t2`、`t3`,合理性检查只能限制它的幅度,消除不了,因为这一侧的真值只有它自己知道。
|
||||
|
||||
### 9.2 对 Backend 的资源和输入
|
||||
|
||||
- **按节点的配额**:每个节点的字节、块数和速率上限,只是计数,不占资源;全局再限制同时解码的块数(单块解码上限是 64 MiB,多个节点同时发大块可以耗尽内存)。
|
||||
- **节点上报的字符串**:`hello` 里的 `platform`、事件的 `detail` 等一律当不可信数据,入库前统一校验长度、字符集和换行,不依赖前端转义;管理端加 CSP,不渲染成 HTML。
|
||||
- **匿名公开接口**:登录已经有限流,但热力图、分布这类重查询只有范围和指标数的上限,要加每源限速和结果缓存(TODO 里的人机验证会部分覆盖)。
|
||||
- **webhook**(TODO 里有):实现之前先定约束:只允许 HTTPS、禁止私网和回环地址、不跟随重定向、限制超时和响应大小、解析之后再检查地址。
|
||||
- 解码器保持大小上限;模糊测试已在 TODO。
|
||||
|
||||
### 9.3 对对端和身份
|
||||
|
||||
- 被攻破的 Probe 只能拿到名册授权的会话,Agent 的速率上限由本地兜底;被攻破的 Agent 发来的数据只会影响它自己的线路。
|
||||
- **身份被偷**:操作员签一份新名册吊销它,所有节点立即拒绝,且因为 `version` 递增不可回放。
|
||||
- **Probe 私钥要自己生成**:现在 Backend 知道它(G2),名册也防不了这一点。改成和 Agent 一样用加入令牌注册,私钥只在节点上。
|
||||
- **密钥按用途拆开**(G11):面向 Backend 的和面向对等面的是两把独立的 P-256 密钥;敲门、会话等对称用途用 KDF 加用途标签派生,不复用签名密钥直接做别的事。
|
||||
|
||||
### 9.4 对设计的补充要求(实现时核实现状)
|
||||
|
||||
- **所有下行状态文档都防回放**:节点持久化每种状态文档见过的最大 `version`,低版本一律拒绝,不只是名册。
|
||||
- **UDP 会话迁移要做路径验证**:协议允许会话在 NAT 映射变化后跟随新的源地址,路径上的攻击者可以借此把会话牵到自己的地址。迁移前先做一次挑战应答,验证前限制发送量(不超过收到字节数的 3 倍),类似 QUIC 的 path validation。
|
||||
- **名册里的主机名解析之后再检查地址**:签名保护的是字符串,不是解析结果;默认只放行公网地址,内网需要节点本地显式允许;名册里优先写地址字面量。
|
||||
- **不协商、不降级**:现在固定 ECDSA P-256 和固定的签名串格式,没有算法协商,保持这一点;升级是硬失败,任何「兼容旧版」的开关都要有明确的下线期限。
|
||||
|
||||
### 9.5 默认值
|
||||
|
||||
Agent 的 `max_total_mbps` 默认是 1200,对 1 核 1 GB 的小机器没有意义,却是被攻破的 Backend 能让 Agent 向某个 Probe 打出去的上限。默认值应该保守,需要的人再调高;`allowed_target_cidrs` 默认为空是对的。
|
||||
|
||||
## 10. 监听隐身:预握手敲门
|
||||
|
||||
目标:Agent 的 TCP 端口对没有完成挑战的人**不写出任何字节**,包括 TLS 的 ServerHello,扫描器拿不到证书和 TLS 指纹(G4)。UDP 端口本来就是静默的。
|
||||
|
||||
```text
|
||||
Probe Agent TCP 7443 / 7444
|
||||
│── TCP 连接 ───────────────────────────────────▶ accept(内核已完成三次握手)
|
||||
│── 敲门包 32 字节:nonce(16) │ tag(16) ─────────▶ 读 32 字节(限时 3 秒)
|
||||
│ tag = HMAC(K_pair, 时间窗 ‖ nonce) 用名册里每个 Probe 的 K_pair 逐个核对
|
||||
│ ├ 合法:继续 TLS 握手 → 现有的签名认证
|
||||
│◀──────────── TLS ServerHello ─────────────────── └ 不合法或超时:什么都不发,直接断开
|
||||
```
|
||||
|
||||
- `K_pair = HKDF(ECDH(Probe 身份私钥, Agent 身份公钥), "moshiro-knock-v1")`:只有名册里的成员算得出;ECDH 和签名共用同一把 P-256 密钥时,用不同的 HKDF 标签做域隔离。
|
||||
- 时间窗 30 秒,接受前后各一个窗(容忍时钟偏差);`nonce` 在窗内去重,防重放;每个来源地址限速,避免陌生人用随机数据消耗 HMAC 计算。
|
||||
- 这是破坏性变更:旧 Probe 不发敲门包,会被拒绝。
|
||||
- **已实现**:`Fleet.Identity.Knock`(构造与核对)和 `KnockGate`(读 32 字节、名册、nonce 去重、按来源限速,默认每来源每 10 秒 300 次);控制端口(`PeerListener`)和数据端口(`TcpAcceptor`)共用同一个关卡,Probe 一侧 `PeerConnector` 和 `TcpLink` 在 TCP 连接建立后、TLS 之前发敲门包。名册是期望状态里的 `peers`(Probe 的对等面公钥)。Probe 需要 Agent 的证书公钥来派生配对密钥,所以 `probe_agents` 下发的是 `public_key`(Agent 在 hello 里上报,面板自己算指纹)。陌生连接得到的是被关闭的连接(Windows 上可能表现为重置),没有任何字节。
|
||||
|
||||
**局限,必须说清楚**:TCP 的 SYN-ACK 由内核发出,用户态进程拦不住,所以端口在扫描器眼里仍是「开着,但不说话」,**不是「没监听」**。要做到真正的没监听,有两条路:
|
||||
|
||||
- **等级二:UDP 单包授权**。TCP 平时不监听;Probe 先发一个签名的 UDP 敲门包(UDP 对陌生包本来就静默),Agent 才临时开 TCP 监听几秒,并只放行敲门者的源地址。代价是每次连接多一个往返、要处理 UDP 丢包重试、多出口的 Probe 源地址可能对不上。
|
||||
- 宿主防火墙规则:需要 root,不适合小鸡节点,不做。
|
||||
|
||||
先做等级一,等级二作为可选。
|
||||
|
||||
## 11. extension:敏感能力的归宿
|
||||
|
||||
Agent 和 Probe 的核心永远是纯采集器。**所有敏感能力**(exec、web ssh 和终端、文件读写、抓包等)都不进核心,做成 extension,**用户可选安装,默认没有**。这样核心保持 NativeAOT、体积小、没有可被滥用的能力,需要这些功能的人装上,不需要的人什么都不用管,便利性不丢。
|
||||
|
||||
```text
|
||||
节点主机
|
||||
┌ 核心容器(NativeAOT,纯采集)┐ ┌ extension 容器(可选,例如 ssh)┐
|
||||
│ 身份 A:节点自己的密钥 │ │ 身份 E:自己的密钥,独立注册 │
|
||||
│ 方法:moshiro.* │ │ 方法:ext.ssh.* │
|
||||
└───────────────┬──────────────┘ └───────────────┬────────────────┘
|
||||
│ wss(节点面) │ wss(节点面,另一条独立的会话)
|
||||
▼ ▼
|
||||
Backend(核心不知道 extension,extension 不能调用核心)
|
||||
```
|
||||
|
||||
- **独立进程,不是插件**:extension 是单独的二进制和镜像,和核心之间**没有进程内接口**,核心不加载任何外部代码、不用反射和动态加载,所以核心的 NativeAOT 和「不能 exec」都不受影响。extension 自己用什么技术实现都行(可以是 NativeAOT,也可以不是)。
|
||||
- **授权只认节点的属主**:每次调用的能力令牌必须由这个节点绑定的属主 passkey 签名,账户角色(owner、admin)不赋予权力;管理员只能删除节点或卸载 extension,读不到、用不了;交互式会话端到端加密。属主的设计见 [access.md](access.md) 第 8 节。
|
||||
- **独立身份**:extension 有自己的密钥、自己向 Backend 注册,在名册里是单独的成员(角色 `extension`,记录它依附的节点 ID)。核心被攻破碰不到 extension 的密钥,反过来也一样。
|
||||
- **独立发布**:沿用签名更新的流程,`component` 不同(例如 `ext-ssh`),独立的镜像、独立的清单;装不装、升不升级都不影响核心的更新。
|
||||
- **独立命名空间**:extension 的方法只能是 `ext.<名字>.*`,同样进方向声明表;核心的声明表里没有 `ext.*`,核心收到一律拒绝。extension 只响应自己命名空间的方法。
|
||||
- **安装像装一个选项**:部署脚本加一个选项(例如 `--extension ssh`)拉起对应容器并写好配置;管理端在节点详情里出现对应入口。卸载就是停掉那个容器。
|
||||
- **本地显式启用**:extension 的本地配置里有 `enabled`、允许的操作员凭证、允许的命令集或目标范围(例如允许登录的用户、允许访问的路径),默认关闭,没有配置就什么都不做。
|
||||
- **每次调用都要授权**:操作员签名的一次性能力令牌,`{节点 ID, extension, 命令哈希, not_before, not_after, nonce}`,有效期以秒计;extension 用本地固定的操作员凭证验签。被攻破的 Backend 不能启用 extension,也伪造不了一次调用。
|
||||
- **交互式会话的内容对 Backend 不透明**(例如 web ssh):浏览器和 extension 在能力令牌携带的公钥基础上协商一次性会话密钥,Backend 只转发密文,不应该能读到终端里的内容。做法见 [access.md](access.md) 第 8.2 节:属主 passkey 授权、临时 ECDH 加 AEAD,静态数据用属主的 `enc_pubkey` 封装。
|
||||
- **不做的**:核心里的任何 exec、文件写、加载 extension;用 extension 绕过核心的本地硬上限(extension 有它自己的上限,核心的不放宽)。
|
||||
|
||||
## 12. 实施顺序
|
||||
|
||||
```text
|
||||
零 协议方向声明表 + 收发校验 + 对账测试 G7,第 8 节;同时把 NodeConnection 按职责拆开(第 13.2 节) 已完成,不破坏
|
||||
一 Probe 自己生成密钥 + 加入令牌注册 + 密钥按用途拆开 G2、G11 已完成,破坏性:旧 probe.json 失效
|
||||
二 预握手敲门(等级一) G4;先基于现有的对端公钥列表,名册出来后换成名册 已完成,破坏性:旧 Probe 被拒绝
|
||||
三 访问控制 + 入网信任链 G1 的大部分、G3、G6;拆成 5 步,见 [access.md](access.md) 第 12 节:能力模型与 API key → 账户与 passkey → 操作员集合 → 入网链 → 吊销 破坏性:节点要固定创世公钥并重新注册;旧 admin 令牌不能再生成加入代码
|
||||
四 测量数据的可信 + 入库配额和输入校验 + CSP G5、G8、G9、G10、T5(第 9.1、9.2 节) 破坏性:块格式加签名
|
||||
五 Backend 应用层身份固定 + 下行消息签名 G1 的剩余部分(链路劫持) 破坏性
|
||||
六 extension 框架 独立身份和名册条目、`ext.*` 命名空间、能力令牌、部署脚本的 --extension 选项(先不做具体的 ssh)
|
||||
可选 等级二:UDP 单包授权;UDP 会话迁移的路径验证
|
||||
```
|
||||
|
||||
阶段一和二是敲门的前提(双方身份密钥都由节点自己生成)。项目允许破坏性变更,当前只有三台节点,重新注册的成本很小。
|
||||
|
||||
## 13. 密码学原语与代码落点
|
||||
|
||||
### 13.1 原语现状,以及为什么不用 BLAKE3
|
||||
|
||||
| 用途 | 现状 |
|
||||
| --- | --- |
|
||||
| 身份签名 | ECDSA P-256 加 SHA-256(BCL) |
|
||||
| 数据面逐包认证 | HMAC-SHA256 截断到 16 字节(`UdpPacket`、`TcpWire`) |
|
||||
| 会话密钥派生 | HKDF-SHA256(`SessionKeys`) |
|
||||
| 令牌哈希、SPKI 指纹 | SHA-256 |
|
||||
| 原始块完整性 | CRC32C,不是密码学校验 |
|
||||
|
||||
**BLAKE3 的评估(已定:不采用)**:
|
||||
|
||||
- 适合的位置:数据面的逐包 MAC(原生的 keyed 模式,一个原语、32 字节密钥)、KDF(`derive_key` 自带用途标签,比手拼 HKDF 的 `info` 更不容易出错)、给块做摘要再签名(第 9.1 节需要一个块摘要)。
|
||||
- 不能替换的位置:ECDSA P-256 本身用 SHA-256,令牌哈希和 SPKI 指纹也继续用 SHA-256。所以 BLAKE3 是**多一个**哈希族,不是换掉一个,两套原语的表面都要维护。
|
||||
- 代价:BCL 里没有;Probe 和 Agent 不引用 NuGet 包,只能在仓库里自己实现,几百行的便携实现加官方测试向量,这是要长期维护的密码学代码。托管实现没有 SHA-NI 加速,而 .NET 的 SHA-256 走系统 OpenSSL、有硬件加速,所以在 x86 上未必更快;收益主要在没有 SHA 指令的 ARM、MIPS 路由器上。
|
||||
- 结论:**不采用,原语保持现状**(签名 ECDSA P-256 加 SHA-256,逐包 MAC 用 HMAC-SHA256,KDF 用 HKDF-SHA256):不增加一套要长期维护的自实现密码学代码,也不多一个哈希族。块摘要(第 9.1 节)用 SHA-256。若以后在目标硬件上实测发现逐包 MAC 是瓶颈,再单独评估,届时只替换 `Fleet.Identity` 里 `Mac` 和 `Kdf` 两个静态函数,调用方不动。
|
||||
|
||||
### 13.2 代码落点与分层原则
|
||||
|
||||
目标是**每个机制只有一个落点,层数尽量少,方便维护**:
|
||||
|
||||
1. **一个机制,一个落点,纯函数优先**:输入字节、输出判定,不引入新的接口、适配层、端口或注册。节点侧继续不用 DI。
|
||||
2. **声明和校验放在同一个项目**:`fleet.*` 的声明表在 `Fleet.Rpc`,`moshiro.*` 的在 `MoShiro.Protocol`;校验统一在 `RpcRouter`,节点和服务端共用,这是唯一的卡口。
|
||||
3. **新代码进现有项目,不新增项目**:Fleet 侧进 `Identity`、`Rpc`、`Link`;Backend 侧的入库检查就放在 `IngestService`,不为安全另建一层。
|
||||
4. **先收缩再加**:`NodeConnection`(703 行)里混着认证、收包分发、状态推送、在线状态和帧编解码,先按职责拆成同一项目里的几个文件,再往里加方向校验;否则每加一个安全机制它就更难读。**已拆**:`NodeConnection.cs`(生命周期)、`.Auth`、`.Receive`、`.Send`、`.State`、`.Wire` 六个 partial 文件,原来的 729 行主文件现在 143 行。
|
||||
|
||||
| 机制 | 落点 | 调用点 |
|
||||
| --- | --- | --- |
|
||||
| 方向和角色声明、收发校验 | 声明:`Fleet.Rpc`、`MoShiro.Protocol`;校验:`RpcRouter` | 节点和服务端各一处创建路由器的地方 |
|
||||
| 对账测试 | `ArchitectureTests`(已有) | 比对声明表和实际注册的处理器 |
|
||||
| 敲门的构造与验证 | `Fleet.Identity` 里一个静态类 | 服务端两处:`Fleet.Link.PeerListener`(7443)、`Measurement.TcpAcceptor`(7444);客户端两处:`PeerConnector`、`TcpLink` |
|
||||
| 操作员凭证验签、名册 | `Fleet.Identity`(三种凭证各一个验签函数) | `Fleet.Link` 的 `StateHandler` 采用名册,用 `ConfigStore` 持久化版本 |
|
||||
| 块和列块的签名与验签 | `MoShiro.Protocol/Raw`(格式和签名都在这里) | 签名:Probe 的列块、节点上传的原始块;验签:`IngestService` |
|
||||
| 归属校验 | `IngestService` | 入库的唯一入口 |
|
||||
| 合理性检查 | `Backend.Analysis` 的 `EchoOps` | 计算指标的入口 |
|
||||
| 密钥按用途拆开、`Mac`、`Kdf` | `Fleet.Identity` | 原有的使用方 |
|
||||
|
||||
## 14. 还没定的事
|
||||
|
||||
访问控制与入网信任链的细节、待确认的默认值见 [access.md](access.md) 第 17 节。这里只列其余的:
|
||||
|
||||
1. 敲门先做等级一,等级二先记 TODO(已按此实现等级一)。
|
||||
2. 严格程度:节点没有固定操作员公钥就拒绝一切 Backend 给的对端(默认),弱信任模式是安装时的显式选择(见 access.md 第 8 节)。
|
||||
3. 节点面和外部面是否拆成两个监听端口(8.1 的目标):拆的话部署文档、Caddy 和隧道的配置都要跟着改。
|
||||
4. extension 的交互式会话**已定**:端到端加密,Backend 只转发密文(见 access.md 第 8.2 和 8.3 节);剩下的是静态数据私钥来源和 AEAD 的选择。
|
||||
5. extension 的第一个具体实现做不做 web ssh,放在哪个阶段。
|
||||
6. 块摘要固定 SHA-256,不预留算法字段(不协商、不降级,见第 9.4 节)。
|
||||
7. Agent 的 `max_total_mbps` 默认值调到多少(第 9.5 节),比如 100。
|
||||
8. 块的签名放进格式的哪个位置、是否影响现有 `MSRC` 容器版本(第 9.1 节)。
|
||||
@@ -0,0 +1,203 @@
|
||||
# 访问控制与入网信任链
|
||||
|
||||
这份文档讲**谁能做什么**(账户、能力、API key)和**谁能让节点入网**(passkey 到节点的信任根)。它是 [security.md](security.md) 的展开:security.md 讲缺口和威胁,这里讲现在怎么做。以已实现的为主,没做的在最后一节列出。
|
||||
|
||||
一句话:登录和授权分开。登录证明「你是谁」,授权只看能力;改变信任(操作员集合)只认 passkey 的签名,并且这个签名由**节点自己验证**,Backend 只搬运,伪造不了。
|
||||
|
||||
## 1. 总览
|
||||
|
||||
```text
|
||||
两个根,各管一摊
|
||||
根 A:CI(代码) 发布公钥写在安装脚本、宿主机更新器的本机配置和 Backend 里
|
||||
└─ CI 签名的发布清单 → 镜像 digest → 节点跑的二进制;安装脚本的 SHA-256 也记在清单里,并公布在发布页
|
||||
假设:CI(Woodpecker、Gitea、镜像仓库、发布密钥)可信;CI 被攻破等于能给所有节点签恶意更新
|
||||
根 B:你的 passkey(运维)
|
||||
└─ 操作员集合(passkey 签,版本递增;创世指纹在安装时固定到节点)
|
||||
├─ 操作员的 passkey 公钥
|
||||
├─ 服务器密钥:Backend 的身份密钥(节点据此认服务器,链路用应用层加密)
|
||||
└─ 域名(rp、origins)
|
||||
|
||||
添加节点:创建加入码不需要 passkey、不需要触碰,一个码可以装任意多台。命令里只有公开的值:
|
||||
面板地址、角色、一次性码、创世指纹、属主(passkey 凭证 id)、安装脚本的哈希(命令先核对哈希再执行)
|
||||
节点注册:向面板取操作员集合链,从命令里的创世指纹验起,属主必须是集合里的凭证;任何一步不对就不注册、不交出密钥;
|
||||
通过后写 trust.json(创世指纹、链、属主),之后只认从这个创世出发的新版本
|
||||
节点和 Backend 之间:集合里登记了服务器密钥之后,节点的认证挑战要求服务器用它签名;两边各出一把临时 ECDH 密钥,
|
||||
认证签名覆盖双方的临时公钥,之后控制链路每一帧都是 AES-256-GCM(每个方向一把密钥、严格递增的计数器)。
|
||||
所以 TLS 在 Cloudflare 或被冒充的证书那里终止,中间人没有服务器密钥,既冒充不了服务器,也读不了、改不了这条链路
|
||||
密码登录和 passkey 登录的会话拿同样的能力,授权只看能力;passkey 只用在节点自己会验证的事上(现在是操作员集合的签名,以后是 extension 会话和加密数据)。
|
||||
Backend 自己拦自己没有意义,所以没有提权窗口
|
||||
```
|
||||
|
||||
没有采用「成员证明」(每个节点一条 Grant、Ticket、Binding 的入网链):被攻破的 Backend 往名册里塞节点,拿不到比 Backend 已经有的更多的东西(监测数据本来就是明文给 Backend 的;敏感的 extension 靠节点属主,不靠名册),成本却是每个节点一次触碰和一套证明格式。操作员集合和创世固定已经在,以后要叠加不用重装节点。
|
||||
|
||||
**发布密钥可以换**:见 [deployment.md](../ops/deployment.md) 的「换发布密钥」;回滚由清单的 `seq` 防住(更新器只收比已安装更大的)。面板信任哪些发布密钥(官方的,加上可选的自有公钥)在管理端「系统 → 信任 → 可信资产来源」。
|
||||
|
||||
## 2. 名词
|
||||
|
||||
| 名词 | 含义 |
|
||||
| --- | --- |
|
||||
| 账户 | 一个人,有用户名和密码,可以注册 passkey |
|
||||
| 能力(capability) | 一项具体权限,如 `fleet.write`。端点声明自己需要哪项能力 |
|
||||
| 会话 | 登录得到的状态,拿到全部能力(见第 3 节) |
|
||||
| 操作员集合 | 节点固定的、带版本的 passkey 公钥列表,加上服务器密钥和域名,由操作员 passkey 签名 |
|
||||
| API key | 一串随机令牌,只带创建时勾选的能力;明文只在创建时显示一次 |
|
||||
| 属主(controllers) | 一个节点绑定的一组 passkey:只有它们能授权这个节点上的 extension(web ssh 等)。管理员没有这份权力,见第 9 节 |
|
||||
|
||||
## 3. 能力
|
||||
|
||||
| 组 | 能力 | 说明 | 信任类 |
|
||||
| --- | --- | --- | --- |
|
||||
| 展示 | `public.read` | 看隐藏线路;站点私有时看展示页 | |
|
||||
| 节点 | `fleet.read` | 看 Agent、Probe、线路、加入代码 | |
|
||||
| | `fleet.write` | 改名、标签、可见性、手填地址、线路开关、断开会话 | |
|
||||
| | `fleet.enroll` | 生成加入代码 | |
|
||||
| | `fleet.revoke` | 吊销或删除节点 | 是 |
|
||||
| 任务 | `tasks.read` · `tasks.write` | 任务模板、覆盖、发起测试 | |
|
||||
| 数据 | `data.read` · `data.write` | 原始块;清理 | |
|
||||
| 设置 | `settings.read` · `settings.write` | 站点设置、保留策略、重启计划 | |
|
||||
| 更新 | `update.read` · `update.confirm` | 看 Backend 更新和发布清单、确认更新、管理可信的发布密钥 | |
|
||||
| 令牌 | `keys.read` · `keys.manage` | 列出、创建、撤销 API key | |
|
||||
| 账户 | `accounts.read` · `accounts.manage` | 账户 | 管理是 |
|
||||
| 运维 | `events.read` · `audit.read` · `ops.metrics` | 事件;审计日志;`/metrics` | |
|
||||
| 集成(预留) | `data.export` · `webhooks.read` · `webhooks.manage` | 批量拉数据、外部回调订阅,见第 6.4 节 | |
|
||||
|
||||
- **一个能力只在一个地方声明**:`Capability` 常量(`MoShiro.Backend.Application`),端点用 `.Requires(Capability.X)`;`Principal.Has(capability)` 是唯一的判断入口,会话和 API key 都走它。写蕴含读(`fleet.write` 带 `fleet.read`)。
|
||||
- **对账测试**:所有 `/api/admin` 端点必须声明能力,没声明测试就失败;OpenAPI 的每个操作带 `x-moshiro-capability`。
|
||||
- **信任类**是界面上特别标出的(吊销、账户管理),授权和其他能力一样靠 API key 或会话。真正由 passkey 签名保护的是操作员集合,那是节点自己验证的。
|
||||
- **不能越权**:创建 API key 时勾选的能力必须是创建者当前拥有能力的子集。
|
||||
- **Backend 更新确认**:更新清单由 CI 的发布密钥签名,`seq` 防回滚,真正的门是发布密钥,不是确认这一步。
|
||||
|
||||
## 4. 登录与会话
|
||||
|
||||
| 登录方式 | 说明 |
|
||||
| --- | --- |
|
||||
| 密码 | PBKDF2;认领向导创建第一个管理员时设置 |
|
||||
| passkey | WebAuthn,只支持 ES256,attestation 一律当 `none`;可以登录,也能给操作员集合签名(节点验证) |
|
||||
|
||||
- 两种方式的会话拿**同样的能力**。passkey 不是 Backend 用来拦操作的一道门,只用在节点自己会验证的事上,见第 5 节。
|
||||
- **认领向导(没有默认账户)**:面板里还没有任何用户时,打开 `/admin/` 进向导——用后端日志里的认领码(或 `MoShiro__ClaimCode`)创建第一个管理员(用户名和密码自己填),注册第一个 passkey,再用它给操作员集合签名建立信任根。谁先打开页面谁就能当管理员,所以要认领码;创建完码作废、入口关闭(`POST /api/admin/auth/setup` 之后返回 409)。后两步是引导不是门槛,可以稍后补;不注册 passkey、不建立信任根,就添加不了 Agent 和 Probe,只能进面板做基础管理,这是架构如此。之后新增的 passkey 不会自动成为操作员:要由现有操作员在信任页看清「新增了哪些」再签新版本,节点才认。
|
||||
- 建议每个账户至少注册两个 passkey(手机加硬件密钥)。
|
||||
|
||||
### 4.1 passkey 登录的实现
|
||||
|
||||
- 流程:登录页「使用 Passkey 登录」→ `POST /api/admin/auth/passkey/options` 取挑战 → 浏览器 `navigator.credentials.get`(可发现凭证,不用输用户名)→ `POST /api/admin/auth/passkey/login` 提交断言 → 建立会话(Cookie,或跨域前端用 `return_session`)。注册:已登录用户在「系统 → 用户 → 我的 Passkey」添加,`POST /api/admin/auth/passkeys/options` 加 `POST /api/admin/auth/passkeys`;每个用户最多 10 个。
|
||||
- 校验:挑战在内存里,一次有效、5 分钟过期,并且区分注册和登录、绑定用户;`rpIdHash`、UP 和 UV 标志、`clientDataJSON` 的 type、challenge、origin、ES256 签名;签名计数器只增不减(两边都是 0 的同步型 passkey 不检查)。登录失败一律 401,不区分原因,和密码登录共用限流。
|
||||
- 验证断言的纯函数是 `Fleet.Identity.WebAuthnAssertion`(只用 BCL,节点验证操作员集合也用它);注册里的 CBOR 和 attestation 解析在 Backend(`WebAuthnRegistration`),节点不带 CBOR。
|
||||
- 依赖方(RP):`MoShiro__WebAuthnRpId`(缺省取 `PublicUrl` 的主机,没有就取请求的主机)和 `MoShiro__WebAuthnOrigins`(缺省取 `PublicUrl` 的来源)。**passkey 绑定在这个域名上,改了会让已注册的 passkey 失效。**只有和后端同站的页面能用 passkey 登录,管理端在「添加其他后端」(跨域)时不显示这个入口。
|
||||
- 会话记录登录方式(`password` 或 `passkey`),只用于审计和显示;API key 不是用户,不能注册 passkey。
|
||||
- 测试:`SoftwarePasskey` 在测试里模拟认证器走完整的注册和登录,覆盖错误的挑战、来源、RP、签名、缺少 UV、挑战重放、计数器回退、只能管理自己的 passkey。
|
||||
|
||||
## 5. passkey 用在哪里(没有提权窗口)
|
||||
|
||||
原则:**passkey 不是 Backend 用来拦操作的一道门**(Backend 被攻破时,它自己做的拦截也会被一起绕过),而是节点或密钥持有者需要它才能验证、解密的东西。
|
||||
|
||||
| 事 | 谁验证 | 要 passkey |
|
||||
| --- | --- | --- |
|
||||
| 建立、更新操作员集合(信任根) | **节点**验链 | 是(现在唯一做了的) |
|
||||
| extension 会话(web ssh 等) | **节点**验 passkey 签名的临时密钥 | 是(还没做) |
|
||||
| 读加密存储的数据 | 私钥持有者 | 是(还没做) |
|
||||
| 吊销或删除节点、账号、API key、加入码、设置 | 只有 Backend | 否,只看能力 |
|
||||
|
||||
- 这些「只有 Backend」的事靠能力限权:API key 只带需要的能力。密码泄露的后果是这些能力被滥用,改不了信任根,也用不了以后的 extension。
|
||||
- 唯一的风险是往账号里塞一把 passkey 再等人签名:信任页的提案会列出**新增和移除的凭证**,签名之前要看一眼是不是自己加的;签了才算授权。
|
||||
|
||||
## 6. API key
|
||||
|
||||
- 创建时勾选能力,只能是创建者当前拥有能力的子集;可设有效期。明文只在创建时显示一次,列表显示能力数、最后使用时间、过期时间。预设:只读、运维、接入(`enroll`)、全部。
|
||||
- 创建界面的能力用分组复选框:全选(带半选状态)、组内全选、反选、搜索;预设按钮一键套用。
|
||||
- 列表(API key、加入代码、节点、审计日志)都支持多选和全选做批量操作。
|
||||
|
||||
### 6.1 `enroll` key:自动化添加节点
|
||||
|
||||
它就是一把带 `fleet.enroll` 的普通 API key,用 `POST /api/admin/join-tokens` 创建加入码,响应里直接给出安装命令(带创世指纹、属主和脚本哈希,没有任何私钥)。创建码不是信任动作,所以不需要 passkey;CI 和脚本照常工作。信任在两处:命令里的创世指纹和属主由面板生成,节点注册时固定;以后改属主和集合要操作员 passkey 签名。
|
||||
|
||||
### 6.2 CI 登记发布清单不需要令牌
|
||||
|
||||
CI 把签名清单 POST 给 `POST /api/release/offer`,面板用发布公钥验签,没有令牌,所以清库、重装、还没有 passkey 时照常工作,也没有长期令牌可偷。旧清单重放由 `seq` 递增挡住。
|
||||
|
||||
### 6.3 预留:外部 webhook 回调与数据拉取
|
||||
|
||||
后面要让外部系统能订阅事件、触发回调、拉数据。接口和权限先留(`data.export`、`webhooks.*` 标预留),实现放后面;全部发生在 Backend 里,**节点零开销**。订阅只能订阅创建者自己读得到的事件;投递是带 HMAC 签名的 `POST`,至少一次、指数退避;目标 URL 要做 SSRF 限制(只放行公网地址,不跟随重定向到内网,有超时和大小上限)。这些都属于 Backend 的集成配置,不改变节点信任,但每次增删订阅都进审计。
|
||||
|
||||
## 7. 归属与审计
|
||||
|
||||
- 加入代码记 `created_by`,节点记 `added_by`;管理页的 Agent、Probe 详情显示「由 xxx 于 xxx 添加」。
|
||||
- 审计日志(`GET /api/admin/audit`):所有非 GET 的管理操作和登录自动记录(时间、账户、登录方式、动作、对象、详情)。写不进去只记日志,不让已完成的操作报错。保留 365 天,只追加。
|
||||
|
||||
## 8. 入网信任链的实现要点
|
||||
|
||||
- 语句是 `key=value` 文本,第一行是带版本的标题,签名覆盖整段文本;WebAuthn 的挑战是语句的 SHA-256。库在 `Fleet.Identity` 的 `OperatorTrust`(操作员集合的编码、解析、链验证)和 `SecureChannel`(服务器证明和链路加密),都是只用 BCL 的纯函数,节点和 Backend 共用。
|
||||
- **创世**:第一次建立信任根时,把账号下所有 passkey 和这个面板的服务器密钥登记成一个操作员集合,passkey 对它签名;创世语句的 SHA-256 就是命令里的 `MOSHIRO_GENESIS`。之后加删 passkey 或换服务器密钥要签新版本(上一版里的凭证签),页面会提示;签名更新不会让节点重装。
|
||||
- **操作员集合版本**随期望状态(`trust_chain`)下发,节点从固定的创世验起,版本比现有的新才采用并保存。
|
||||
- **节点注册**:`fleet-enroll --genesis … --controllers …`:先 `POST /api/agent/v1/enroll/trust` 取链,验过(创世指纹、签名、属主在集合里)才把密钥交出去;之后写 `trust.json`。没有命令里的指纹(`--genesis`)就不固定,节点按旧方式工作。
|
||||
- **服务器身份和安全链路**:`ChallengeParams` 带服务器的临时公钥和签名(`server_eph`、`server_sig`),`AuthParams` 带节点的临时公钥(`node_eph`),节点签名的用途是 `auth2`,覆盖双方的临时公钥;通过后两边用 HKDF 派生每个方向一把 AES-256-GCM 密钥,每帧 `计数器(8) ‖ 密文 ‖ 标签(16)`,计数器严格递增,明文帧、改动、重放、乱序一律断开。集合里没有服务器密钥的节点(旧节点)保持明文,不会被降级成放行。
|
||||
- **服务器密钥**存在 Backend 的设置里(`server.identity_key`):它防的是面板被冒充,不是面板被攻破;面板被攻破时它也泄露,那时要由 passkey 签一个换掉它的新集合版本。集合里没有这个面板的服务器密钥的版本,Backend 不收(否则固定了它的节点就认不出这个面板了)。
|
||||
- **弱点**:创世指纹放在安装命令里,命令本身要从可信渠道拿到(管理端复制)。缓解见第 10 节(静态签名脚本、按 digest 拉镜像),并在信任页展示指纹供核对。
|
||||
|
||||
## 9. 节点属主与 extension 授权
|
||||
|
||||
extension(web ssh、文件、抓包,见 [security.md](security.md))**现在都不实现**,Agent 和 Probe 一样。这类能力连管理员都不信任:管理员能删,不能读,也不能用。所以入网时就给每个节点固定「属主」,之后不用重装节点。
|
||||
|
||||
**已经实现的部分**
|
||||
|
||||
- 属主 `controllers` 是一组 passkey 凭证 id,必须是操作员集合里的凭证。由安装命令带来,节点注册时写进 `trust.json`,Backend 之后改不了。
|
||||
- **每个 Agent 和 Probe 都必须有属主,至少一把——而且这是节点自己的要求,不是 Backend 的要求**:节点启动时检查本地固定的属主,没有(或者属主的 passkey 已经不在操作员集合里)就不运行(共用 `Fleet.Hosting` 里的 `OwnerGuard`;配置 `fleet.require_owner`,`MOSHIRO_ALLOW_NO_OWNER=1` 放行)。面板被攻破也改不了。
|
||||
- **添加节点时选属主**:`POST /api/admin/join-tokens` 的 `controllers: [credential_id…]`,不给就是操作员集合里的全部凭证;给了就必须非空且都在集合里。还没有信任根或一把属主都没有时,面板**提前拒绝**创建加入码(400,字段 `controllers`),这只是尽早失败,真正的强制在节点;`MoShiro__RequireOwner=false` 可以关掉这个提前检查(只给没有信任根的老部署)。界面里是必选的「属主(passkey)」多选列表,只是选,不用触碰。
|
||||
- **安装端也强制**:`install.sh`、`install.ps1`、容器入口缺了 `MOSHIRO_GENESIS` 或 `MOSHIRO_CONTROLLERS` 就不装;`fleet-enroll --genesis` 不带 `--controllers` 也拒绝。
|
||||
- **逆推出绑定关系**:节点握手时把本地固定的属主(passkey 凭证 id)报给面板(hello 的 `owners`),面板顺着凭证 id 反查出它绑定在**哪个账户的哪把 passkey** 上,显示在「系统 → 版本」的「属主」列(对不上任何已注册 passkey 的显示「未知凭证」)。上报只是自述,真正的授权由节点自己校验 passkey 签名。
|
||||
|
||||
**预留的设计(还没做)**
|
||||
|
||||
- **谁来强制:节点本地,不是 Backend。** web ssh 这类会话要不要加密、要不要验证,由 Agent 和 Probe 自己要求。让 Backend 决定,被攻破的 Backend 一句「这次不用加密」就绕过去了。节点本地强制:没有操作员的现场证明不启动会话;会话密钥只和证明里的那把密钥协商,不和 Backend 转来的公钥协商;加密字段缺失就拒绝,不降级;能开哪些 extension、哪些 passkey 能授权,只看本节点属主。
|
||||
- **证明的方向是反的**:节点先出题(nonce 加它的临时公钥,用节点身份密钥签名),属主的浏览器把 nonce 和自己的临时公钥放进 passkey 断言的挑战里;节点验断言来自本节点属主、要求用户验证、nonce 是自己刚出的且没用过。Backend 仍然管谁有资格发起请求、审计、限流、转发密文。
|
||||
- **交互式会话**(终端、ssh)用现场签名的临时 ECDH 密钥协商,passkey 只签名授权,不参与解密,所以不支持 PRF 的 passkey 也能用,还有前向保密;之后双向是带序号的 AEAD。**静态数据**(抓包文件、会话录像)需要事后解密,才用属主记录里的接收者公钥加密(来自 WebAuthn PRF 或浏览器里不可导出的密钥)。
|
||||
- **删除和使用分开**:admin 能删节点、卸载 extension,读不到也用不了。改属主要现有属主的 passkey 签名(`ControllerUpdate`),账户被删、角色被改都改不了某个节点的属主。属主是 passkey 凭证,不是账户;一个人丢了全部属主 passkey,这个节点上的 extension 就没人能用了,所以添加时建议绑两把以上。
|
||||
- **数据边界**:监测数据(延迟、丢包、吞吐、事件、主机资源汇总)是明文,Backend 要做分析和展示;extension 会话和原始 payload 端到端加密,Backend 只存转发密文。读取权不是能力,是私钥。
|
||||
|
||||
## 10. 安装的供应链
|
||||
|
||||
一行安装要执行 Backend 吐出的脚本、拉仓库里的镜像。Backend 或仓库被攻破,就能在新节点上执行任意命令。收束办法,**优先级最高的是第一跳的哈希核对**:
|
||||
|
||||
**① 脚本是静态产物,哈希固定**
|
||||
|
||||
- 不在服务端做字符串替换。CI 构建时算出 `install.sh`、`install.ps1` 的 SHA-256,写进发布清单(发布密钥签名),同时公布在 Gitea 发布页——**这条渠道独立于 Backend**,这是哈希核对有意义的前提:哈希若和脚本从同一处来,攻击者两个一起换就行。
|
||||
- 参数(代码、面板地址)通过命令行环境变量传入,代码因此不出现在 URL 和反向代理日志里。
|
||||
- 管理端给的命令把「下载、核对、执行」合成一行,哈希核对不通过就不执行:
|
||||
|
||||
```text
|
||||
f=$(mktemp) && curl -fsSL https://panel/install -o "$f" && echo "<sha256> $f" | sha256sum -c - && MOSHIRO_SERVER=… MOSHIRO_ROLE=… MOSHIRO_CODE=… sh "$f"
|
||||
```
|
||||
|
||||
macOS 用 `shasum -a 256 -c`,Windows 用 `Get-FileHash` 比较,不通过就 `throw`。命令里的 `<sha256>` 由 Backend 填入,信任页同时标出它与 CI 签名的发布清单是否一致;严格的人对照 Gitea 发布页一次即可(脚本是静态的,哈希在一个版本内不变)。
|
||||
|
||||
**② 镜像按 digest 拉取**
|
||||
|
||||
- 脚本向面板取已签名的清单(和更新用的是同一份,发布密钥签名、`seq` 防回滚),用**脚本里内置的发布公钥**验签,再按清单里的 digest 拉镜像(`repo@sha256:…`)。Backend 改不了 digest,最多让节点装到一个旧的合法版本。
|
||||
- 下载压缩包安装的人,用 Release 里签名的 `SHA256SUMS` 核对(见 [deployment.md](../ops/deployment.md))。
|
||||
|
||||
**③ 其余**
|
||||
|
||||
- 角色(Agent 还是 Probe)、镜像仓库地址仍由代码决定,但镜像身份由签名清单决定。
|
||||
- macOS 的 Developer ID 签名和公证、Windows 的 Authenticode 需要证书,目前没有做。
|
||||
|
||||
## 11. 运行中的二进制不做校验
|
||||
|
||||
曾经做过「节点二进制绑定」:节点握手时自报正在运行的二进制的 SHA-256,Backend 在第一次握手时绑定,之后对不上就标红或拒绝。**已经移除。**
|
||||
|
||||
原因:哈希和清单都是节点自己报的。一个被换进来、故意撒谎的二进制可以照抄原来的哈希和清单,Backend 分辨不出——上报者就是被怀疑的对象。这一层只能抓到「文件被换了、但程序本身没撒谎」的情形,却让人误以为有保障,还要求运维宿主机更新器、状态卷和每个节点的清单。得不偿失。
|
||||
|
||||
「这个资产是我们的」靠**安装时的来源证明**,那里的哈希来自另一个信道,伪造不了:
|
||||
|
||||
- 安装脚本(SHA-256 记在签名的清单里,并公布在发布页),按清单里的 digest 拉镜像(`repo@sha256:…`);
|
||||
- 下载压缩包的人用签名的 `SHA256SUMS` 核对(验签用 `packaging/update/release-key.pub.pem`);
|
||||
- 节点的更新由宿主机更新器用本机公钥验签,只认本机配置的镜像仓库。
|
||||
|
||||
要让运行中的二进制真的无法伪造,必须有容器之外的东西背书:宿主机更新器持有一把节点进程读不到的密钥,对「当前运行的镜像 digest + Backend 的挑战」签名,由节点转发;容器里的进程被换掉也伪造不了,除非攻破宿主机。这一步没有做(一键容器安装的节点也没有宿主机更新器)。
|
||||
|
||||
## 12. 还没有的与局限
|
||||
|
||||
- **还没有**:吊销表;任务和配置内容的操作员签名;节点向别人证明自己运行的就是官方二进制(见第 11 节);extension 本身、属主的 `ControllerUpdate`、接收者的实际加密(属主只是被固定在节点本地,没有行为);API key 的 IP 白名单(本部署经 Cloudflare、frp 和 Caddy,后端看到的来源地址是代理,需要先有「可信代理」配置才做得对);OIDC 登录。
|
||||
- WebAuthn 只验 ES256 和 attestation = `none`,不验设备证明。
|
||||
- 某个节点的属主 passkey 全部丢失,这个节点上的 extension 就没人能授权,需要重装节点并换属主。
|
||||
- 删除一把操作员 passkey 只是让它不能再签新版本;服务器密钥换新要靠签一个新集合版本,忘了签会让固定了集合的节点认不出面板(fail closed)。
|
||||
- `enroll` key 是持有者凭证,谁拿到令牌,谁就能在它的有效期内创建加入码,必须按密码对待。
|
||||
@@ -1,6 +1,6 @@
|
||||
# MoShiro 的注册与身份
|
||||
|
||||
通用的注册、身份、认证机制由 Fleet 提供,见 [../backend/fleet/README.md](../backend/fleet/README.md) 的「身份」「注册」两节。本文只描述 **MoShiro 在 Fleet 之上的约定**:Agent 注册后如何自动接入测量、Probe 的身份以及 Agent 列表同步,以及节点本地的硬上限。
|
||||
通用的注册、身份、认证机制由 Fleet 提供,见 [../backend/fleet/README.md](../../backend/fleet/README.md) 的「身份」「注册」两节。本文只描述 **MoShiro 在 Fleet 之上的约定**:Agent 注册后如何自动接入测量、Probe 的身份以及 Agent 列表同步,以及节点本地的硬上限。
|
||||
|
||||
## 1. 身份一览
|
||||
|
||||
@@ -10,24 +10,23 @@
|
||||
| Probe | Fleet 节点身份(连面板)+ 另一把独立的密钥(连 Agent,放在 `tls.pfx`) | `fleet-enroll`,和 Agent 一样用加入令牌(`role=probe`);可以有多个 Probe,每个独立吊销 | 两把公钥 |
|
||||
| 面板用户 | 账号 + 密码(PBKDF2-SHA256) | 面板 | 密码哈希 |
|
||||
| API 令牌 | 随机高熵令牌,带一组能力(见 [access.md](access.md)) | 面板 | SHA-256 |
|
||||
| 发布签名 | ECDSA P-256 发布密钥 | CI(Woodpecker 密钥 `release_signing_key`)或离线环境 | 只在节点宿主机的更新器里配置公钥(容器节点是 `moshiro-update`,见 [deployment.md](deployment.md);纯二进制节点的更新是待办),**面板不保存私钥**;登记清单时面板用公钥验签,节点宿主机的更新器之后再验一次 |
|
||||
| 发布签名 | ECDSA P-256 发布密钥 | CI(Woodpecker 密钥 `release_signing_key`)或离线环境 | 只在节点宿主机的更新器里配置公钥(容器节点是 `moshiro-update`,见 [deployment.md](../ops/deployment.md);纯二进制节点的更新是待办),**面板不保存私钥**;登记清单时面板用公钥验签,节点宿主机的更新器之后再验一次 |
|
||||
|
||||
## 2. Agent 接入流程
|
||||
|
||||
```text
|
||||
管理员 ── 创建加入令牌 {标签, max_uses, 过期时间, auto_approve} ──> 面板(Fleet.Server.Enrollment)
|
||||
<── 一行安装命令(Agent 和 Probe 共用,没有占位符,节点名字默认取主机名):
|
||||
curl -fsSL 'https://panel/install?code=jt_xxx' | bash (macOS 用 zsh)
|
||||
irm 'https://panel/install.ps1?code=jt_xxx' | iex (Windows)
|
||||
管理员 ── 创建加入令牌 {标签, max_uses, 过期时间, auto_approve, controllers} ──> 面板(Fleet.Server.Enrollment)
|
||||
<── 一行安装命令(Agent 和 Probe 共用):先下载静态安装脚本、核对它的 SHA-256,一致才执行;
|
||||
面板地址、角色、一次性码、创世指纹和属主都走环境变量,命令里只有公开的值(见 access.md 第 10 节)
|
||||
|
||||
面板按代码生成脚本(packaging/install/,嵌入 Backend):用 Docker 或 Podman 起容器,镜像入口用代码注册,身份和配置在命名卷里。
|
||||
下面是发布包里的 install.sh(二进制安装,不包含任何远程控制功能;容器节点的做法见 deployment.md):
|
||||
├─ 下载 moshiro-agent、fleet-enroll,并校验 SHA-256
|
||||
├─ 创建系统用户 moshiro(运行 Agent)
|
||||
├─ fleet-enroll --server https://panel --join jt_xxx --name jp-1 --identity-dir /etc/moshiro
|
||||
│ └─ 注册元数据(MoShiro 定义):{ probe_tcp_port, probe_udp_ports, public_host?, kinds[] }
|
||||
├─ 写入 agent.json(本地硬上限)
|
||||
└─ 启用 moshiro-agent.service
|
||||
install.sh / install.ps1(packaging/install/,嵌入 Backend,静态文件,不替换任何值):
|
||||
├─ 向面板取 CI 签名的发布清单,用脚本里内置的发布公钥验签,按清单里的 digest 拉镜像
|
||||
├─ 用 Docker 或 Podman 起容器,身份和配置存在命名卷里;节点名字默认取主机名
|
||||
└─ 容器入口用代码注册:fleet-enroll --server … --join jt_… --genesis … --controllers …
|
||||
├─ 先验操作员集合链(创世指纹、属主在集合里),验不过就不交出密钥
|
||||
├─ 注册元数据(MoShiro 定义):{ probe_tcp_port, probe_udp_ports, public_host?, kinds[] }
|
||||
└─ 写 trust.json(创世指纹、链、属主)和 agent.json(本地硬上限)
|
||||
下载压缩包的二进制安装(packaging/install.sh)做同样的注册,另外创建系统用户 moshiro、启用 moshiro-agent.service,见 deployment.md。
|
||||
|
||||
面板收到 OnEnrolled(MoShiro 挂钩)
|
||||
├─ public_host 未提供时,来源 IP 是公网地址才用;之后 hello 里 Agent 上报本机公网地址(`addresses`),面板自动填 public_host 和 ipv6_host,手填的优先
|
||||
@@ -60,7 +59,7 @@ Probe 启动
|
||||
|
||||
- 每个 Agent 通过自己的期望状态(`TaskSet.peers`)获得 Probe 的公钥,用来验证点对点会话的签名。
|
||||
- **哪些 Probe 连哪些 Agent 由面板决定,始终是 Probe 主动拨出**:设置 `fleet.auto_link`(默认开)打开时,所有未吊销的 Probe 和所有 Agent 两两连;关闭时只连手动选中的。每一对还可以在管理端 Probe 页的「线路」矩阵里手动强制开或关(`PUT /api/admin/links/{probe_id}/{agent_id}`),手动设置优先于自动连接。结果同时用在 Probe 的 Agent 列表、Agent 的 `peers` 和线路目录(关掉的线路从总览里消失,历史数据还在)。
|
||||
- 吊销 Probe:在面板上重新生成身份,旧公钥立即从所有 Agent 的期望状态中移除,旧会话随之断开。
|
||||
- 吊销 Probe:`POST /api/admin/probes/{id}/revoke`,旧公钥立即从所有 Agent 的期望状态中移除,旧会话随之断开;要继续用就重新注册。
|
||||
|
||||
## 4. Agent 本地配置(`/etc/moshiro/agent.json`,面板无法修改)
|
||||
|
||||
@@ -80,7 +79,7 @@ Probe 启动
|
||||
|
||||
- `limits` 是**本地硬上限**:期望任务集超出这些范围时,调和器拒绝执行,并在 `rejected[]` 中给出原因。
|
||||
- `allowed_target_cidrs` 默认为空,即不允许执行任何目标任务;需要时由节点所有者在本机显式开启。
|
||||
- `listen` 默认不包含 443。要用 443 测量,需要在本机把它加入 `udp_ports` 或 `tcp_ports`(端口不能被真实服务占用),见 [measurement.md](measurement.md) 第 8 节。
|
||||
- `listen` 默认不包含 443。要用 443 测量,需要在本机把它加入 `udp_ports` 或 `tcp_ports`(端口不能被真实服务占用),见 [measurement.md](../design/measurement.md) 第 8 节。
|
||||
- `fleet_policy.allow_methods` 在 Fleet 默认值的基础上进一步收窄。
|
||||
|
||||
## 5. 相关 API
|
||||
@@ -94,7 +93,8 @@ Probe 启动
|
||||
| GET/PATCH/DELETE | `/api/admin/probes`、`/probes/{id}` | Probe 的列表、改名、删除 |
|
||||
| POST | `/api/admin/probes/{id}/revoke` | 吊销(换密钥就是吊销之后重新注册,没有轮换) |
|
||||
| POST | `/api/admin/agents/{id}/probes/{probe_id}/disconnect` | 让 Agent 断开某个 Probe |
|
||||
| GET | `/install?code=…`、`/install.ps1?code=…` | 一行安装脚本(凭一次性代码,Agent 和 Probe 共用) |
|
||||
| GET | `/install`、`/install.ps1` | 静态安装脚本(Agent 和 Probe 共用,不带参数,哈希在一个版本内固定) |
|
||||
| GET | `/install/manifest?code=…`、`/install/manifest.sig?code=…` | 凭一次性代码取该角色已签名的发布清单(原样,一个字节也不改) |
|
||||
|
||||
|
||||
## Agent 的本地配置:主机状态上报
|
||||
@@ -0,0 +1,198 @@
|
||||
# 零信任设计
|
||||
|
||||
> **实现状态**:协议方向声明表和卡口、Probe 自己生成密钥、预握手敲门、操作员集合与创世固定、服务器身份和安全链路、属主固定都已实现;测量数据的产生者签名、入库配额、Probe 拨号地址限制等还没做(第 3 节有逐项状态)。
|
||||
|
||||
Agent、Backend、Probe 之间互相不信任。这份文档回答三个问题:现在谁听谁的、被攻破之后最坏会怎样、怎么用密码学把信任从 Backend 身上拿走。账户、能力、passkey 和入网信任链的具体做法见 [access.md](access.md);纯采集的边界、本地硬上限和签名更新见 [architecture.md](../design/architecture.md) 第 5 节,这里不重复。
|
||||
|
||||
## 1. 范围
|
||||
|
||||
**要防的**
|
||||
|
||||
- Backend(或 Backend 前面的 Cloudflare Tunnel、frp、Caddy 等中间环节)被攻破或被劫持之后,给节点下发恶意内容。
|
||||
- Agent 或 Probe 自己被入侵之后,去影响 Backend 或对端。
|
||||
|
||||
**有意不做的**
|
||||
|
||||
- **进程沙箱**(seccomp、pids 和内存上限、出站白名单)。Agent 和 Probe 的定位是 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 | 某个 Probe(在客户内网里) | 客户机器被入侵 |
|
||||
| T4 | 管理员的会话或令牌 | 令牌泄露 |
|
||||
| T5 | 管理员的浏览器 | XSS,或者节点上报的字符串被渲染 |
|
||||
|
||||
## 3. 缺口与状态
|
||||
|
||||
```text
|
||||
CI 发布私钥(单把)
|
||||
│ 签名清单
|
||||
管理员 ──令牌/会话──▶ ┌──────────┐ ◀── 原始块、事件、主机数字
|
||||
│ Backend │
|
||||
└──┬───┬───┘
|
||||
任务集、对端公钥、 │ │ Agent 名单(host:port,Probe 全部照拨)
|
||||
重启计划、更新清单 ▼ ▼
|
||||
┌──────┐ ┌──────┐
|
||||
│Agent │◀─▶│Probe │ Probe 私钥由节点自己生成
|
||||
└──────┘ └──────┘
|
||||
```
|
||||
|
||||
| # | 缺口 | 状态 |
|
||||
| --- | --- | --- |
|
||||
| G1 | 节点不验证 Backend 的身份,只靠 TLS 加公共 CA | **已解决**:操作员集合里登记服务器密钥,节点要求服务器在认证挑战里签名,链路用应用层加密([access.md](access.md) 第 8 节)。对 Backend 自己被攻破没有帮助 |
|
||||
| G2 | Probe 的私钥由 Backend 生成 | **已解决**:Probe 自己生成密钥,用加入令牌注册,私钥只在节点上(破坏性:旧 `probe.json` 失效) |
|
||||
| G3 | Probe 会拨 Backend 给的任意 `host:port` | 未解决:被攻破的 Backend 能让客户内网里的 Probe 去探测内网;本地 `limits` 只有速率、pps 和 `allowed_target_cidrs` |
|
||||
| G4 | Agent 的 TCP 端口对任何人完成 TLS 握手并出示证书 | **已解决**:预握手敲门,见第 8 节(破坏性:旧 Probe 被拒绝) |
|
||||
| G5 | Backend 入库只限制了单块的大小,没有按节点的速率或配额 | 未解决:恶意节点可以灌满磁盘 |
|
||||
| G6 | 对端公钥、Agent 地址全由 Backend 决定 | 未解决:操作员集合不包含成员名册,被攻破的 Backend 能把恶意 Probe 加进 Agent 的白名单,或把 Probe 指向恶意 Agent |
|
||||
| G7 | 方法的方向和角色没有统一声明 | **已解决**:声明表和卡口,见第 6 节 |
|
||||
| G8 | 测量数据没有产生者签名(块只有 CRC32C) | 未解决:中间环节可以改写测量数据;被攻破的 Agent 可以伪造 Probe 侧的时间戳 |
|
||||
| G9 | 入库只核对了 `agent_id`,不核对块里的 `series`(`line:<probe_id>`) | 未解决:被攻破的 Agent 可以为从没连过它的 Probe 灌入假线路数据 |
|
||||
| G10 | 没有合理性检查(`EchoOps` 不检查回显时间戳的一致性) | 未解决:恶意 Agent 可以谎报 `t2`、`t3` |
|
||||
| G11 | Probe 的一把密钥多处使用 | **已解决**:面向 Backend 和面向对等面的是独立的密钥,用途前缀做域隔离 |
|
||||
|
||||
没有问题的部分:UDP 端口对未认证的包静默丢弃,不回应(`DropInvalid`,有测试);Probe 本身没有 TCP 监听;任务有本地硬上限;更新清单有签名和 `seq` 防回滚。
|
||||
|
||||
## 4. 核心思路:信任锚点从 Backend 移走
|
||||
|
||||
要防 Backend 被攻破,**改变信任的内容必须由 Backend 之外的密钥授权**。Backend 只搬运、计算和存储,不创造信任:
|
||||
|
||||
```text
|
||||
① 发布密钥(CI) ──▶ 「跑什么代码」 清单签名、seq 防重放
|
||||
② 操作员密钥(管理员 passkey)──▶ 「谁是操作员、哪个是合法的 Backend」 操作员集合,Backend 持有不了
|
||||
③ 节点自己的密钥 ──▶ 「我是我」 Agent 和 Probe 都自己生成
|
||||
|
||||
被攻破的 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,节点签名认证)
|
||||
对等面(Probe 拨 Agent) TLS 控制 7443/tcp、数据 7443/udp、7444/tcp(先过敲门)
|
||||
```
|
||||
|
||||
- **每个平面只认自己的凭证**:节点面不接受令牌和 Cookie;外部面不接受节点签名;对等面只认敲门通过的成员。
|
||||
- **连接方向固定**:节点面永远是节点拨入 Backend,Backend 从不主动连节点,所谓「下发」只是在节点拨入的连接上发下行消息;对等面永远是 Probe 拨 Agent,Agent 不拨 Probe。
|
||||
- **外部面不能直通节点面**:管理端的操作(例如发起测试)只会触发 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→Probe)。
|
||||
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 未解决) | 能把恶意 Probe 加进 Agent 的白名单,或把 Probe 指向恶意 Agent |
|
||||
| 任务集、测试请求 | Backend 计算;节点本地硬上限兜底 | 只能在本地速率和端口上限内发测量流量 |
|
||||
| 重启计划(`moshiro.schedule`) | Backend;节点本地启动后 10 分钟内不触发 | 最多让节点每 10 分钟重启一次 |
|
||||
| 未来的 `extension`(exec 等) | **属主 passkey 签名的一次性能力令牌**,见第 9 节 | 什么都做不了 |
|
||||
|
||||
## 7. 节点自己被攻破时(设计,未实现)
|
||||
|
||||
### 7.1 测量数据的可信(G8、G9、G10)
|
||||
|
||||
测量数据是这个项目的产出,不能只靠传输层可信:链路有 Cloudflare 这样的中间环节,Agent 又要转发 Probe 的数据。
|
||||
|
||||
- **产生者签名**:每个列块由 Probe 签名,每个原始块由上传它的节点签名;Agent 转发 Probe 的块时原样保留签名。签名覆盖块的摘要(SHA-256),签名串带用途前缀 `chunk` 和节点 ID。Backend 入库时逐个验签,不通过的丢弃并计数。每 10 秒一块,一次 ECDSA 验签的开销可以忽略。
|
||||
- **归属校验**:入库时核对块里的 Probe 是和这个 Agent 有线路的成员,不是就丢弃并计数。
|
||||
- **合理性检查**:`t3−t2` 必须小于 `t4−t1`,各时间戳单调;违反的样本不计入指标,记为异常并产生事件。
|
||||
- 边界:这样中间环节改不了数据,被攻破的 Agent 改不了 Probe 一侧的数据;但它仍然可以谎报自己一侧的 `t2`、`t3`,合理性检查只能限制它的幅度,因为这一侧的真值只有它自己知道。
|
||||
|
||||
### 7.2 对 Backend 的资源和输入
|
||||
|
||||
- **按节点的配额**:每个节点的字节、块数和速率上限,只是计数,不占资源;全局再限制同时解码的块数(单块解码上限是 64 MiB)。
|
||||
- **节点上报的字符串**:`hello` 里的 `platform`、事件的 `detail` 等一律当不可信数据,入库前统一校验长度、字符集和换行,不依赖前端转义;管理端不渲染成 HTML。
|
||||
- **匿名公开接口**:登录已经有限流,但热力图、分布这类重查询只有范围和指标数的上限,要加每源限速和结果缓存。
|
||||
- **webhook**:实现之前先定约束:只允许 HTTPS、禁止私网和回环地址、不跟随重定向、限制超时和响应大小、解析之后再检查地址。
|
||||
|
||||
### 7.3 对对端和身份
|
||||
|
||||
- 被攻破的 Probe 只能拿到 Backend 授权的会话,Agent 的速率上限由本地兜底;被攻破的 Agent 发来的数据只会影响它自己的线路。
|
||||
- **Probe 只拨公网地址**(G3):固定 SPKI 只能防止建立会话,防不了探测本身;要在 Probe 本地拒绝私网、回环、链路本地地址。
|
||||
- **名册里的主机名解析之后再检查地址**:签名保护的是字符串,不是解析结果;默认只放行公网地址,内网需要节点本地显式允许。
|
||||
- **UDP 会话迁移要做路径验证**:协议允许会话在 NAT 映射变化后跟随新的源地址,路径上的攻击者可以借此把会话牵到自己的地址。迁移前先做一次挑战应答,验证前限制发送量(不超过收到字节数的 3 倍),类似 QUIC 的 path validation。
|
||||
- **不协商、不降级**:现在固定 ECDSA P-256 和固定的签名串格式,没有算法协商,保持这一点。
|
||||
- Agent 的 `max_total_mbps` 默认是 1200,对 1 核 1 GB 的小机器没有意义,却是被攻破的 Backend 能让 Agent 向某个 Probe 打出去的上限。默认值应该保守,需要的人再调高;`allowed_target_cidrs` 默认为空是对的。
|
||||
|
||||
## 8. 监听隐身:预握手敲门
|
||||
|
||||
目标:Agent 的 TCP 端口对没有完成挑战的人**不写出任何字节**,包括 TLS 的 ServerHello,扫描器拿不到证书和 TLS 指纹(G4)。UDP 端口本来就是静默的。
|
||||
|
||||
```text
|
||||
Probe Agent TCP 7443 / 7444
|
||||
│── TCP 连接 ───────────────────────────────────▶ accept(内核已完成三次握手)
|
||||
│── 敲门包 32 字节:nonce(16) │ tag(16) ─────────▶ 读 32 字节(限时 3 秒)
|
||||
│ tag = HMAC(K_pair, 时间窗 ‖ nonce) 用名册里每个 Probe 的 K_pair 逐个核对
|
||||
│ ├ 合法:继续 TLS 握手 → 现有的签名认证
|
||||
│◀──────────── TLS ServerHello ─────────────────── └ 不合法或超时:什么都不发,直接断开
|
||||
```
|
||||
|
||||
- `K_pair = HKDF(ECDH(Probe 身份私钥, Agent 身份公钥), "moshiro-knock-v1")`:只有名册里的成员算得出;时间窗 30 秒,接受前后各一个窗;`nonce` 在窗内去重,防重放;每个来源地址限速(默认每来源每 10 秒 300 次)。
|
||||
- **实现**:`Fleet.Identity.Knock`(构造与核对)和 `KnockGate`(读 32 字节、名册、nonce 去重、按来源限速);控制端口(`PeerListener`)和数据端口(`TcpAcceptor`)共用同一个关卡,Probe 一侧 `PeerConnector` 和 `TcpLink` 在 TCP 连接建立后、TLS 之前发敲门包。名册是期望状态里的 `peers`(Probe 的对等面公钥);`probe_agents` 下发的是 Agent 的 `public_key`(Agent 在 hello 里上报)。陌生连接得到的是被关闭的连接,没有任何字节。
|
||||
|
||||
**局限**:TCP 的 SYN-ACK 由内核发出,用户态进程拦不住,所以端口在扫描器眼里仍是「开着,但不说话」,**不是「没监听」**。真正的没监听有两条路:等级二的 UDP 单包授权(Probe 先发签名的 UDP 敲门包,Agent 才临时开 TCP 监听几秒,只放行敲门者的源地址;代价是多一个往返、UDP 丢包重试、多出口源地址可能对不上),或宿主防火墙规则(需要 root,不适合小机器,不做)。先做等级一,等级二可选。
|
||||
|
||||
## 9. extension:敏感能力的归宿
|
||||
|
||||
Agent 和 Probe 的核心永远是纯采集器。**所有敏感能力**(exec、web ssh 和终端、文件读写、抓包等)都不进核心,做成 extension,**用户可选安装,默认没有**,现在都没有实现。
|
||||
|
||||
- **独立进程,不是插件**:单独的二进制和镜像,和核心之间没有进程内接口,核心不加载任何外部代码、不用反射和动态加载,核心的 NativeAOT 和「不能 exec」都不受影响。
|
||||
- **授权只认节点的属主**:每次调用的能力令牌必须由这个节点绑定的属主 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 字节(`UdpPacket`、`TcpWire`) |
|
||||
| 会话密钥派生 | HKDF-SHA256(`SessionKeys`) |
|
||||
| 控制链路加密 | AES-256-GCM(每个方向一把密钥,严格递增的计数器) |
|
||||
| 令牌哈希、SPKI 指纹 | SHA-256 |
|
||||
| 原始块完整性 | CRC32C,不是密码学校验 |
|
||||
|
||||
**BLAKE3 的评估(已定:不采用)**:ECDSA P-256 本身用 SHA-256,令牌哈希和 SPKI 指纹也继续用 SHA-256,所以 BLAKE3 是**多一个**哈希族,不是换掉一个;BCL 里没有,Probe 和 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)。
|
||||
@@ -1,6 +1,6 @@
|
||||
# Docker 和 rootless/rootful Podman 共用。配置、身份和缓存保存在命名 volume 中。
|
||||
# docker compose up -d / podman compose up -d(Podman 以普通用户执行即可 rootless)。
|
||||
# 首次注册、配置导入和自动更新见 docs/deployment.md。
|
||||
# 首次注册、配置导入和自动更新见 docs/ops/deployment.md。
|
||||
services:
|
||||
agent:
|
||||
image: ${MOSHIRO_IMAGE:-registry.oeo.one/moshiro/moshiro-agent}:${MOSHIRO_VERSION:-0.1.1}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Docker 和 rootless/rootful Podman 共用。配置、身份和缓存保存在命名 volume 中。
|
||||
# docker compose up -d / podman compose up -d(Podman 以普通用户执行即可 rootless)。
|
||||
# 首次注册、配置导入和自动更新见 docs/deployment.md。
|
||||
# 首次注册、配置导入和自动更新见 docs/ops/deployment.md。
|
||||
services:
|
||||
probe:
|
||||
image: ${MOSHIRO_IMAGE:-registry.oeo.one/moshiro/moshiro-probe}:${MOSHIRO_VERSION:-0.1.1}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#
|
||||
# Backend 只支持 Podman,更新策略固定为 manual(管理端确认),监听宿主网络的 --port。
|
||||
# 需要 root、systemd,以及 Podman 或 Docker;脚本要和 packaging/ 里的 systemd/、update/ 放在一起。
|
||||
# rootless Podman 见 docs/deployment.md。
|
||||
# rootless Podman 见 docs/ops/deployment.md。
|
||||
set -eu
|
||||
|
||||
usage() {
|
||||
@@ -45,7 +45,7 @@ while [ $# -gt 0 ]; do
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
[ "$(id -u)" -eq 0 ] || { echo "run as root (rootless Podman: see docs/deployment.md)" >&2; exit 1; }
|
||||
[ "$(id -u)" -eq 0 ] || { echo "run as root (rootless Podman: see docs/ops/deployment.md)" >&2; exit 1; }
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
PACKAGING="$(cd "$HERE/.." && pwd)"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
前提:WSL 里有 Debian(root)、tc(netem 注入丢包/延迟/黑洞用);已经用 scripts/publish.sh 发布了 linux-x64 的 agent、backend、fleet-enroll,
|
||||
并且 Windows 上发布了 probe(dotnet publish backend/probe/src/MoShiro.Probe -c Release -r win-x64 -p:PublishAot=true -o backend/artifacts/aot-probe)。
|
||||
把 linux-x64 的产物复制到 WSL 的 /root/mon/,在 WSL 里用 setsid -f nohup 启动 moshiro-backend(监听 0.0.0.0:29181),再运行本脚本。
|
||||
故障注入见 docs/deployment.md 第 3 节。
|
||||
故障注入见 docs/ops/deployment.md 第 3 节。
|
||||
"""
|
||||
import json, os, subprocess, sys, time, urllib.request, urllib.error
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Chip, Confirm, CopyButton, ErrorNotice, Field, useAction } from '@share
|
||||
const KEYGEN = `openssl ecparam -name prime256v1 -genkey -noout -out release.key
|
||||
openssl ec -in release.key -pubout -out release.pub.pem`
|
||||
|
||||
const SIGN = `# manifest.json 是一行扁平的 JSON(字段见 docs/deployment.md),对它的原始字节签名:
|
||||
const SIGN = `# manifest.json 是一行扁平的 JSON(字段见 docs/ops/deployment.md),对它的原始字节签名:
|
||||
openssl dgst -sha256 -sign release.key -out manifest.sig.bin manifest.json
|
||||
base64 -w0 manifest.sig.bin > manifest.sig`
|
||||
|
||||
|
||||
Reference in new issue
Block a user