111 lines
6.9 KiB
Markdown
111 lines
6.9 KiB
Markdown
# 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` 中移除旧公钥的版本;已经被恶意更新的节点需要人工重装。
|