惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Microsoft Azure Blog
Microsoft Azure Blog
有赞技术团队
有赞技术团队
IT之家
IT之家
博客园 - 聂微东
Jina AI
Jina AI
Hugging Face - Blog
Hugging Face - Blog
Last Week in AI
Last Week in AI
Apple Machine Learning Research
Apple Machine Learning Research
WordPress大学
WordPress大学
小众软件
小众软件
爱范儿
爱范儿
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
Visual Studio Blog
雷峰网
雷峰网
酷 壳 – CoolShell
酷 壳 – CoolShell
阮一峰的网络日志
阮一峰的网络日志
宝玉的分享
宝玉的分享
博客园 - 三生石上(FineUI控件)
大猫的无限游戏
大猫的无限游戏
博客园 - Franky
量子位
月光博客
月光博客
博客园 - 【当耐特】
博客园 - 叶小钗

博客园 - 张善友

.NET 11 性能深度解读:这一次,真的「到 11」了 RedNb.Nacos 2.0.0 正式发布:.NET 接入 Nacos 3.2.4,AI Registry 全能力落地 Rust 成为微软一线语言之后:谈谈 C# 与 Rust 的互补性 .NET 异常处理的"暗门":代码里写满 catch,你依然能抓住它——从一个 AI Agent 运行时的源码说起 写给 C++ 工程师的 OpenClaw.NET 上手指南:用你熟悉的 C++ 思维,跑起一个生产级 AI Agent .NET 11 RC1 发布:拿到"准生证",生产环境可以上了! 写给 PHP 工程师的 OpenClaw.NET 上手指南:用你熟悉的 PHP 思维,跑起一个生产级 AI Agent 写给 Rust 工程师的 OpenClaw.NET 上手指南:用你熟悉的 Rust 思维,跑起一个生产级 AI Agent 从对标 Java 到对标 Go:Native AOT 的"无痛化"之路,走到哪一站了? NuGet 半年度总结:周下载量从 54 亿到 67 亿,.NET 生态的"新一轮增长期"实锤了 写给 Java 工程师的 OpenClaw.NET 上手指南:用你熟悉的 Spring 思维,跑起一个生产级 AI Agent 写给 TypeScript 工程师的 OpenClaw.NET 上手指南:用你熟悉的 TS 思维,跑起一个生产级 AI Agent 写给 Python 工程师的 OpenClaw.NET 上手指南:用你熟悉的 Python 思维,跑起一个生产级 AI Agent 写给 Golang 工程师的 OpenClaw.NET 上手指南:用你熟悉的 Go 思维,跑起一个生产级 AI Agent MetaSkill 落地 .NET:当 Agent 从「调用工具」进化到「组织工具」 都是 AI 写代码,为什么 C# 比 Java 快半拍 MHS 三部曲(下):谁允许 AI 行动?——权力、合规与中国厂商的答卷 弱模型不能裸奔:Agent Harness 凭什么真实有效 MHS 三部曲(中):8 小时集成、六种被拦截的故障,和一次教科书级的翻车 TensorSharp 3.3.0.0 发布,视频生成、DFlash2 投机解码、安全加固一起来了 MHS 三部曲(上):别急着叫它「物理 MCP」——Anthropic 到底发布了什么 编程语言的「第三条道路」上,走得最远的其实是 C# 纯 .NET 手写 CUDA kernel,GLM-5.3-Flash decode 跑出 llama.cpp 的 2 倍 3 张卡到底能不能跑大模型推理?从 vLLM、llama.cpp 到 TensorSharp 的多卡真相 AI 编程时代,.NET 的机会在哪里? Vibe Coding 月提交量 29 亿次之后:GitHub 的危机、Azure 迁移,以及 .NET 的机会 从 PostgreSQL 到 Kubernetes:开源的护城河,从来不写在代码里 人工智能最先替代的,是人工智能学院自己 企业架构的六种场景:从"四大流派"到数字原生与 AI 原生 TensorSharp 最新进展研究报告:从 DeepSeek V4 Flash 到 GLM-5.2,以及等待中的 GLM-5.3
把 LLM 密钥从环境变量里解放出来:OpenClaw.NET 迎来 Vault/...
张善友 · 2026-09-16 · via 博客园 - 张善友

pr241-vault-architecture

引言:一个所有 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:VARraw: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=trueAddress 必填且必须是 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 里定义了一组语义化异常:VaultAuthExceptionVaultUnavailableException(带 retryable 标记)、VaultPathNotFoundExceptionVaultKeyNotFoundExceptionVaultRefParseException。它把"运维排障语言"直接编码进了类型系统:告警系统可以只对 VaultAuthException 升级 page 人(凭据类问题几乎不会自愈),对 retryable=trueVaultUnavailableException 只做计数观察(网络抖动大概率自愈)。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(中英双语)