Files
MoShiro/docs/fleet/update.md
T
2026-10-04 21:46:39 +08:00

111 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:<version>`(锁定版本)。
- 面板上的批准只对 `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` 实现,签名端和验签端使用同一份代码。
- 发布源的目录约定:`<source>/<app>/<channel>/latest.json`(指向当前版本)和 `<source>/<app>/<version>/manifest.json`。
- 防回滚:updater 在本地记录每个应用已安装过的最高版本;除非本地配置显式允许降级,否则拒绝安装更低的版本,防止旧的签名清单被重放。
## 4. 更新流程
```text
轮询(间隔加随机抖动)
└─ 拉取 latest.json → 验签 ✗ → 记录并忽略
├─ 版本 ≤ 当前版本,或当前版本 < min_from → 跳过(后者需要人工介入)
├─ policy = notify → 下载并校验 → 上报“可以更新” → 等待批准
└─ policy = auto 或已批准:
下载 → 校验 sha256 和大小 → 解包到 versions/<v>/ → 检查可执行文件
→ 原子切换 current → versions/<v>(Linux 用符号链接,Windows 见 §5)
→ 重启服务 → 等待健康信标(version == v,并且 ok_at 晚于重启时刻)
✓ → 记录成功,按 keep_versions 清理旧版本
✗ 超时 → 切回旧版本 → 重启 → 记录失败,该版本标记为坏版本,不再自动尝试
```
## 5. 平台适配
| 平台 | 切换方式 | 服务管理 |
| --- | --- | --- |
| Linux | `current` 符号链接 + `rename(2)` 原子替换 | systemd:`systemctl restart <name>`,由 polkit 规则限定只能操作被管理的单元 |
| Windows | 服务的 ImagePath 指向 `current\app.exe`;先停服务,再切换目录联结(junction),然后启动 | SCM:updater 的服务账户只被授予这几个服务的启动和停止权限 |
| OpenWrt | 与 Linux 相同的符号链接切换;默认 `keep_versions = 1`,下载前检查闪存剩余空间 | procd:`/etc/init.d/<name> 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` 中移除旧公钥的版本;已经被恶意更新的节点需要人工重装。