# Fleet:通用更新(Fleet.Update / fleet-updater) ## 1. 原则 - **与产品无关**:`fleet-updater` 通过本地配置声明它管理哪些应用,一台机器上可以同时管理多个产品。 - **信任来自签名,而不是来源**:只接受由**离线发布密钥**签名的清单。发布源可以是面板、GitHub Releases、对象存储或任意 HTTP 服务。**面板即使被攻破,也推不出恶意二进制。** - **本地说了算**:更新策略(`auto`、`notify`、`pinned`)写在节点本地的配置中,面板只能“建议”目标版本。 - **权限分离**:业务进程不能改写自己的二进制;更新进程读不到业务身份私钥,也不参与业务。 ```text ┌─ 节点 ─────────────────────────────────────────────────────────────────────┐ │ myapp(业务进程) 用户 myapp:只读 /opt/myapp,只能写配置目录 │ │ └─ HealthBeacon ──写──> /run/fleet/myapp.beacon │ │ fleet-updater 用户 fleet-update:只能写 /opt/myapp/versions, │ │ ├─ 读取信标 通过 polkit/sudoers 规则只允许重启 myapp.service │ │ └─ 不读取 node.key │ └────────────────────────────────────────────────────────────────────────────┘ ``` ## 2. 本地配置(`/etc/fleet/updater.json`) ```json { "trust": [ { "id": "rel-2026a", "alg": "ecdsa-p256", "pubkey": "BASE64..." }, { "id": "rel-2027a", "alg": "ecdsa-p256", "pubkey": "BASE64..." } ], "check_interval_s": 3600, "apps": [ { "id": "monitor-agent", "sources": ["https://panel.example.com/fleet/releases", "https://github.com/org/monitor/releases/download"], "channel": "stable", "policy": "notify", "rid": "linux-x64", "install_dir": "/opt/monitor-agent", "keep_versions": 2, "service": { "kind": "systemd", "name": "monitor-agent" }, "health": { "kind": "beacon", "path": "/run/fleet/monitor-agent.beacon", "timeout_s": 60 } } ] } ``` - `trust` 可以配置多把公钥,用于轮换;清单中的 `key_id` 用来选择验证哪一把。 - `policy` 取值:`auto`(验签通过即安装)、`notify`(只下载并上报“有可用更新”,等待面板上的人工批准)、`pinned:`(锁定版本)。 - 面板上的批准只对 `notify` 生效,并且**批准的版本同样必须通过签名验证**。 ## 3. 发布清单(由 `fleet-release` 在 CI 或离线环境中生成) ```json { "schema": 1, "app": "monitor-agent", "channel": "stable", "version": "0.4.2", "released_at": "2026-10-01T00:00:00Z", "min_from": "0.3.0", "artifacts": [ { "rid": "linux-x64", "url": "monitor-agent-0.4.2-linux-x64.tar.gz", "sha256": "…", "size": 4120334 }, { "rid": "linux-arm64", "url": "monitor-agent-0.4.2-linux-arm64.tar.gz", "sha256": "…", "size": 3987112 }, { "rid": "linux-musl-arm64", "url": "monitor-agent-0.4.2-linux-musl-arm64.tar.gz", "sha256": "…", "size": 3990021 }, { "rid": "win-x64", "url": "monitor-agent-0.4.2-win-x64.zip", "sha256": "…", "size": 4455010 } ], "notes_url": "https://…/CHANGELOG#0.4.2", "key_id": "rel-2026a", "signature": "BASE64(ECDSA(规范化 JSON,不含 signature 字段))" } ``` - 签名范围是**规范化后的 JSON**:键排序、无空白、不含 `signature` 字段。规范化规则由 `Fleet.Update` 实现,签名端和验签端使用同一份代码。 - 发布源的目录约定:`///latest.json`(指向当前版本)和 `///manifest.json`。 - 防回滚:updater 在本地记录每个应用已安装过的最高版本;除非本地配置显式允许降级,否则拒绝安装更低的版本,防止旧的签名清单被重放。 ## 4. 更新流程 ```text 轮询(间隔加随机抖动) └─ 拉取 latest.json → 验签 ✗ → 记录并忽略 ├─ 版本 ≤ 当前版本,或当前版本 < min_from → 跳过(后者需要人工介入) ├─ policy = notify → 下载并校验 → 上报“可以更新” → 等待批准 └─ policy = auto 或已批准: 下载 → 校验 sha256 和大小 → 解包到 versions// → 检查可执行文件 → 原子切换 current → versions/(Linux 用符号链接,Windows 见 §5) → 重启服务 → 等待健康信标(version == v,并且 ok_at 晚于重启时刻) ✓ → 记录成功,按 keep_versions 清理旧版本 ✗ 超时 → 切回旧版本 → 重启 → 记录失败,该版本标记为坏版本,不再自动尝试 ``` ## 5. 平台适配 | 平台 | 切换方式 | 服务管理 | | --- | --- | --- | | Linux | `current` 符号链接 + `rename(2)` 原子替换 | systemd:`systemctl restart `,由 polkit 规则限定只能操作被管理的单元 | | Windows | 服务的 ImagePath 指向 `current\app.exe`;先停服务,再切换目录联结(junction),然后启动 | SCM:updater 的服务账户只被授予这几个服务的启动和停止权限 | | OpenWrt | 与 Linux 相同的符号链接切换;默认 `keep_versions = 1`,下载前检查闪存剩余空间 | procd:`/etc/init.d/ restart`(updater 以 root 运行,这是 OpenWrt 的常见做法;仍然不读取节点身份目录) | | 其他(以后) | launchd、OpenRC | 通过适配器接口 `IServiceAdapter` 扩展 | ## 6. 面板侧(`Fleet.Server.Releases`,可选) - 托管清单和制品(也可以只做代理或索引,制品放在 GitHub 或对象存储)。 - **通道与灰度**:每个通道可以指定目标版本,并按标签分批发布,例如先更新 1 台节点,观察 N 分钟没有失败后再更新其余节点。 - **状态汇总**:节点在 `fleet.hello` 中上报 `{app, version}`,updater 上报 `{available, installing, failed, rolled_back}`。面板据此展示版本分布和更新进度。 - updater 与面板通信时,**不使用业务进程的身份**。首版只轮询公开的通道清单,完全不需要身份,状态也由业务进程在 `fleet.hello` 中代为上报。给 updater 单独一对身份密钥是以后的扩展。 ## 7. 发布密钥管理 - 私钥只存在于 CI 的密钥服务或离线环境中,**永远不上传到面板**。 - 轮换方式:新旧两把公钥同时写进 `trust` → 用新密钥发布 → 所有节点都更新到信任新密钥的版本后 → 移除旧公钥。 - 密钥泄露时:发布一个用新密钥签名、并从 `trust` 中移除旧公钥的版本;已经被恶意更新的节点需要人工重装。