TREK 插件管理面板实战:从零装好一个第三方插件并守住安全底线
【免费下载链接】TREKA self-hosted travel/trip planner with real-time collaboration, interactive maps, PWA support, SSO, budgets, packing lists, and more.项目地址: https://gitcode.com/GitHub_Trending/nomad22/TREK
TREK 是一款自托管的旅行规划应用,插件系统让它可以在不改一行源码的前提下长出新的能力:仪表盘 widget、独立页面、行程页标签,还有照片、日历、通知渠道这类集成。本文带你在Admin → Plugins面板里走完一遍真实运维动作:安装、审查、启用、更新、卸载,以及每一步背后的安全边界。看完你可以直接接手一个生产实例的插件治理。
装之前,先确认两个开关状态
打开面板前有两个东西要先弄清楚。
第一层:运行时总开关。环境变量TREK_PLUGINS_ENABLED控制整个插件系统,默认是开的。判定逻辑写得很直白(见 kill-switch.ts):值只要不是false、0、off、no(大小写不敏感)就算开启,而且每次调用时实时读取环境变量,改完重启立即生效。
第二层:逐个激活。总开关打开只代表"电闸合上了",但每个插座(插件)默认还是断电的——所有插件必须逐个手动激活,激活之前没有任何第三方代码执行。两层关系记成一句话:开关管系统,开关不管具体谁在跑。
如果总开关被关掉,面板会提示Plugins are disabled,大意是运行时已关闭,任何插件都无法运行,需要管理员在服务端配置里重新打开。总开关关闭时,已安装插件不会从磁盘消失,只是处于停用状态,重新打开后随时可恢复。
插件相关的环境变量一共四个,先列在这里,后文用到时会展开:
| 环境变量 | 作用 | 默认 |
|---|---|---|
TREK_PLUGINS_ENABLED | 插件系统总开关 | 开启 |
TREK_PLUGINS_DEV_LINK | 允许从本地构建目录注册插件并热重载,值必须恰好是1 | 关闭 |
TREK_PLUGIN_ALLOW_PRIVATE_EGRESS | 设为on后允许插件出口访问私网/内网地址 | 关闭(默认拒绝私网) |
TREK_PLUGIN_REGISTRY_URL | 覆盖 Discover 页浏览的注册表索引地址,可指向自己的镜像 | 官方注册表 |
面板长什么样:两个视图加一个容易忽略的按钮
面板顶部是分段切换器,两个视图:
- Installed(已安装):当前实例装了什么,带数量统计,多一个Status状态筛选(Active / Off / Update available / Error)。
- Discover(发现):社区注册表,卡片式浏览。
两边共享一套工具栏:Search plugins…搜索框、Type类型筛选(Widget / Page / Integration / Trip page)、Sort排序、Upload plugin上传按钮、Rescan重扫描按钮。
Rescan 值得单独说。很多人以为它只是"刷一下列表",其实它做两件事:重新发现磁盘上本地安装的插件,同时强制拉取远程注册表,绕过服务器 30 分钟的缓存(CACHE_TTL = 30 * 60 * 1000,见 registry.service.ts)和 GitHub 的 CDN。为什么非要绕缓存?因为不绕的话,一个刚发布的插件最长要等约 35 分钟才看得见。点一次 Rescan,新插件立刻出现。拉取的是聚合的dist/index.json而不是逐个插件调 GitHub API,避免速率限制;即使注册表请求失败也只是软降级,面板本身照常能用。
从注册表装一个插件:三步,以及版本不兼容时按钮会变的逻辑
安装流程就三步:切到 Discover 找到卡片 → 点开卡片进入预安装审查对话框→ 点Install。
这里有一个容易被忽略的细节:你看到的按钮不总是Install。如果最新版要求的 TREK 版本比当前实例新,按钮会变形——还有兼容的旧版本时,按钮变成Install {version}(装那个旧版本);如果所有版本都不兼容,按钮变成Incompatible并禁用。无论哪种情况,对话框里都会有一条琥珀色提示条把原因讲清楚,不会藏进 tooltip。背后是 host-compat.ts 的assertHostCompatible/hostSatisfies在比对插件声明的trek版本范围与宿主版本。
装完的插件默认是关闭状态。安装流程只做下载、校验、安全解压、重新校验 manifest、注册为 inactive,不执行任何内容。
预安装审查对话框:四块信息,数据来源是"审查时点的 manifest"
点卡片后你会看到四个区块:
- What it can access——它请求的权限,逐条用大白话渲染("读取行程……"、"创建和编辑地点……")。未知权限代码会原样显示;一条不请求就显示Needs no special access.
- Connects to——manifest 声明的所有可访问主机,等宽字体 chip。
- Setup——插件要求填写的设置项,标注Instance-wide或Per user,必填项带Required。
- Details——版本、体积、所需 TREK 版本范围、审查时间、总下载量。
对话框底部还有Source repository、Report an issue、Homepage三个链接。
关键在数据来源:这些内容不是作者写的介绍文案,而是服务端拉取插件审查提交点的实时 manifest 生成的预览(ManifestPreview,registry.service.ts),包含权限列表、出口主机列表、operatorEgress标记、设置字段、许可证、图标、所需 addon 与插件依赖。也就是说,你审查的是"被钉在某个提交上、经过维护者扫描的那个字节"的声明,而不是作者事后想说什么就说什么。
"只有我知道地址"的插件:允许主机白名单怎么管
有一类插件要访问只有你自己知道地址的服务,比如自托管的 Gotify、ntfy。manifest 没法预知主机名,所以这类插件会声明operatorEgress,审查对话框里就多出一个+ hosts you addchip 和一条提示:装完后去 ⋯ → Allowed hosts 添加它允许到达的主机,其它主机一律到不了。
装完之后你会在插件行看到一个琥珀色的Add allowed hostchip——这是有意的,因为此时它一个主机都访问不到,不提示的话看起来就像静默故障。往 ⋯ → Allowed hosts 对话框里逐个添加主机名后,chip 变蓝并显示数量。
三个机制必须知道:
- 保存即重启。运行中插件的出口白名单不能在原进程内热扩容——出口守卫在子进程初始化时安装一次且拒绝二次 init(plugin-runtime.service.ts 中有明确注释),所以唯一让新列表生效的方式就是重新拉起子进程。删除某个主机同理,立即失去出口并重启。
- 校验规则与 manifest 一致:不允许裸
*、不允许整 TLD 通配、不允许带协议前缀。 - 权限边界是死的。没声明
operatorEgress的插件永远无法被授予主机,安装时同意的范围就是硬边界;只有管理员能添加主机,普通用户永远不能扩大插件出口,哪怕凭据是用户自己填的。
❗ 如果目标服务跑在 TREK 同一台机器或同一局域网(localhost、192.168.x.x),还要设TREK_PLUGIN_ALLOW_PRIVATE_EGRESS=on——默认插件不得访问私网。但这个变量会放宽所有已安装插件的私网出口策略,只有你信任全部插件时才该开。
启用失败:三种被拒原因和它们的补救路径
每个插件行上有一个Enable plugin开关,旁边是Active或Off状态。图标块上的彩色圆点反映运行时健康:active 绿、starting 蓝闪、error 红、inactive 淡、disabled/incompatible 琥珀。
启用被拒时会给出结构化错误码(plugin-runtime.service.ts 的assertActivatable检查链),从最严重到最次依次检查:
- 宿主版本不兼容(
TREK_VERSION_INCOMPATIBLE/TREK_VERSION_UNKNOWN)——插件声明的 TREK 支持范围与当前实例对不上。 - 权限再同意——新版本请求了尚未授予的权限,见下文更新章节。
- 必需 addon 未启用(
ADDON_DISABLED)——toast 会报出 addon 名称,去 Admin → Addons 开启后重试。 - 插件依赖缺失或过旧(
DEPENDENCY_MISSING)——对话框逐条列出依赖,提供一键Download / Update,装好最新兼容版本后自动重试启用。
整个预检是只读的,任何一项不满足都不会留下半激活状态。
级联语义双向存在。启用时,服务端先按依赖图算出enableOrder,拉起所有依赖再拉起目标;已安装但停用的依赖会被自动级联启用,toast 会告诉你哪些被顺带打开。反过来,如果某插件依赖的 addon 被关闭,它会被自动停用(deactivateForDisabledAddon会顺带停用所有传递依赖它的插件);同理,你手动停用一个被依赖的插件,所有依赖它的插件也会一起停(deactivateWithDependents)——一个插件不能在依赖缺失的情况下继续跑。
⋯ 菜单里还有:Restart(仅活动插件)、View error log、Allowed hosts、Source repository与Report an issue(仅注册表插件)、Delete。
更新:绝不静默扩权,新权限必须二次点头
有新版本时,插件行出现Update → v{version}按钮,列表上方多一条{count} updates available for your plugins.提示和Update all批量按钮。
如果新版本请求了尚未授予的权限,TREK 的做法是:新代码装上去,但插件保持关闭,并提示大意是"{name} v{version} 在请求你还没批准的权限,新版本已安装但在你批准前保持关闭"。对话框分两列:Newly requested permissions与New outbound connections,你选Approve & turn on或Keep off for now。批量更新多个插件时,这些同意提示会排队依次出现,一个都不跳。若被同意的新版本未签名,对话框还会补一句:没有任何机制把这个版本和作者绑定。
服务端保证"更新永不会静默扩大权限":update()对比新版本声明权限与已授予权限的差集(newGrants),无新增就透明重启到新代码;有任何新增(权限或出口主机)就保持 inactive 并返回差集。同时resolveUpdateTarget挑的是当前 TREK 能运行的最新版本而非简单最新版——避免新版本放弃对旧宿主的支持时,把本来好好的插件更新坏。
签名密钥变更:唯一可以"覆盖"的情形,且必须回显完整公钥
作者的签名密钥和安装时钉住的密钥对不上时,更新被拒,插件行显示Update blocked — {reason}和Review链接。对话框并排展示钉住的密钥指纹与当前提供的密钥指纹,并提醒:TREK 分不清合法的密钥轮换和账号接管,两者在这里长得一样,请先通过你信任的渠道向作者确认新密钥。
覆盖范围有严格限定:只有"密钥变更"(SIGNATURE_KEY_CHANGED)可以给Trust the new key & update按钮。签名无效、缺失或半声明(half-declared)只有解释,没有任何覆盖按钮,服务端同样拒绝这些情形。
服务端的/retrust端点强制了这一点(plugins.controller.ts 与assertRetrustable):调用方必须回显对话框中展示的完整公钥,防止对话框渲染之后注册表条目又被换钥。重信任与更新在同一调用内完成——要么新密钥通过校验、插件落到新版本并钉住新密钥,要么什么都不变,不存在"钉住未验证密钥"的中间窗口。
卸载:它会清掉哪些东西
⋯ → Delete弹确认框:Uninstall plugin?——大意:这会停止插件、移除代码、删掉它的全部数据,不可撤销。
uninstall执行的动作(plugin-runtime.service.ts):停止插件进程、移除代码目录、删除plugins注册表行与设置字段。选deleteData时再清:插件自己的数据目录、错误日志、实体元数据、每用户配置(含加密密钥)、OAuth 令牌与状态、迁移台账、能力审计日志、待处理的 GDPR 擦除队列。
两条无条件规则要记牢:
- 出口主机与定时任务无条件删除——否则后续复用同一个 id 的插件会悄悄继承前人的权限。
- 若选择保留数据,待处理的用户数据擦除义务也会保留,等同 id 插件重装后继续兑现。
⚠️ 卸载前想留资料就提前导出,这个对话框里没有"先备份再删"的选项。
徽章说明什么:Reviewed、Signed、Unsigned 的保证与不保证
- Reviewed(已审查):TREK 维护者对每个版本做过恶意软件扫描。但注意,这不承诺质量、不承诺能用,更不是"无害"担保。
- Signed(已签名):安装时文件已对照作者签名密钥校验,且密钥被钉住(TOFU,信任首见)。校验和证明"字节是注册表担保的字节",签名再往前一步,证明"这些字节来自作者"。
- Unsigned(未签名):字节和注册表担保的一致,但没有任何东西把它和作者绑定。目前注册表里大多数插件未签名,所以这是琥珀色提示而非警报。
两个徽章都不告诉你代码到底做了什么。这也是下面这节要回答的问题:如果它恶意,最坏会发生什么?
最坏情况推演:隔离模型与权限边界
每个活动插件跑在独立 OS 子进程里,用 Node 权限模型(--permission)启动,文件系统读取限定在它自己的代码目录内。插件摸不到JWT_SECRET、数据库连接或任何 TREK 机密——这些对它的进程物理不可达;它不能打开trek.db、不能写文件、不能派生子进程、不能用 worker 线程、不能加载原生模块;它自己的数据放在独立 SQLite 文件里,且只能经由 TREK 访问。
插件与 TREK 只通过内部 RPC 通道通信,TREK 只应答 manifest声明且你批准的能力,未授予的调用是被拒绝而非忽略。RPC 通道本身对插件代码是封死的:即使插件跑在 fork 进程里,其原始 IPC 原语(process.send、process.on('message'))在代码加载前就被吊销——它既伪造不了宿主消息,也窃听不了在途请求,一切交互都被迫经过能力校验的 SDK(实现见 runtime/plugin-sdk.ts 与 supervisor/plugin-supervisor.ts)。
界面侧,插件的页面/组件跑在密封的浏览器 frame中,读不到会话 cookie,碰不到外围 TREK 页面。进程侧,插件崩溃、挂起或内存耗尽时,死掉的只有它自己的进程,TREK 继续运行,你可以随时重启或停用它。
把上面串起来,"最坏情况"的边界就清楚了:你批准的权限列表是真实边界而非标签。它界定插件能触及什么,但管不了它在授权范围内的意图——一个被允许读行程、且允许连某主机的插件,完全可能把行程数据发过去。所以装之前读权限清单和出口主机,是这道防线里唯一由你拍板的一环。
两条旁路:上传插件与开发链接
绕过注册表有两条路,各有适用场景和风险。
Upload plugin(旁路上传)。工具栏按钮或直接拖.zip到面板。上传的归档保持 inactive,激活时照样要同意权限。该行标记Sideloaded,含义是"手动上传,非来自注册表,无签名无审查"。服务端sideload()先解压到 staging,做和注册表安装相同的硬性防护:防 zip-slip/zip 炸弹的安全解压、严格 manifest 校验、拒绝原生二进制;只有 SHA-256/签名校验不适用(因为没有注册表条目可比对)。上传上限 50 MB(plugins.controller.ts 中50 * 1024 * 1024 + 4096)。用相同 id 覆盖上传时,旧代码会先被强制停止并停用再替换,不会出现"未经重新激活仍在跑"的旧状态。
Link a local plugin(开发链接)。一个路径输入框,从本地构建目录注册插件并针对真实数据热重载。该入口仅在TREK_PLUGINS_DEV_LINK=1(值必须恰好是1,见 dev-link.ts)时出现,面向开发环境。该行标记Dev-Link。实现是link():对代码目录建符号链接而非复制,校验 manifest、拒绝原生二进制,注册为 inactive,再用fs.watch监听构建输出,重建后防抖 400ms 自动重新 fork。
对自己诚实一点:这两个徽章都只是卡片上的标签。它们记录代码来源,但不会触发额外检查、不同沙箱或额外限制——旁路插件以它声明的权限原样运行,和注册表插件待遇完全相同,只是没人替这份代码做过恶意扫描和签名背书。source 徽章会替换这些行上的 Signed/Unsigned 徽章,因为它本身就是更强的声明。
审计:插件替你做了什么,谁来查
面板上的每个端点都要求管理员账户,且叠加TREK_PLUGINS_ENABLED总开关;dev-link 还额外要求TREK_PLUGINS_DEV_LINK。服务端路由统一挂在@Controller('api/admin/plugins'),同时用JwtAuthGuard与AdminGuard(plugins.controller.ts);运行时关闭时,install、upload、activate、update、rescan等操作统一返回 503。
除了管理端点,插件的每次行动本身也是可审计的。每个用户都能在Settings → Plugins的活动日志里查看插件以自己名义执行的全部操作:读了哪些行程和费用、写了哪些地点、TREK 代发的每一次出站调用。这个视图不设管理员门槛——这是 TREK 基于哈希链的防篡改插件审计的用户侧;管理员则在Admin → Plugins看到按插件维度的聚合视图。
每条权限的确切授予范围——从只读的db:read:trips(每次调用对操作者做 membership 校验),到写入类的db:write:places(叠加place_edit权限与写入审计),再到宿主中介的oauth:client(宿主持令牌,插件只拿短期 access token)和notify:send(收件人被限定为操作者本人或其所属行程)——完整记录在 wiki/Plugin-Permissions.md。
延伸阅读
- wiki/Plugins.md:插件系统全貌,类型、隔离模型、依赖与活动日志
- wiki/Plugin-Permissions.md:每条权限的授予范围与
http:outbound细节 - wiki/Plugin-Development.md:SDK 与 manifest 编写
- wiki/Plugin-Publishing.md:注册表提交流程与
trek-pluginCLI - wiki/Admin-Addons.md:插件可能依赖的 addon 管理
- wiki/Admin-Panel-Overview.md:管理面板总览
- wiki/Environment-Variables.md:插件相关环境变量完整参考
- wiki/Security-Hardening.md:安全加固建议
- 插件运行时源码:server/src/nest/plugins/
【免费下载链接】TREKA self-hosted travel/trip planner with real-time collaboration, interactive maps, PWA support, SSO, budgets, packing lists, and more.项目地址: https://gitcode.com/GitHub_Trending/nomad22/TREK
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考