把 LLM 密钥从环境变量里解放出来:OpenClaw.NET 迎来 Vault/OpenBao 密钥后端

引言:一个所有 AI 网关都绕不开的问题
自托管一个 AI 智能体网关,最让人头疼的环节往往是密钥管理:OpenAI 的 API Key 写在环境变量里,Slack 的签名密钥躺在 appsettings.json 里,数据库连接串散落在容器编排文件的 environment: 段中。环境变量方案的问题人人都清楚:进程列表可见、容器 inspect 可读、轮换要重启、权限粒度约等于零。但要改,通常意味着把每个读取密钥的调用点翻出来重构一遍。
9 月 15 日,.NET 开发者 geffzhang 向 OpenClaw.NET 提交了一个 +7317 行、57 个文件的 PR(#241,目前处于 open 状态),为这个项目带来了 HashiCorp Vault / OpenBao 密钥解析后端。它的关键设计是所有现存调用点零改动:原来写 env:OPENAI_API_KEY 的地方,现在可以直接写 vault:secret/data/openclaw/openai#api_key。
OpenClaw.NET 是一个 NativeAOT 友好的 .NET AI 智能体运行时与网关(带聊天 UI、OpenAI 兼容端点、MCP 支持、9 类消息渠道,默认监听 127.0.0.1:18789)。这篇文章从 PR 的设计和代码出发,聊它的架构取舍、安全姿态,以及它为什么是企业级 .NET AI 基础设施拼图中重要的一块。
为什么是现在:Vault 生态在 AI 基础设施中的位置
在解读代码之前,先交代一下背景。HashiCorp Vault 自 2023 年变更许可证(BUSL)后,Linux 基金会孵化的 OpenBao 接过了开源接力棒(MPL 2.0),两者 API 兼容,企业选哪个更多取决于许可证策略,而不是技术差异。在 AI 平台语境下,密钥管理的诉求被进一步放大:一个智能体网关手里的,不再只是数据库密码,还有按调用计费的模型 API Key、触达真实用户的 IM 渠道凭据、能读写企业系统的插件密钥。泄漏的代价比传统微服务时代高出一个量级。
OpenClaw.NET 此前的密钥方案与绝大多数同类项目一样:env: 引用环境变量、raw: 内嵌字面量(并明确标注"生产环境不推荐")。这套方案在个人开发者和单节点部署场景完全够用,但一旦进入"多副本、多环境、需要审计与轮换"的企业语境就立刻捉襟见肘。PR #241 补的正是这一层,而且它没有重复造轮子,直接站在了 Vault 生态成熟的 KV v2、token 认证与审计体系之上。
设计哲学:门面模式 + 可插拔提供者链
PR 的第一个决策是不动调用点。OpenClaw.NET 原有的 SecretResolver 是一个静态门面,支持 env:VAR、raw:literal 和裸串三种引用格式,全项目所有工具和网关共享这一个实现。如果直接在里面塞 Vault 客户端代码,Core 项目就会背上 VaultSharp 这个不小的依赖。而 OpenClaw.NET 的架构纪律恰恰是"Core 保持小而零依赖,厂商集成进可选扩展包"。
作者的解法是典型的依赖倒置:
Core 层(零新依赖):
SecretResolver(静态门面,向后兼容)
└─ ResolverAccessor.Current → ISecretResolver
└─ CompositeSecretResolver → ISecretProvider 链
├─ EnvRawSecretProvider(内置:env:/raw: 语义原样保留)
└─ (可选)VaultSecretProvider(扩展包注册)
扩展层(新增 OpenClaw.Security.Vault 项目):
VaultSecretProvider(Scheme = "vault",基于 VaultSharp)
VaultRefCache / VaultRefParser / VaultRefPrewarmService ...
门面的兼容处理很讲究:DI 容器完成引导后,SecretResolver.Resolve() 委托给注册的 ISecretResolver;DI 未引导(比如纯工具场景)则回退到内嵌的 legacy 逻辑,env:/raw: 行为与之前完全一致。VaultNotConfiguredException 被刻意放在 Core 层而非扩展包里,这样 fail-closed 检查可以留在解析链上,又不会造成 Core↔Vault 的循环引用。
引用语法与配置:一切从 vault: 开始
密钥引用采用 KV v2 语义的语法:
vault:<mount>/data/<path>#<key>
vault:secret/data/openclaw/openai#api_key # 标准形态
vault:openclaw/data/payments/stripe#sk_live # 自定义 mount
vault:data/config#nested_key # 省略 mount,默认 secret
配置挂在既有的 OpenClaw:Security:Vault 节下,与 ConfigValidator 校验体系打通:
{
"OpenClaw": {
"Security": {
"Vault": {
"Enabled": true,
"Address": "https://vault.example.internal:8200",
"TokenRef": "env:VAULT_TOKEN", // 注意:token 本身也走 env:/raw: 间接引用
"KvMount": "secret",
"RequestTimeout": "00:00:10",
"CacheTtl": "00:05:00",
"PrewarmRequired": true,
"Tls": { "SkipVerify": false, "CaCertPath": null }
}
}
}
}
校验器会强制一批硬性约束:Enabled=true 时 Address 必填且必须是 HTTPS;TokenRef 必填且禁止以 vault: 开头(递归防护:解析 token 不能依赖 Vault 后端自己);CacheTtl/RequestTimeout 有范围检查;公网绑定时拒绝环回 Vault 地址。坏配置在启动期就报错退出,不会留到运行时才爆雷。
Fail-Closed:拒绝静默降级
很多密钥管理集成的失败模式是"静默降级":后端连不上时把 vault:xxx 当字面量返回,于是应用拿着字符串 "vault:secret/data/..." 去调 OpenAI,报一个莫名其妙的 401,排查半天。这个 PR 的做法相反,所有失败路径都显式抛异常:
| 场景 | 行为 |
|---|---|
Vault 后端未启用却用了 vault: 引用 |
抛 VaultNotConfiguredException(绝不退化为字面量) |
| Vault 401/403 | 抛 VaultAuthException |
| 5xx / 网络故障 / 超时 | 抛 VaultUnavailableException(可重试标记);有旧缓存时回退旧值 + 告警 |
| 路径级 / 键级 404 | 分别抛 VaultPathNotFoundException / VaultKeyNotFoundException |
| 引用格式错误 | 抛 VaultRefParseException |
| 启动预热失败 | PrewarmRequired=true(默认)时启动直接失败 |
同样重要的还有"不泄露":解析出的密钥值永远不会出现在异常消息、日志、追踪或堆栈里,异常只携带路径、键名、HTTP 状态码和错误类型名,日志输出还会再过一道 RedactionPipeline。同步解析路径(Resolve)在缓存未命中时直接抛 SecretResolutionException,不会阻塞等 HTTP。同步路径永远不碰网络,冷缓存的 fetch 只能走异步路径或启动预热。这条纪律避免了一类"配置加载线程偷偷发起网络请求"的隐蔽故障。
缓存策略:TTL + 单飞 + 提前刷新 + 失败回退旧值
VaultRefCache 是这个 PR 里实现最扎实的组件,在 IMemoryCache 之上做了四件事:
- TTL 缓存(默认 5 分钟):密钥轮换在 TTL 到期后自动生效,不用重启网关;
- 单飞(single-flight):每个键一把
SemaphoreSlim,N 个并发缓存未命中只产生一次 Vault HTTP 调用; - 提前刷新(refresh-ahead):条目过期后先返回旧值,后台异步刷新;用
Refreshing标记和版本号防止并发读者各自重复触发刷新; - 失败回退(stale-on-failure):刷新失败时保留旧值并记录告警,Vault 短暂抖动不会击穿正在运行的网关。
配合 VaultRefPrewarmService(一个 IHostedService),网关在开始对外服务之前就会解析 PrewarmRefs 清单,并自动扫描整个配置树找出所有 vault: 引用提前拉取。预热并发受 RateLimit.RequestsPerSecond(默认 20)限制,预热失败默认阻断启动。
一个值得单独说说的实现细节:异常分层
VaultExceptions.cs 里定义了一组语义化异常:VaultAuthException、VaultUnavailableException(带 retryable 标记)、VaultPathNotFoundException、VaultKeyNotFoundException、VaultRefParseException。它把"运维排障语言"直接编码进了类型系统:告警系统可以只对 VaultAuthException 升级 page 人(凭据类问题几乎不会自愈),对 retryable=true 的 VaultUnavailableException 只做计数观察(网络抖动大概率自愈)。VaultSecretProvider 里的 catch 链按 VaultSharp 的 VaultApiException HTTP 状态码精确分流:401/403 归认证类,5xx 和网络/超时归不可用类,路径与键的 404 各自独立。
TLS 与边界:务实,但划线清晰
TLS 提供两个选项且互斥:Tls.SkipVerify(仅限隔离的集成/开发环境,且必须同时设置全局开关 Security.AllowInsecureTls=true 校验器才放行)和 Tls.CaCertPath(加载自定义 CA 证书包,PEM 文件或目录,作为自定义根信任,主机名校验保持强制)。证书文件缺失或损坏同样启动即失败。
有一个对部署形态影响很大的边界必须知道:NativeAOT 构建不包含 Vault 后端。VaultSharp 不是 trim-safe 的,AOT 发布物中 vault: 引用会 fail-closed 抛异常,只有 JIT 构建才包含该后端。这与 OpenClaw.NET 一贯的"AOT/JIT 能力分巷、动态面 JIT-only"的纪律一致,但意味着想用 Vault 的团队要接受 JIT 部署车道(或者等社区出现 trim-safe 的 Vault 客户端)。
顺手修了两个"前任"bug
PR 还修复了两个与 Vault 无关的存量测试失败(为了让测试套件恢复全绿):Windows 上裸名称启动 npm.cmd 会把 %~dp0 展开为工作目录、破坏 npm 的 shim,改为全路径启动;Companion 的 Avalonia UI 测试硬编码了 "\n",而 TextBox 插入的是平台换行符。全量验证结果:Windows 11 + .NET 10 下 2740 个测试全部通过,另有 5 个针对真实 OpenBao 2.0.0 容器的端到端集成测试通过。PR 还附带了 deploy/docker-compose/openbao.yml 开发编排文件和一个可选的 CI vault-integration job,以及中英双语文档,完成度相当高。
视角放大:这块拼图意味着什么
把视角拉远一点。我们此前分析 Nacos AI Registry × OpenClaw.NET 融合方案时,配置样例里 Nacos 凭据写的是 "Password": "env:NACOS_PASSWORD"。这在当时已是最佳实践,但密钥本质上仍由容器环境变量承载。有了这个 PR,同一位置可以写成 vault:secret/data/openclaw/nacos#password:密钥集中到 Vault 审计、轮换免重启、节点不再持有明文。对正在用 Nacos 做 AI 资源控制面、用 OpenClaw.NET 做 .NET 数据面的团队来说,密钥治理这块短板就此补齐,控制面(Nacos)、运行时(OpenClaw.NET)、密钥面(Vault/OpenBao)各就各位。
OpenBao 是 Vault 的 Linux 基金会开源分支,对许可证敏感的企业可以直接替换。PR 作者的集成测试就跑在 OpenBao 容器上,倾向已经很明显。
结语
PR #241 落地企业级功能的方式:Core 只加抽象不加依赖、现存调用点零改动、所有失败路径 fail-closed、密钥值全程不进可观测面、配置在启动期校验、测试与文档齐备。如果要从中挑一个最值得借鉴的点,我会选"拒绝静默降级"这一整条线索:从 vault: 引用在禁用状态下显式抛错,到预热失败阻断启动,再到同步路径拒绝阻塞网络。每一处的思路都一样:把故障暴露得更早、更响,线上就更少出现隐蔽事故。
它尚未合并(review 仍在进行),但其设计本身已经值得每个在 .NET 上构建 AI 基础设施的团队参考。哪怕你不用 OpenClaw.NET,"门面 + 提供者链 + fail-closed + 启动预热"这套模式也可以直接搬到任何需要对接 Vault 的系统里。密钥管理做得好不好,常常决定了 AI 平台是玩具还是生产系统。PR #241 把 OpenClaw.NET 往生产这一侧推了一步。
项目地址:github.com/clawdotnet/openclaw.net | PR:#241 Secrectprovider | 文档:docs/security/vault.md(中英双语)
欢迎大家扫描下面二维码成为我的客户,扶你上云

浙公网安备 33010602011771号