CI signs the image digest of each agent and probe release; the panel only stores and forwards it. Nodes write the manifest to their data directory and a host updater verifies the signature with a local public key, pulls by digest, restarts the unit and rolls back when it is unhealthy. The panel can list releases, ask a node to update and show the reported update state.
7.4 KiB
7.4 KiB
Fleet:通用更新(Fleet.Update / fleet-updater)
实现状态:容器节点(Agent、Probe)的更新已经实现,见 deployment.md 的「容器节点的更新」:同样的信任模型(离线发布密钥签名、本机配置公钥和仓库、
seq防重放、面板只通知和请求),清单里是镜像 digest,更新器是宿主机上的monitor-update。本文下面描述的纯二进制方案(Fleet.Update、fleet-updater、fleet-release、健康信标)还没有实现。
1. 原则
- 与产品无关:
fleet-updater通过本地配置声明它管理哪些应用,一台机器上可以同时管理多个产品。 - 信任来自签名,而不是来源:只接受由离线发布密钥签名的清单。发布源可以是面板、GitHub Releases、对象存储或任意 HTTP 服务。面板即使被攻破,也推不出恶意二进制。
- 本地说了算:更新策略(
auto、notify、pinned)写在节点本地的配置中,面板只能“建议”目标版本。 - 权限分离:业务进程不能改写自己的二进制;更新进程读不到业务身份私钥,也不参与业务。
┌─ 节点 ─────────────────────────────────────────────────────────────────────┐
│ 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)
{
"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 或离线环境中生成)
{
"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. 更新流程
轮询(间隔加随机抖动)
└─ 拉取 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中移除旧公钥的版本;已经被恶意更新的节点需要人工重装。