Fleet-maintained apps 全链路解析:Fleet 如何让应用目录安全且保持最新
【免费下载链接】fleetOpen device management项目地址: https://gitcode.com/GitHub_Trending/fl/fleet
Fleet 应用目录(Fleet-maintained apps)中的每一款应用,都经过"从厂商官方地址直连下载、按固定哈希校验、在真实硬件上完整安装/卸载验证、由人工审阅"四道关卡,才会抵达你的主机。本文基于 Fleet 开源仓库的清单、生成器、验证器与 CI 工作流,逐层拆解这条从厂商发版到主机部署的供应链管道,并给出添加新应用、冻结坏版本、配置补丁策略的完整实操。
核心要点
- 安装包直接来自厂商:Fleet 从不二次托管或修改安装包,Fleet 服务器从厂商官方分发地址下载应用,并在存储前校验其 SHA-256 哈希。
- 目录自我更新:自动化每 4 小时检查一次上游包源(macOS 的 Homebrew casks、Windows 的 winget manifests),厂商发版当天即可成为更新候选。
- 未经测试不发布:目录的每一次变更,都会先在真实的 macOS 与 Windows 主机上执行完整的安装/卸载生命周期验证,再由 Fleet 团队成员审阅合并。
- 坏更新会被冻结而非发布:若新版本验证失败,Fleet 将该应用保持在最后一个可用版本并记录 bug,绝不让坏更新替换好版本。
- 主机无需人工盯守即可保持最新:应用默认追踪最新已验证版本;你也可以固定版本进行变更控制;补丁策略(patch policies)会自动修复运行过期软件的主机。
- 全程可审计:应用清单、安装/卸载脚本全部开源,你可以直接阅读主机上实际运行的每一条命令,并查看每一次变更的历史。
目录从何而来:上游元数据 + 4 小时自动摄取
Fleet 并不自行维护"每个应用在哪里"的记录。元数据来自被数百万开发者信赖的上游源:macOS 使用 Homebrew casks 中定义,由schedule的 cron 表达式'0 */4 * * *'驱动,即每 4 小时运行一次(同时也支持workflow_dispatch手动触发,以及ee/maintained-apps/**路径变更时的 push 触发)。
工作流的执行逻辑很直接:
- 检出仓库、安装 Go 环境;
- 运行
go run cmd/maintained-apps/main.go,摄取器会对比上游最新的版本、下载 URL 与 SHA-256 哈希; - 若有变化,用
peter-evans/create-pull-request打开一个名为 "Update Fleet-maintained apps" 的 PR,更新应用版本、下载 URL、哈希,并重新生成安装/卸载脚本; - 自动关闭之前已存在、但已被本次新 PR 取代的旧 PR。
新应用进入目录的路径相同:Fleet 团队成员或社区贡献者编写一个输入清单(input manifest),发起 PR。生成器与验证器的入口都在 cmd/maintained-apps/main.go,核心数据结构(FMAManifestApp、FMAManifestFile等)定义在 ee/maintained-apps/maintained_apps.go。
在真实主机上验证:装、验、卸的完整生命周期
任何变更合并前,自动化测试都会下载被变更的应用,在真实硬件上走完完整生命周期:安装它 → 确认应用确实存在于主机上 → 卸载它并确认已彻底消失。macOS 应用在 macOS 主机上验证,Windows 应用则在 x64 或 Arm 主机上验证,以匹配安装器架构。
验证由两个矩阵化的可复用工作流驱动:
- macOS:.github/workflows/test-fma-darwin.yml 先在廉价的 Linux runner 上把所有 darwin 应用切分成 shard(默认每片 25 个),再扇出到 macOS runner 并行验证。因为 GitHub 对 macOS 并发任务有上限,超出上限的 shard 会排队分波运行。
- Windows:.github/workflows/test-fma-windows.yml 按安装器架构做分区——
arm64应用路由到windows-11-armrunner,x64/x86/neutral应用路由到 x64 runner;标记了requires_client_os的应用永远走windows-11-arm(因为 x64 runner 是 Windows Server)。
每个 shard 的实际验证逻辑在 test-fma-darwin-validate.yml 与 test-fma-windows-validate.yml 中。值得注意的细节包括:runner 镜像预装了一些目录中的应用(Chrome、7-Zip、Firefox、Node.js、PowerShell、R、Git 等),验证前会先卸载干净,确保从"干净状态"开始验证;darwin 验证器先安装 osquery 5.18.1 用于查询应用是否存在;Windows 验证通过go run -buildvcs=false ./cmd/maintained-apps/validate执行,且刻意在"移除预装 Git"之后再跑(此时 runner 上已无 git,故需关闭 VCS 烙印)。
真正的验证程序是 cmd/maintained-apps/validate/main.go(含darwin.go、windows.go与app_commander.go三个平台/执行模块),它依据ee/maintained-apps/outputs/apps.json逐个应用执行"下载 → 安装 → 校验存在 → 卸载 → 校验消失"。
通过的变更仍不会自行合并——Fleet 团队成员会审阅每一个 PR 后才发布。失败的变更则根本不会发布:Fleet 将应用冻结在最后一个成功安装的版本并记录 bug。冻结有一个诚实的权衡:如果厂商在冻结期间删除了旧版本的下载链接,那么该应用的安装会失败,直到 Fleet 发布修复版本。团队认为这比"静默发布一个我们无法验证的更新"要好得多。
验证通过的硬性标准
ee/maintained-apps/README.md 明确列出了每个被更新应用必须独立满足的四条验收标准:
- 应用可通过清单中的 URL 下载;
- 应用可通过清单中的安装脚本在主机上成功安装;
- 应用确实存在于主机上;
- 应用可通过清单中的卸载脚本在主机上成功卸载。
只要有一条不满足,就进入"冻结 + 记录 bug"流程。
生命周期一览
主机上的应用如何保持更新
一旦变更发布,你的 Fleet 服务器会自动拾取。服务器每小时刷新一次目录,或在你运行fleetctl trigger --name=maintained_apps时立即刷新。你无需升级 Fleet 就能获得新应用或新版本。
这个"每小时刷新"在服务端有对应的定时任务常量定义,见 server/fleet/cron_schedules.go 中的CronMaintainedApps(值为maintained_apps)。此外还有两个相关的 cron:
CronMaintainedAppsAutoUpdate(maintained_apps_auto_update):Premium 功能,每小时运行一次,把每个 FMA 的活动安装器推进到其固定状态允许的最新缓存版本;CronWindowsMaintainedAppTitles:把报告名称中内嵌版本号的 Windows 软件标题,合并到 FMA 安装器所拥有的标题下。
服务端拉取远程清单并写入本地库的核心逻辑在 server/mdm/maintainedapps/sync.go。其中的Hydrate函数(sync.go 第 174 行起)负责把应用级 FMA 清单中的信息灌入从数据库取出的 FMA 骨架:
- 若指定了版本且有缓存,优先从本地缓存加载安装器级字段(版本、平台、下载 URL、SHA-256、安装/卸载脚本、补丁查询等);
- 缓存未命中时回退到远程清单——这样"刚发布、尚未缓存"的版本(例如管理员在单次 GitOps apply 中固定到某个刚发布的最新版)也能被水合并下载;
- 若请求的版本在当前已发布清单中也不存在,则返回 "version not available" 错误。
默认情况下,Fleet-maintained apps 追踪最新已验证版本。厂商发布更新后,Fleet 会在新安装时使用它;无论主机是通过自助服务、手动安装还是策略自动化安装应用,运行旧版本的主机都会在下次安装时升级到最新版。如果你的变更控制流程需要更强的可预测性,可以把应用固定到特定版本或主版本(即 pin a version);如果某个更新引入了 bug,也可以通过固定到上一版来回滚。
补丁策略:闭环的最后一环
补丁策略(patch policies)负责"收网"落后主机。从应用的详情页进入Actions > Deploy,启用Patch,Fleet 就会生成一条检测运行过期版本主机的策略。要启用安装自动化,选择Patch when app is closed或Force patch。之后可在Actions > Deploy或Policies > [policy] > Edit policy > Patch处修改。
补丁策略的查询会每小时自动更新为引用最新版本(或在你应用 GitOps 配置时更新),所以策略永远不会过期。最终形成一条无手动环节的链路:厂商发布 → Fleet 验证并发布 → 你的服务器同步 → 你的主机自我修复。
安全模型
目录的安全建立在几条朴素承诺之上:
- 厂商直连下载:Fleet 从不重新托管或修改安装包。添加或更新应用时,你的 Fleet 服务器从厂商官方分发 URL 下载安装器——与你自己去下载是同一个地址。
- 固定哈希:每个应用版本都记录了上游包清单中发布的 SHA-256 哈希,Fleet 服务器会拒绝任何不匹配的下载。部分厂商只发布无法预先固定的滚动 "latest" URL,对这些应用,Fleet 改为在下载时记录安装器的哈希。
- 端到端开源:每一份清单、安装脚本和卸载脚本都存在于公开的 Fleet 仓库中,每一次变更都以附带验证结果的 PR 形式到达。你无需猜测某个 FMA 会在主机上运行什么——直接去读就好。
从数据结构上看,哈希与脚本确实以"逐版本 + 去重引用"的方式组织在输出清单中。以 ee/maintained-apps/outputs/7-zip/windows.json 为例:每个版本记录installer_url、sha256、install_script_ref与uninstall_script_ref,而脚本正文则集中存放在同文件的refs映射里,引用键由 ee/maintained-apps/maintained_apps.go 中的GetScriptRef函数(取脚本 SHA-256 的前 8 个十六进制字符)生成——同一脚本多处复用,不重复存储。
以 7-Zip 的 Windows 清单为例,可以看到三个关键查询与安装/卸载脚本引用:
{ "version": "26.03", "queries": { "exists": "SELECT 1 FROM programs WHERE name LIKE '7-Zip %' AND publisher = 'Igor Pavlov';", "patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name LIKE '7-Zip %' AND publisher = 'Igor Pavlov' AND version_compare(version, '26.03') < 0);", "open": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM processes WHERE LOWER(name) IN ('7zfm.exe','7zg.exe'));" }, "installer_url": "https://www.7-zip.org/a/7z2603-x64.msi", "sha256": "c0680064d698a62dd4a5a47f403db356a6531a5473e4c4b1d090ea2590513926", "upgrade_code": "{23170F69-40C1-2702-0000-000004000000}" }这三条 osquery 查询分别回答"应用是否存在"(exists)、"应用是否已打过补丁"(patched)、"应用是否正在运行"(open),是策略与补丁逻辑的判断基础。
如果你在 Fleet-maintained app 中发现疑似安全问题,请通过 Fleet 的漏洞披露计划(见仓库根目录 SECURITY.md)报告。
服务级别目标(SLO)
以下是 Fleet 为目录设定的自我约束目标:
| 活动 | 目标 |
|---|---|
| 检查上游包源是否有新版本 | 每 4 小时 |
| 检测到新版本后发布已验证的应用更新 | 1 个工作日内 |
| 客户 Fleet 服务器拾取已发布的目录变更 | 1 小时内 |
| 审阅新增应用的社区 PR | 3 个工作日内 |
为目录贡献新应用
任何人都可以提议一个新的 Fleet-maintained app。分步说明见 ee/maintained-apps/README.md。提交的应用会与目录中的其他内容一样接受相同的自动化验证,并由 Fleet 工程经理在 3 个工作日内审阅。
macOS 应用:输入清单与生成命令
在 Homebrew formulae:
{ "name": "Box Drive", "slug": "box-drive/darwin", "unique_identifier": "com.box.desktop", "token": "box-drive", "installer_format": "pkg", "default_categories": ["Productivity"] }macOS 输入清单的核心字段:
| 名称 | 类型 | 说明 |
|---|---|---|
name | string | 必填。面向用户的应用程序名称。 |
unique_identifier | string | 必填。平台特有的唯一标识(macOS 上即 bundle identifier)。 |
token | string | 必填。Homebrew 的唯一标识,即 Homebrew API 响应中的token字段。 |
installer_format | string | 必填。安装包文件格式(zip、dmg、pkg),通过 Homebrew APIurl字段的文件扩展名判断;若无扩展名则下载安装包确认。 |
slug | string | 必填。标识应用/平台组合(如box-drive/darwin),格式<app-name>/<platform>,用于命名清单文件并在 Fleet 的 GitOps 配置中引用该应用。 |
default_categories | string | 必填。自助服务未指定分类时的默认分类,合法值为Browsers、Communication、Developer Tools、Productivity。 |
pre_uninstall_scripts | string | 在生成的卸载脚本之前运行的命令行(见 Box Drive 示例中的 File Provider 域清理与 sudoers 处理)。 |
post_uninstall_scripts | string | 在生成的卸载脚本之后运行的命令行。 |
install_script_path | string | 自定义安装脚本(.sh)路径,覆盖生成的安装脚本;脚本必须放在inputs/homebrew/scripts/。 |
uninstall_script_path | string | 自定义卸载脚本(.sh)路径,覆盖生成的卸载脚本;不能与pre_uninstall_scripts/post_uninstall_scripts同时使用。 |
cask_path | string | 本地 cask JSON 文件的仓库相对路径(用于第三方 tap,见下)。 |
Box Drive 的输入清单展示了pre_uninstall_scripts/post_uninstall_scripts的真实用法:卸载前通过fileproviderctl与 Box 的streem工具移除 File Provider 域(区分"归档未同步内容"与"保留未同步内容"两种模式),删除com.box.desktop的偏好设置,并在/etc/sudoers.d/写入临时 sudoers 规则以便调用厂商卸载器,卸载后清理该文件。
自定义 tap 的应用摄取
位于第三方 Homebrew tap(非Homebrew/homebrew-cask)中的应用不会被https://formulae.brew.sh/api/代理。要摄取它们,需把.rb源文件和生成的.json一起提交到ee/maintained-apps/inputs/homebrew/custom-tap/,按 Homebrew tap 的目录布局组织:
custom-tap/ ├── Casks/<token>.rb # Cask DSL source ├── api/<token>.json # Generated with regenerate.sh └── regenerate.sh # Rebuild api/*.json from Casks/*.rb流程为:编写 cask DSL → 在custom-tap/内运行./regenerate.sh生成api/<token>.json(需 macOS + Homebrew +jq)→ 在输入清单中把cask_path设为ee/maintained-apps/inputs/homebrew/custom-tap/api/<token>.json。未设置cask_path的应用继续从formulae.brew.sh抓取。
Windows 应用:输入清单与生成命令
在 winget-pkgs 清单中找到PackageIdentifier后,先在测试 Windows 主机上手动安装该应用,再运行对应的 PowerShell 查询获取 Fleet 用于软件清单匹配的unique_identifier(即注册表中的DisplayName):
- 机器级安装(machine scope):
Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue | Where-Object {$_.DisplayName -like '*<App Name>*'} | Select-Object DisplayName, DisplayVersion, Publisher - 用户级安装(user scope):
Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue | Where-Object {$_.DisplayName -like '*<App Name>*'} | Select-Object DisplayName, DisplayVersion, Publisher
关键提示:如果
unique_identifier与DisplayName不匹配,Fleet 会在 FMA 添加并安装后错误地创建两个软件标题——一个是 FMA,另一个是清单库中的软件。
然后填写输入清单,例如 ee/maintained-apps/inputs/winget/box-drive.json:
{ "name": "Box Drive", "slug": "box-drive/windows", "package_identifier": "Box.Box", "unique_identifier": "Box", "installer_arch": "x64", "installer_type": "msi", "installer_scope": "machine", "default_categories": ["Productivity"] }Windows 输入清单的核心字段:
| 名称 | 类型 | 说明 |
|---|---|---|
name | string | 必填。面向用户的应用程序名称。 |
unique_identifier | string | 必填。平台特有的唯一标识;Windows 上即DisplayName。 |
package_identifier | string | 必填。winget 的PackageIdentifier,Fleet 据此拉取正确的应用元数据。 |
slug | string | 必填。应用/平台组合标识(如box-drive/windows)。 |
installer_arch | string | 必填。x64、x86或arm64(多数应用为x64)。ARM64 构建是独立的 FMA,slug 带-arm64后缀、名称带(ARM64)后缀(如firefox@nightly-arm64/windows),CI 在windows-11-armrunner 上验证。 |
installer_type | string | 必填。exe、msi或msix(指文件类型,而非厂商技术如 "wix")。 |
installer_scope | string | 必填。machine或user(受管安装优先machine)。 |
default_categories | string | 必填。默认分类,合法值同上。 |
install_script_path | string | 自定义安装脚本(.ps1)路径,必须放在inputs/winget/scripts/。.msi应用由摄取器自动生成安装脚本,除非要覆盖生成行为否则不要添加;.exe应用必须提供直接运行安装器的 PowerShell 脚本,Fleet 在安装时把安装包发给主机,脚本必须通过INSTALLER_PATH环境变量执行它。 |
uninstall_script_path | string | 自定义卸载脚本(.ps1)路径。.msi应用自动生成;.exe应用需按厂商文档的静默卸载开关或注册的 UninstallString 编写,确保静默运行并返回安装器退出码。 |
fuzzy_match_name | boolean | 当unique_identifier与DisplayName不匹配时,设为true让 Fleet 用模糊匹配把 FMA 与清单库软件对应起来(如 Pritunl 的unique_identifier是 "Pritunl",而清单库DisplayName是 "Pritunl Client")。 |
requires_client_os | boolean | 安装器拒绝在 Windows Server SKU 上运行时设为true(如 Dell Display and Peripheral Manager)。CI 据此把验证路由到windows-11-armrunner(GitHub 托管的唯一客户端 OS Windows runner),而不是默认的 Windows Server x64 runner。 |
运行生成器并提交
在仓库根目录运行以下命令生成输出数据:
go run cmd/maintained-apps/main.go --slug="<app-name>/darwin" --debug # 或 go run cmd/maintained-apps/main.go --slug="box-drive/windows" --debug贡献者还需负责把应用图标加入 Fleet(TypeScript 与网站 PNG 组件),图标由tools/software/icons下的 generate-icons 脚本生成;脚本会自动在frontend/pages/SoftwarePage/components/icons/index.ts中添加 import 语句和 map 条目,无需手动更新索引文件。之后在ee/maintained-apps/outputs/apps.json中为应用补充描述(macOS 可借用 Homebrew formulae 的描述,采用句子式大小写,格式为<App Name>is a(n) ...,以.结尾)。最后打开 PR,工程经理会被自动添加为 reviewer,同时需 @ 提及 FMA 的负责人;若涉及新图标,还应 @ 提及产品设计师做第二双眼睛。
Windows 常见问题排查
ee/maintained-apps/README.md 还收录了几条高频故障的排查方向:
- Fleet UI 中找不到应用:确认
apps.json已由生成器更新,且你的覆盖 URL 正确; - 安装静默失败:确认
installer_type、installer_arch、installer_scope与所选 winget 安装器一致,并在测试主机上手动运行你的 PowerShell 脚本; - 卸载不干净:优先使用显式卸载脚本;否则确保 winget 清单暴露了
ProductCode或UpgradeCode; - 哈希不匹配错误:若上游清单仍在变动,可在输入 JSON 中设置
ignore_hash: true(谨慎使用)。
在 macOS 上也可以做
以上 Windows 流程本意是在 Windows 主机上运行,但大部分也可以在 macOS 上完成:摄取器是 Go 代码,从 winget/GitHub 抓取数据,跨平台可用;PackageName与Publisher可以在 winget-pkgs 仓库的 locale 和 installer yaml 文件中查找。只有验证与测试阶段仍需要 Windows 主机(用于确认programs.name以及真实运行安装/卸载)。
更新与冻结既有应用
Fleet-maintained apps 需要"尽可能频繁地更新,同时保持可靠性",这目前是一个平衡行为,因为两种情形都会造成阻塞客户工作流的 bug:
- 厂商对安装器的更新可能破坏安装/卸载脚本;
- 厂商会弃用旧安装器的下载链接。
GitHub Action 会周期性创建 PR,通过"提升版本 + 重新生成安装/卸载脚本"来更新目录中的一个或多个应用。PR 中每个被更新的应用都必须独立验证,只有全部满足前述四条验收标准才可合并。若某应用未通过,则执行冻结流程:
不要按原样合并该 PR;
在失败应用的输入文件中加入
"frozen": true(如inputs/homebrew/<app>.json);把对应的输出清单(如
outputs/<slug>.json)回退到main分支版本:git checkout origin/main -- ee/maintained-apps/outputs/<slug>.json运行
go run cmd/maintained-apps/main.go --slug="<slug>" --debug验证冻结后的输入文件——应无报错且不产生任何变更;将输入变更与输出回退提交到同一个 PR。
信任,但要验证
软件供应链攻击之所以得逞,是因为多数更新管道是不可见的——你无法审计你看不见的东西。Fleet 的答案是让从厂商发布到主机安装的整条路径公开:数据源、脚本、测试、审阅。这些承诺全部落在仓库本身:清单在 ee/maintained-apps/inputs/(Homebrew 与 winget 两份输入)与 ee/maintained-apps/outputs/(生成的输出与脚本引用),摄取与验证工作流在 .github/workflows/(ingest-maintained-apps.yml、test-fma-darwin*.yml、test-fma-windows*.yml),生成器与验证器在 cmd/maintained-apps/,服务端同步与水合逻辑在 server/mdm/maintainedapps/。不要只听 Fleet 说——仓库是开放的,PR 历史也是。
参考与延伸
- ee/maintained-apps/README.md:贡献新应用的完整分步指南(macOS/Windows 输入清单、冻结流程、自定义 tap 摄取)。
- .github/workflows/ingest-maintained-apps.yml:每 4 小时自动摄取并开 PR 的 CI。
- .github/workflows/test-fma-darwin-validate.yml 与 .github/workflows/test-fma-windows-validate.yml:真实主机安装/卸载验证。
- cmd/maintained-apps/main.go 与 cmd/maintained-apps/validate/main.go:摄取生成器与验证器。
- server/mdm/maintainedapps/sync.go:服务端目录刷新与版本水合。
- server/fleet/cron_schedules.go:
maintained_apps等定时任务定义。 - 实战延伸:在应用详情页通过Actions > Deploy添加应用并启用Patch补丁策略;需要变更控制时把应用固定到特定版本;GitOps 用户可在配置中直接引用
fleet_maintained_apps条目(参见 cmd/fleetctl/fleetctl/templates/new/fleets/workstations.template.yml 的示例)。
【免费下载链接】fleetOpen device management项目地址: https://gitcode.com/GitHub_Trending/fl/fleet
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考