尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Headlamp 桌面版安全存储实战:用 pluginSecureStorage 实现插件级加密凭据存取

Headlamp 桌面版安全存储实战:用 pluginSecureStorage 实现插件级加密凭据存取 Headlamp 桌面版安全存储实战用 pluginSecureStorage 实现插件级加密凭据存取【免费下载链接】headlampA Kubernetes web UI that is fully-featured, user-friendly and extensible项目地址: https://gitcode.com/GitHub_Trending/he/headlampHeadlamp 是 Kubernetes 的 Web 管理界面其插件体系允许开发者扩展桌面版Electron 应用的功能。pluginSecureStorage是其中一项仅限桌面版的能力它让插件把 OAuth Token 一类的本地小凭据用 ElectronsafeStorage加密后保存在运行 Headlamp 的计算机上并且每个插件只能访问自己安装命名空间内的键值无法按名称读取其他插件的数据。读完本文你将能够运行官方示例插件、在自己的插件中正确声明并调用save / load / delete三个接口并理解 Headlamp 主进程如何通过能力令牌capability token实现插件间存储隔离、输入校验与持久化原子性。功能定位本地加密存储而非 Kubernetes Secret在动手之前先明确这个 API 的适用边界这也是官方文档反复强调的核心前提只存在于桌面版pluginSecureStorage仅在 Headlamp 以桌面应用方式运行Electron时由运行时注入。作为 Web 应用部署的 Headlamp 没有该参数插件必须先用Headlamp.isRunningAsApp()判断环境再注册依赖该 API 的界面只存本地应用凭据数据保存在运行 Headlamp 的那台计算机上不跨机器同步也不会创建或同步任何 Kubernetes 资源不是 K8s Secret 的替代品如果凭据需要与集群工作负载或其他用户共享应使用 Kubernetes API 创建 Secret而不是把它写进这个 API。示例插件的 README 对此的表述非常直白Do not use the example value as a Kubernetes Secret. This API stores local application credentials on the computer running Headlamp。快速运行示例插件仓库内置了一个完整的演示插件secure-storage-example它会保存、读取并删除一个示例凭据。运行方式cd plugins/examples/secure-storage npm install npm start启动后在 Headlamp 左侧 HOME 侧边栏点击Secure Storage即可进入示例页面。页面提供三个操作输入一段示例凭据后 Save加密保存、Load解密加载、Delete删除条目并实时显示操作状态。该插件的 package.json 遵循 Headlamp 插件的标准脚手架所有脚本start、build、lint、test、storybook等都通过kinvolk/headlamp-plugin提供的headlamp-pluginCLI 驱动开发依赖也只有这一个插件 SDK。示例插件实现走读全部逻辑在 src/index.tsx 一个文件中结构清晰地展示了插件接入安全存储的完整模式。1. 声明注入参数pluginSecureStorage是 Headlamp 在桌面模式下运行插件时注入的“私有参数”private argument插件代码需要自己在 TypeScript 层声明它/** Result returned by a plugin secure-storage operation. */ interface SecureStorageResult { /** Whether the operation completed successfully. */ success: boolean; /** The loaded value, or null when the key does not exist. */ value?: string | null; /** A stable error description when the operation fails. */ error?: string; } /** Encrypted key/value operations scoped to this plugin installation. */ interface PluginSecureStorage { save(key: string, value: string): PromiseSecureStorageResult; load(key: string): PromiseSecureStorageResult; delete(key: string): PromiseSecureStorageResult; } // Headlamp injects this private argument when it runs a plugin in the desktop app. declare const pluginSecureStorage: PluginSecureStorage;2. 三个操作的调用与结果处理示例使用固定键STORAGE_KEY example-credential对每个操作都检查success字段const saveValue async () { const result await pluginSecureStorage.save(STORAGE_KEY, value); setStatus(result.success ? Value encrypted and saved. : Save failed: ${result.error}); }; const loadValue async () { const result await pluginSecureStorage.load(STORAGE_KEY); if (!result.success) { setStatus(Load failed: ${result.error}); return; } // load 的语义键不存在时 value 为 null但 success 仍为 true setLoadedValue(result.value ?? null); setStatus(result.value null ? No value is stored. : Value loaded and decrypted.); }; const deleteValue async () { const result await pluginSecureStorage.delete(STORAGE_KEY); if (result.success) { setLoadedValue(null); setStatus(Stored value deleted.); return; } setStatus(Delete failed: ${result.error}); };这里有几个必须记住的 API 契约细节与功能文档中的 Secure Storage 一节一致load在键不存在时返回success: true且value: null——“不存在”是正常结果不是错误失败的请求返回success: false加一个稳定的error描述插件不应把失败当成“值缺失”处理Headlamp 会在操作系统密钥库不可用、持久化数据无法安全读取、或输入/存储超限等场景下拒绝操作。3. 条件注册 UI 路由示例最后只在桌面模式下注册侧边栏条目和路由if (Headlamp.isRunningAsApp()) { registerSidebarEntry({ name: secure-storage-example, label: Secure Storage, url: /secure-storage-example, icon: mdi:shield-key, sidebar: HOME, }); registerRoute({ path: /secure-storage-example, sidebar: secure-storage-example, useClusterURL: false, // 该页面不依赖集群上下文 noAuthRequired: true, // 无需登录集群即可访问 name: secure-storage-example, exact: true, component: SecureStorageExample, }); }这正是 README 所说“作为 Web 应用运行时不注册路由”的原因条件注册保证插件在 Web 模式下也能正常加载只是不提供依赖pluginSecureStorage的界面。底层原理能力令牌与 IPC 隔离README 中一句“Headlamp isolates the value in the example plugins trusted installation namespace, so another plugin cannot select that namespace by name”背后是一套完整的隔离机制横跨渲染进程、插件加载器和 Electron 主进程三层。命名空间从哪里来从源码结构看命名空间不由插件代码自己命名而是由后端插件清单plugin inventory的可信元数据推导。frontend/src/plugin/secureStorage.ts 中的getPluginSecureStorageNamespace根据folderName后端报告的插件目录名和sourcedevelopment/user/shipped之一生成形如source--folderName的命名空间元数据不完整时直接返回undefined插件就拿不到存储能力。能力令牌capability token每次页面加载时Electron 主进程会为每个可信插件命名空间生成一个 32 字节随机令牌的十六进制串见 app/electron/secureStorage.ts 中的createSecureStorageCapabilitiesconst capability crypto.randomBytes(32).toString(hex); capabilities[namespace] capability; namespaceByCapability.set(capability, namespace);渲染进程侧createPluginSecureStorage把该令牌闭包进save / load / delete三个方法中——插件拿到的对象只有key/value形参令牌随每次请求自动附加插件既不能选择命名空间也无法直接操纵令牌本身。注入时机在 frontend/src/plugin/index.ts 的插件参数构造逻辑中可以看到注入条件const storageNamespace secureStorageNamespaces[index]; const storageCapability storageNamespace ? secureStorageCapabilities[storageNamespace] : undefined; if (storageCapability secureStorageBridge) { argumentNames.push(pluginSecureStorage); argumentValues.push(createPluginSecureStorage(storageCapability, secureStorageBridge)); }即只有当插件拥有有效命名空间、且secureStorageBridgeElectron 桌面 API 桥存在时pluginSecureStorage才会出现在插件的私有参数列表里——Web 部署下该参数自然缺席。主进程 IPC 边界主进程在 app/electron/secureStorage.ts 中注册了四条 IPC 通道secure-storage-register注册命名空间、领取令牌、secure-storage-save / -load / -delete。每个操作请求都必须通过双重校验才能被接受见setupSecureStorageHandlers来源校验event.sender必须是主窗口的webContents、event.senderFrame必须是主框架且请求方文档 URL 与受信的 Headlamp 文档 URL 完全一致hash 片段除外因为 Headlamp 用 hash 做客户端路由令牌校验请求携带的 capability 必须能反查到本次页面加载注册的命名空间否则返回Invalid secure storage capability。另外主框架发生导航did-start-navigation/did-frame-navigate时已注册的令牌会被清空下一次加载必须重新注册——旧窗口的请求与令牌在新窗口一律失效。app/e2e-tests/tests/pluginSecureStorage.spec.ts 的 Electron e2e 测试就验证了这一点通过window.desktopApi.secureStorage.save(invalid-capability, ...)发起携带未知令牌的请求会被拒绝。校验规则与资源限额主进程在 app/electron/secureStorage.ts 中定义了一组硬边界超出即返回失败而不是抛异常限制项常量上限命名空间格式VALID_NAMESPACEnpm 风格包名最长 214 字符存储键格式VALID_KEY[a-z0-9_-][a-z0-9:_-]*最长 128 字符单个值大小MAX_VALUE_LENGTH64 KiBUTF-16 码元计每个命名空间的条目数MAX_ENTRIES_PER_NAMESPACE256命名空间总数MAX_NAMESPACES256存储文件总大小MAX_STORAGE_FILE_BYTES4 MiB除格式外DANGEROUS_KEYS集合明确禁止__proto__、constructor、prototype作为持久化键防止原型污染反序列化存储文件时同样只接受“命名空间键”均合法的条目畸形数据直接丢弃。加密可用性方面encryptionIsAvailable()不仅要求safeStorage.isEncryptionAvailable()还特别拒绝 Linux 下的basic_text后端即 Electron 退化为明文存储的情形此时所有操作返回Encryption unavailable而非静默降级。持久化方式与平台差异存储文件位于 Electron 的userData目录下的secure-storage.json所有值经safeStorage.encryptString加密后以 base64 字符串保存键统一为namespace:key形式。写入采用“临时文件 fsyncrename覆盖”的方式writeSecureStorageFile临时文件在 POSIX 上以0o600仅属主可读写权限创建写完先fsyncSync刷盘再重命名覆盖目标文件避免目标文件出现写了一半的状态源码注释同时诚实地说明了局限Node 并未为renameSync提供跨平台原子性保证且未做目录级fsyncWindows 上不实现 Unix 权限位文件访问控制依赖userData目录继承的 ACL内容保密性则仍由safeStorage加密保证读取或解密失败时既有密文会被保留而不是把存储视为空防止一次瞬时故障导致数据丢失。从模块文档注释看这些 IPC 处理器与文件操作是同步执行的同一 Headlamp 进程内的请求被串行化模块不提供操作系统级文件锁跨进程并发不是其设计目标Headlamp 的应用级单实例锁通常阻止第二个 Headlamp 进程。验证与安全测试除了前述 e2e 用例仓库中还配套了单元/集成测试可作为行为契约的依据app/electron/secureStorage.test.ts主进程模块的测试覆盖命名空间/键校验、限额、文件读写与安全回退逻辑app/e2e-tests/tests/pluginSecureStorage.spec.ts真实启动 Electron临时user-data-dir验证未知 capability 跨 Electron 桥被拒绝等端到端行为frontend/src/plugin/secureStorage.test.ts 对应的测试覆盖命名空间推导与令牌闭包行为。实践建议把上面的契约收敛成开发检查清单环境判断任何使用pluginSecureStorage的 UI都用Headlamp.isRunningAsApp()包起来再注册类型声明在插件内声明PluginSecureStorage接口和declare const pluginSecureStorage与 frontend/src/plugin/secureStorage.ts 的接口定义保持一致避免直接any结果三分法success: truevalue: null表示键不存在success: true 有值表示成功success: false时必须读error并显式处理绝不能当作“无值”继续走逻辑遵守限额这是为“小凭据”设计的 API单值 64 KiB、每插件 256 键、总文件 4 MiB不要把日志、缓存或大块配置塞进来选对存储位置只存本地桌面端的用户凭据需要集群可见的机密走 Kubernetes Secret这是该 API 明确划出的边界。完整 API 契约与安全模型说明见插件功能文档的 Secure Storage 章节示例代码见 plugins/examples/secure-storage/ 目录主进程实现见 app/electron/secureStorage.ts。【免费下载链接】headlampA Kubernetes web UI that is fully-featured, user-friendly and extensible项目地址: https://gitcode.com/GitHub_Trending/he/headlamp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表