From e014a19554619506f0d10d166fece8e046220ccd Mon Sep 17 00:00:00 2001 From: JustMe Date: Thu, 8 Oct 2026 19:37:56 +0800 Subject: [PATCH] docs: reorganize into design, security, api, ops and dev; drop stale designs; refresh the diagrams [CI] - 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). --- AGENTS.md | 2 +- README.md | 70 +-- TODO.md | 8 +- backend/README.md | 6 +- backend/agent/README.md | 6 +- backend/fleet/README.md | 4 +- .../fleet/src/Fleet.Hosting/TrustAnchor.cs | 2 +- .../fleet/src/Fleet.Identity/OperatorTrust.cs | 2 +- .../src/Fleet.Identity/WebAuthnAssertion.cs | 2 +- backend/probe/README.md | 6 +- .../Admin/TrustEndpoints.cs | 2 +- .../src/MoShiro.Backend.Api/InstallScripts.cs | 2 +- .../Auth/Capabilities.cs | 4 +- .../Auth/PasskeyService.cs | 2 +- .../Auth/TrustService.cs | 2 +- .../Auth/WebAuthnRegistration.cs | 2 +- .../ArchitectureTests.cs | 2 +- .../MoShiro.Backend.Tests/CapabilityTests.cs | 2 +- .../OpenApiConformanceTests.cs | 2 +- .../MoShiro.Backend.Tests/PasskeyTests.cs | 2 +- .../ProtocolDeclarationTests.cs | 4 +- .../QueryPlanningTests.cs | 2 +- .../tests/MoShiro.Backend.Tests/TrustTests.cs | 2 +- docs/README.md | 18 + docs/access.md | 498 ------------------ docs/{ => api}/api-guidelines.md | 4 +- docs/{ => api}/api.md | 17 +- docs/{ => design}/analysis.md | 0 docs/{ => design}/architecture.md | 71 +-- docs/{ => design}/measurement.md | 0 docs/{ => design}/protocol.md | 12 +- docs/{ => design}/query.md | 4 +- docs/{ => design}/storage.md | 0 docs/{ => dev}/coding-style.md | 2 +- docs/{ => dev}/project-layout.md | 14 +- docs/{ => ops}/deployment.md | 21 +- docs/security.md | 312 ----------- docs/security/access.md | 203 +++++++ docs/{ => security}/enrollment.md | 34 +- docs/security/security.md | 198 +++++++ packaging/compose/agent/compose.yaml | 2 +- packaging/compose/probe/compose.yaml | 2 +- packaging/container/deploy-node.sh | 4 +- scripts/smoke-wsl.py | 2 +- web/admin/src/pages/ReleaseKeys.tsx | 2 +- 45 files changed, 592 insertions(+), 966 deletions(-) create mode 100644 docs/README.md delete mode 100644 docs/access.md rename docs/{ => api}/api-guidelines.md (98%) rename docs/{ => api}/api.md (94%) rename docs/{ => design}/analysis.md (100%) rename docs/{ => design}/architecture.md (78%) rename docs/{ => design}/measurement.md (100%) rename docs/{ => design}/protocol.md (96%) rename docs/{ => design}/query.md (97%) rename docs/{ => design}/storage.md (100%) rename docs/{ => dev}/coding-style.md (99%) rename docs/{ => dev}/project-layout.md (96%) rename docs/{ => ops}/deployment.md (96%) delete mode 100644 docs/security.md create mode 100644 docs/security/access.md rename docs/{ => security}/enrollment.md (72%) create mode 100644 docs/security/security.md diff --git a/AGENTS.md b/AGENTS.md index 1e1c30d..91933fc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/README.md b/README.md index 9c5c48c..beea4c2 100644 --- a/README.md +++ b/README.md @@ -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 │ - │ Store(段文件 + SQLite) │ - └──────────────────────────────────────────────────────────┘ + 操作员 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 后 │ - └────────────────────┘ └──────────────────────┘ + ┌────────────────────┐ ┌──────────────────────┐ + │ Agent × 10+ │ ◀── 测量会话 ── │ Probe × N │ + │ 有公网 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`,展示端是 ,管理端是 。 +本地运行:先在 `backend/` 里 `dotnet run --project src/MoShiro.Backend -- --urls http://localhost:5000`,再 `npm run dev --prefix web`,展示端是 ,管理端是 。第一次打开管理端会进认领向导,认领码在后端日志里(或用 `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/`(流水线)。 diff --git a/TODO.md b/TODO.md index 7d45c7c..8e2b26f 100644 --- a/TODO.md +++ b/TODO.md @@ -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//` → 原子切换 `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 做模糊测试;伪造签名和重放的专项测试。 diff --git a/backend/README.md b/backend/README.md index 8aab4ff..b6a65f0 100644 --- a/backend/README.md +++ b/backend/README.md @@ -36,7 +36,7 @@ dotnet test --solution MoShiro.slnx dotnet publish src/MoShiro.Backend -c Release -r -p:PublishAot=true ``` -例如 `` 可为 `win-x64`、`linux-x64` 或 `linux-arm64`。NativeAOT 发布需要对应平台的 C/C++ 工具链。仓库根目录的 `scripts/publish.sh backend ` 可构建单个平台的 Backend 可执行文件;容器发布由 Woodpecker tag 流水线完成,详见 [`../docs/deployment.md`](../docs/deployment.md)。 +例如 `` 可为 `win-x64`、`linux-x64` 或 `linux-arm64`。NativeAOT 发布需要对应平台的 C/C++ 工具链。仓库根目录的 `scripts/publish.sh backend ` 可构建单个平台的 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)。 diff --git a/backend/agent/README.md b/backend/agent/README.md index ea082c6..296d24a 100644 --- a/backend/agent/README.md +++ b/backend/agent/README.md @@ -29,7 +29,7 @@ dotnet publish agent/src/MoShiro.Agent -c Release -r -p:PublishAot=true dotnet publish fleet/tools/fleet-enroll -c Release -r -p:PublishAot=true ``` -也可在仓库根目录运行 `bash scripts/publish.sh agent ` 和 `bash scripts/publish.sh fleet-enroll `。部署包和平台说明见 [`../../docs/deployment.md`](../../docs/deployment.md)。 +也可在仓库根目录运行 `bash scripts/publish.sh agent ` 和 `bash scripts/publish.sh fleet-enroll `。部署包和平台说明见 [`../../docs/ops/deployment.md`](../../docs/ops/deployment.md)。 ## 注册与运行 @@ -47,7 +47,7 @@ fleet-enroll --server https:// --join --name --ide moshiro-agent --config /etc/moshiro/agent.json ``` -容器节点的运行方式(podman + systemd)和宿主机更新器见 [`../../docs/deployment.md`](../../docs/deployment.md)。也可使用 OCI 镜像 `registry.oeo.one/moshiro/moshiro-agent:`,自动选择 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:`,自动选择 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。首次导入和自动更新见部署文档。 diff --git a/backend/fleet/README.md b/backend/fleet/README.md index ae5ffdd..64bed52 100644 --- a/backend/fleet/README.md +++ b/backend/fleet/README.md @@ -23,7 +23,7 @@ 2. **安全默认值**:Fleet 永远不提供 exec、shell、文件读写、远程下发代码这类方法;节点只响应本地注册过的方法(本地策略 `allow_methods` 还能收窄);调用对方不支持的方法在本地直接返回 `fleet_unsupported`。 3. **权限分离**:注册(`fleet-enroll`,一次性)和常驻业务进程是两个进程;常驻进程不含注册代码,也不使用 `HttpClient`。 4. **NativeAOT**:节点侧只依赖 BCL,泛型扩展点通过产品传入的 `JsonTypeInfo` 序列化,不使用反射。 -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 类型 | 正文`)上。 | 保留方法 | 方向 | 说明 | diff --git a/backend/fleet/src/Fleet.Hosting/TrustAnchor.cs b/backend/fleet/src/Fleet.Hosting/TrustAnchor.cs index b4cf216..55566df 100644 --- a/backend/fleet/src/Fleet.Hosting/TrustAnchor.cs +++ b/backend/fleet/src/Fleet.Hosting/TrustAnchor.cs @@ -25,7 +25,7 @@ public sealed partial class TrustFileContext : JsonSerializerContext; /// /// 节点的信任根:安装命令里带来的创世指纹和属主,在注册时固定到本地,之后 Backend 改不了。 /// 操作员集合的新版本随期望状态下发,节点只接受从固定的创世出发、签名完整、版本更新的链。 -/// 属主只是被固定和保存:节点现在没有 extension,等做 extension 时由节点本地强制(见 docs/access.md 第 8 节)。 +/// 属主只是被固定和保存:节点现在没有 extension,等做 extension 时由节点本地强制(见 docs/security/access.md 第 9 节)。 /// public sealed class TrustAnchor : IServerVerifier { diff --git a/backend/fleet/src/Fleet.Identity/OperatorTrust.cs b/backend/fleet/src/Fleet.Identity/OperatorTrust.cs index fa25752..e8b510d 100644 --- a/backend/fleet/src/Fleet.Identity/OperatorTrust.cs +++ b/backend/fleet/src/Fleet.Identity/OperatorTrust.cs @@ -20,7 +20,7 @@ public sealed record OperatorKey(string CredentialId, byte[] PublicKeySpki); public sealed record OperatorSet(int Version, string Previous, string RpId, IReadOnlyList Origins, IReadOnlyList Keys, IReadOnlyList? Servers = null); /// -/// 信任根:操作员(passkey)→ 操作员集合 → 节点本地固定的属主。设计见 docs/access.md 第 7 节。 +/// 信任根:操作员(passkey)→ 操作员集合 → 节点本地固定的属主。设计见 docs/security/access.md 第 8 节。 /// 安装命令里带着创世指纹和属主(passkey 的凭证 id):节点把它们固定在本地,之后只认从创世出发、由操作员 passkey 签过名的集合版本, /// 属主必须是集合里的凭证。Backend 能转发集合,但签不出新的版本,也改不了节点已经固定的东西。 /// 全是纯函数,只用 BCL,节点和 Backend 共用。语句是 key=value 的文本,第一行是带版本的标题;签名覆盖整段文本。 diff --git a/backend/fleet/src/Fleet.Identity/WebAuthnAssertion.cs b/backend/fleet/src/Fleet.Identity/WebAuthnAssertion.cs index 60a0d0d..4329cc4 100644 --- a/backend/fleet/src/Fleet.Identity/WebAuthnAssertion.cs +++ b/backend/fleet/src/Fleet.Identity/WebAuthnAssertion.cs @@ -7,7 +7,7 @@ namespace Fleet.Identity; /// /// 验证一次 WebAuthn 断言(navigator.credentials.get 的结果)。只支持 ES256(P-256 的 ECDSA),纯函数,只用 BCL: -/// 面板用它验证 passkey 登录,节点以后用它验证 passkey 签出的委托(docs/access.md 第 7 节)。 +/// 面板用它验证 passkey 登录,节点验证操作员集合的签名也用它(docs/security/access.md 第 8 节)。 /// 不解析 CBOR——注册时的 attestation 解析只在 Backend 里。 /// public static class WebAuthnAssertion diff --git a/backend/probe/README.md b/backend/probe/README.md index 09dc3b9..f41e65b 100644 --- a/backend/probe/README.md +++ b/backend/probe/README.md @@ -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:`,容器默认读取 `/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:`,容器默认读取 `/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 -p:PublishAot=true ``` -也可在仓库根目录运行 `bash scripts/publish.sh probe `,再使用 `scripts/package.sh probe ` 生成部署包。支持平台和安装流程见 [`../../docs/deployment.md`](../../docs/deployment.md)。 +也可在仓库根目录运行 `bash scripts/publish.sh probe `,再使用 `scripts/package.sh probe ` 生成部署包。支持平台和安装流程见 [`../../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。首次导入和自动更新见部署文档。 diff --git a/backend/src/MoShiro.Backend.Api/Admin/TrustEndpoints.cs b/backend/src/MoShiro.Backend.Api/Admin/TrustEndpoints.cs index 036916c..3af6294 100644 --- a/backend/src/MoShiro.Backend.Api/Admin/TrustEndpoints.cs +++ b/backend/src/MoShiro.Backend.Api/Admin/TrustEndpoints.cs @@ -8,7 +8,7 @@ using MoShiro.Protocol; namespace MoShiro.Backend.Api; /// -/// 信任根(docs/access.md 第 7 节):操作员集合由 passkey 签名,节点从安装命令里固定的创世指纹出发验证它; +/// 信任根(docs/security/access.md 第 8 节):操作员集合由 passkey 签名,节点从安装命令里固定的创世指纹出发验证它; /// 带操作员委托的加入码也在这里创建——委托是 passkey 对「这个码」签的,面板自己签不出来。 /// public static class TrustEndpoints diff --git a/backend/src/MoShiro.Backend.Api/InstallScripts.cs b/backend/src/MoShiro.Backend.Api/InstallScripts.cs index 6417fbe..4f042a5 100644 --- a/backend/src/MoShiro.Backend.Api/InstallScripts.cs +++ b/backend/src/MoShiro.Backend.Api/InstallScripts.cs @@ -6,7 +6,7 @@ namespace MoShiro.Backend.Api; /// /// 一行安装脚本:packaging/install/ 下的 install.sh 和 install.ps1 作为嵌入资源打进 Backend,原样返回,不替换任何值。 -/// 静态,所以它的 SHA-256 在一个版本内固定:CI 构建时算出来写进签名的发布清单,面板给的命令先核对哈希再执行(docs/access.md 第 15 节)。 +/// 静态,所以它的 SHA-256 在一个版本内固定:CI 构建时算出来写进签名的发布清单,面板给的命令先核对哈希再执行(docs/security/access.md 第 10 节)。 /// 角色、面板地址、代码都走命令里的环境变量。 /// public static class InstallScripts diff --git a/backend/src/MoShiro.Backend.Application/Auth/Capabilities.cs b/backend/src/MoShiro.Backend.Application/Auth/Capabilities.cs index e34718c..767c5d3 100644 --- a/backend/src/MoShiro.Backend.Application/Auth/Capabilities.cs +++ b/backend/src/MoShiro.Backend.Application/Auth/Capabilities.cs @@ -2,7 +2,7 @@ namespace MoShiro.Backend.Application; /// /// 能力(capability):一项具体的权限。端点声明自己需要哪一项, 是唯一的判断入口, -/// 会话、API key 都走它。设计见 docs/access.md 第 2 节。 +/// 会话、API key 都走它。设计见 docs/security/access.md 第 3 节。 /// 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"; diff --git a/backend/src/MoShiro.Backend.Application/Auth/PasskeyService.cs b/backend/src/MoShiro.Backend.Application/Auth/PasskeyService.cs index c058494..1b0bf91 100644 --- a/backend/src/MoShiro.Backend.Application/Auth/PasskeyService.cs +++ b/backend/src/MoShiro.Backend.Application/Auth/PasskeyService.cs @@ -22,7 +22,7 @@ public sealed record PasskeyAssertion(string CredentialId, string AuthenticatorD /// /// passkey 的注册与登录(WebAuthn,ES256,attestation 当 none)。挑战只在内存里,一次有效、5 分钟过期; /// 登录是「可发现凭证」:不需要先输用户名,凭证 id 找到用户。验证断言的纯函数在 (节点以后也用)。 -/// 设计见 docs/access.md 第 3 节。 +/// 设计见 docs/security/access.md 第 4 节。 /// public sealed class PasskeyService { diff --git a/backend/src/MoShiro.Backend.Application/Auth/TrustService.cs b/backend/src/MoShiro.Backend.Application/Auth/TrustService.cs index 5e31245..b949108 100644 --- a/backend/src/MoShiro.Backend.Application/Auth/TrustService.cs +++ b/backend/src/MoShiro.Backend.Application/Auth/TrustService.cs @@ -20,7 +20,7 @@ public sealed record TrustState(bool Initialized, string? Genesis, int Version, /// /// 信任根的 Backend 一侧:保存操作员集合的链、给操作员出下一版的语句、验证他签回来的版本。 /// Backend 不持有任何签名的能力:每一版都要操作员的 passkey 触碰;节点从安装命令里固定的创世指纹出发自己验链, -/// 所以 Backend 在这里的验证只是不收垃圾,不是信任的来源。设计见 docs/access.md 第 7 节。 +/// 所以 Backend 在这里的验证只是不收垃圾,不是信任的来源。设计见 docs/security/access.md 第 8 节。 /// public sealed class TrustService : IOperatorTrustSource, IDisposable { diff --git a/backend/src/MoShiro.Backend.Application/Auth/WebAuthnRegistration.cs b/backend/src/MoShiro.Backend.Application/Auth/WebAuthnRegistration.cs index a85021b..8555172 100644 --- a/backend/src/MoShiro.Backend.Application/Auth/WebAuthnRegistration.cs +++ b/backend/src/MoShiro.Backend.Application/Auth/WebAuthnRegistration.cs @@ -9,7 +9,7 @@ public sealed record RegisteredCredential(byte[] CredentialId, byte[] PublicKeyS /// /// 解析并校验 WebAuthn 注册(navigator.credentials.create 的结果)。只接受 ES256;attestation 一律当 none 处理: -/// 不验设备证明,信任浏览器的实现(docs/access.md)。CBOR 解析只在 Backend 里,节点不带。 +/// 不验设备证明,信任浏览器的实现(docs/security/access.md)。CBOR 解析只在 Backend 里,节点不带。 /// public static class WebAuthnRegistration { diff --git a/backend/tests/MoShiro.Backend.Tests/ArchitectureTests.cs b/backend/tests/MoShiro.Backend.Tests/ArchitectureTests.cs index cdebec7..65737c8 100644 --- a/backend/tests/MoShiro.Backend.Tests/ArchitectureTests.cs +++ b/backend/tests/MoShiro.Backend.Tests/ArchitectureTests.cs @@ -3,7 +3,7 @@ using System.Xml.Linq; namespace MoShiro.Backend.Tests; /// -/// 依赖规则(docs/project-layout.md 第 3 节):由测试在构建期强制。 +/// 依赖规则(docs/dev/project-layout.md 第 3 节):由测试在构建期强制。 /// 读取仓库里所有 .csproj 和源文件,违反规则就失败。 /// public sealed class ArchitectureTests diff --git a/backend/tests/MoShiro.Backend.Tests/CapabilityTests.cs b/backend/tests/MoShiro.Backend.Tests/CapabilityTests.cs index a3362f7..e2a7393 100644 --- a/backend/tests/MoShiro.Backend.Tests/CapabilityTests.cs +++ b/backend/tests/MoShiro.Backend.Tests/CapabilityTests.cs @@ -8,7 +8,7 @@ using MoShiro.Backend.Application; namespace MoShiro.Backend.Tests; -/// 能力模型(docs/access.md 第 2 节):每个管理端点声明能力;密钥只能拿到自己拥有能力的子集;写蕴含读。 +/// 能力模型(docs/security/access.md 第 3 节):每个管理端点声明能力;密钥只能拿到自己拥有能力的子集;写蕴含读。 [Collection("probe-e2e")] public sealed class CapabilityTests { diff --git a/backend/tests/MoShiro.Backend.Tests/OpenApiConformanceTests.cs b/backend/tests/MoShiro.Backend.Tests/OpenApiConformanceTests.cs index be64709..116ac82 100644 --- a/backend/tests/MoShiro.Backend.Tests/OpenApiConformanceTests.cs +++ b/backend/tests/MoShiro.Backend.Tests/OpenApiConformanceTests.cs @@ -4,7 +4,7 @@ using System.Text.RegularExpressions; namespace MoShiro.Backend.Tests; /// -/// 对照 docs/api-guidelines.md 检查运行时生成的 OpenAPI 文档:operationId、路径风格、错误响应格式、字段命名。 +/// 对照 docs/api/api-guidelines.md 检查运行时生成的 OpenAPI 文档:operationId、路径风格、错误响应格式、字段命名。 /// 新增端点违反规范时,这里会直接失败(等价于文档里的 eng/check-openapi)。 /// [Collection("backend")] diff --git a/backend/tests/MoShiro.Backend.Tests/PasskeyTests.cs b/backend/tests/MoShiro.Backend.Tests/PasskeyTests.cs index bf07b66..d4d56ad 100644 --- a/backend/tests/MoShiro.Backend.Tests/PasskeyTests.cs +++ b/backend/tests/MoShiro.Backend.Tests/PasskeyTests.cs @@ -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)]; } -/// passkey 登录:注册、登录、拒绝各种错误的断言、只能管理自己的 passkey。设计见 docs/access.md 第 3 节。 +/// passkey 登录:注册、登录、拒绝各种错误的断言、只能管理自己的 passkey。设计见 docs/security/access.md 第 4 节。 [Collection("probe-e2e")] public sealed class PasskeyTests { diff --git a/backend/tests/MoShiro.Backend.Tests/ProtocolDeclarationTests.cs b/backend/tests/MoShiro.Backend.Tests/ProtocolDeclarationTests.cs index 5dc920e..90a8a9d 100644 --- a/backend/tests/MoShiro.Backend.Tests/ProtocolDeclarationTests.cs +++ b/backend/tests/MoShiro.Backend.Tests/ProtocolDeclarationTests.cs @@ -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); } diff --git a/backend/tests/MoShiro.Backend.Tests/QueryPlanningTests.cs b/backend/tests/MoShiro.Backend.Tests/QueryPlanningTests.cs index 259aa3d..182fb65 100644 --- a/backend/tests/MoShiro.Backend.Tests/QueryPlanningTests.cs +++ b/backend/tests/MoShiro.Backend.Tests/QueryPlanningTests.cs @@ -6,7 +6,7 @@ using MoShiro.Protocol; namespace MoShiro.Backend.Tests; -/// 时间段、步长与数据源的规划:与 docs/query.md 里的表格逐行对应。 +/// 时间段、步长与数据源的规划:与 docs/design/query.md 里的表格逐行对应。 public sealed class QueryPlanningTests { private const long Second = 1000; diff --git a/backend/tests/MoShiro.Backend.Tests/TrustTests.cs b/backend/tests/MoShiro.Backend.Tests/TrustTests.cs index 71a81d0..baa2cd3 100644 --- a/backend/tests/MoShiro.Backend.Tests/TrustTests.cs +++ b/backend/tests/MoShiro.Backend.Tests/TrustTests.cs @@ -15,7 +15,7 @@ namespace MoShiro.Backend.Tests; /// /// 信任根:操作员(passkey)签操作员集合;安装命令里带着创世指纹和属主(都是公开值),节点注册时验证面板的集合并固定在本地。 -/// 设计见 docs/access.md 第 7 节。 +/// 设计见 docs/security/access.md 第 8 节。 /// [Collection("probe-e2e")] public sealed class TrustTests diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..9dd9d56 --- /dev/null +++ b/docs/README.md @@ -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)。 diff --git a/docs/access.md b/docs/access.md deleted file mode 100644 index d0e2b33..0000000 --- a/docs/access.md +++ /dev/null @@ -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=`),适合外部系统轮询。 - - `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=`、时间戳和投递 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 " /tmp/moshiro-install.sh" | sha256sum -c - && sh /tmp/moshiro-install.sh '' -``` - - macOS 用 `shasum -a 256 -c`,Windows 用 `Get-FileHash` 比较,不通过就 `throw`。命令里的 `` 由 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 的挑战」签名,由节点转发;容器里的进程被换掉也伪造不了,除非攻破宿主机。这一步没有做(一键容器安装的节点也没有宿主机更新器)。 diff --git a/docs/api-guidelines.md b/docs/api/api-guidelines.md similarity index 98% rename from docs/api-guidelines.md rename to docs/api/api-guidelines.md index cac57ce..0cde69d 100644 --- a/docs/api-guidelines.md +++ b/docs/api/api-guidelines.md @@ -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` | diff --git a/docs/api.md b/docs/api/api.md similarity index 94% rename from docs/api.md rename to docs/api/api.md index a4ef5f8..4a28d00 100644 --- a/docs/api.md +++ b/docs/api/api.md @@ -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`) diff --git a/docs/analysis.md b/docs/design/analysis.md similarity index 100% rename from docs/analysis.md rename to docs/design/analysis.md diff --git a/docs/architecture.md b/docs/design/architecture.md similarity index 78% rename from docs/architecture.md rename to docs/design/architecture.md index f1f90df..abe704e 100644 --- a/docs/architecture.md +++ b/docs/design/architecture.md @@ -11,25 +11,30 @@ ## 2. 拓扑 ```text - Web(展示端 · 管理端) - ▲ REST + SSE - ┌──────────────────────────────────────────────────────────┐ - │ Backend(面板) │ - │ Api · Registry · Planner · Links │ - │ Releases · Analysis · LiveHub │ - │ Store(段文件 + SQLite) │ - └──────────────────────────────────────────────────────────┘ + 操作员 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 后 │ - └────────────────────┘ └──────────────────────┘ + ┌────────────────────┐ ┌──────────────────────┐ + │ Agent × 10+ │ ◀── 测量会话 ── │ Probe × N │ + │ 有公网 IP,监听 │ 先敲门再 TLS │ 家宽等出口,NAT 后 │ + └────────────────────┘ └──────────────────────┘ 测量会话:TLS 控制通道 + UDP/TCP 数据通道,由 Probe 主动拨出 + 宿主机更新器(容器节点):用本机公钥验签 → 按 digest 拉镜像 → 重启,失败回滚 ``` -- **线路(line)** = 一对(Probe,Agent)。可以有多个 Probe(身份由 Backend 生成,可以吊销、删除),每个 Probe 连接多个 Agent,每个 Agent 同时服务多个 Probe;一条线路的数据按 `line:` 单独存放,互不混合。对外的线路 id 是 `~`。 +- **线路(line)** = 一对(Probe,Agent)。可以有多个 Probe(身份由 Probe 自己生成并用加入令牌注册,可以吊销、删除),每个 Probe 连接多个 Agent,每个 Agent 同时服务多个 Probe;一条线路的数据按 `line:` 单独存放,互不混合。对外的线路 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 ──▶ 管理端(有新版本时点「更新」) + │ 状态写进只读的 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. 仍待确认 diff --git a/docs/measurement.md b/docs/design/measurement.md similarity index 100% rename from docs/measurement.md rename to docs/design/measurement.md diff --git a/docs/protocol.md b/docs/design/protocol.md similarity index 96% rename from docs/protocol.md rename to docs/design/protocol.md index 795b9ab..684a267 100644 --- a/docs/protocol.md +++ b/docs/design/protocol.md @@ -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`,并更新黄金字节测试。 diff --git a/docs/query.md b/docs/design/query.md similarity index 97% rename from docs/query.md rename to docs/design/query.md index 256fbc3..ffe2d71 100644 --- a/docs/query.md +++ b/docs/design/query.md @@ -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` 用于在曲线上做标注(断线、端点变化、时钟跳变等)。 diff --git a/docs/storage.md b/docs/design/storage.md similarity index 100% rename from docs/storage.md rename to docs/design/storage.md diff --git a/docs/coding-style.md b/docs/dev/coding-style.md similarity index 99% rename from docs/coding-style.md rename to docs/dev/coding-style.md index cb2e2db..ab98b58 100644 --- a/docs/coding-style.md +++ b/docs/dev/coding-style.md @@ -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 节判断是否属于破坏性变更。 diff --git a/docs/project-layout.md b/docs/dev/project-layout.md similarity index 96% rename from docs/project-layout.md rename to docs/dev/project-layout.md index 3150bac..6c1c386 100644 --- a/docs/project-layout.md +++ b/docs/dev/project-layout.md @@ -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`); - `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 ` 输出到 `backend/artifacts/publish///`。 -- 容器镜像由 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)。 diff --git a/docs/deployment.md b/docs/ops/deployment.md similarity index 96% rename from docs/deployment.md rename to docs/ops/deployment.md index e124bed..31a5555 100644 --- a/docs/deployment.md +++ b/docs/ops/deployment.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--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 " $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-`:只读根文件系统、丢掉全部能力、Linux 上用宿主网络(Agent 对外暴露端口,也靠它识别本机公网地址),macOS 和 Windows 上 Agent 发布 7443、7444 端口;注册成功(日志出现 `link up`)才算装好。 - 身份和配置在命名卷 `moshiro--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:// --join --name sudo sh deploy-node.sh probe --image-repo registry.oeo.one/moshiro/moshiro-probe --server https:// --join --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 节。 ## 签名清单放在镜像仓库里(更新和首次部署都不依赖面板) diff --git a/docs/security.md b/docs/security.md deleted file mode 100644 index 2b740b5..0000000 --- a/docs/security.md +++ /dev/null @@ -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 | 被攻破的 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 节)。 diff --git a/docs/security/access.md b/docs/security/access.md new file mode 100644 index 0000000..1a4bac4 --- /dev/null +++ b/docs/security/access.md @@ -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 " $f" | sha256sum -c - && MOSHIRO_SERVER=… MOSHIRO_ROLE=… MOSHIRO_CODE=… sh "$f" +``` + +macOS 用 `shasum -a 256 -c`,Windows 用 `Get-FileHash` 比较,不通过就 `throw`。命令里的 `` 由 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 是持有者凭证,谁拿到令牌,谁就能在它的有效期内创建加入码,必须按密码对待。 diff --git a/docs/enrollment.md b/docs/security/enrollment.md similarity index 72% rename from docs/enrollment.md rename to docs/security/enrollment.md index aa548f0..d342772 100644 --- a/docs/enrollment.md +++ b/docs/security/enrollment.md @@ -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 的本地配置:主机状态上报 diff --git a/docs/security/security.md b/docs/security/security.md new file mode 100644 index 0000000..e738e52 --- /dev/null +++ b/docs/security/security.md @@ -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:`) | 未解决:被攻破的 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)。 diff --git a/packaging/compose/agent/compose.yaml b/packaging/compose/agent/compose.yaml index 57cb567..7947686 100644 --- a/packaging/compose/agent/compose.yaml +++ b/packaging/compose/agent/compose.yaml @@ -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} diff --git a/packaging/compose/probe/compose.yaml b/packaging/compose/probe/compose.yaml index 7c20553..05902b9 100644 --- a/packaging/compose/probe/compose.yaml +++ b/packaging/compose/probe/compose.yaml @@ -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} diff --git a/packaging/container/deploy-node.sh b/packaging/container/deploy-node.sh index 40acc45..6929027 100644 --- a/packaging/container/deploy-node.sh +++ b/packaging/container/deploy-node.sh @@ -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)" diff --git a/scripts/smoke-wsl.py b/scripts/smoke-wsl.py index 029bb4f..f4a55d8 100644 --- a/scripts/smoke-wsl.py +++ b/scripts/smoke-wsl.py @@ -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 diff --git a/web/admin/src/pages/ReleaseKeys.tsx b/web/admin/src/pages/ReleaseKeys.tsx index b50ba24..d7fe8e9 100644 --- a/web/admin/src/pages/ReleaseKeys.tsx +++ b/web/admin/src/pages/ReleaseKeys.tsx @@ -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`