上个周末帮一位朋友排查 OpenClaw 实例的 401 报错,打开他的项目目录时差点没绷住:API Key 就明文躺在config文件里,而且这个项目昨天刚被他推到 Git 仓库。更麻烦的是他用了中转网关,密钥权限范围还是全量账号,等于把主钥匙挂在门口,然后自己出门旅行了。
这篇文章只聊一件事:在 OpenClaw 里,API 密钥到底该怎么安全地存放、加载、轮换。我默认你已经在 Ubuntu、Windows(WSL2) 或者阿里云服务器上部署过 OpenClaw,并且接了 OpenAI 兼容接口、云端模型 API,或者正在本地跑 Ollama(qwen2.5-3b 这类模型)。如果你只是本地玩一玩 Ollama,后面权限治理的部分同样值得看完,因为密钥管理习惯是一通百通的。
1. OpenClaw 的密钥入口都在哪里"裸奔"
1.1 配置文件中最常见的三种明文写法
OpenClaw 本身是高度配置驱动的,很多人的第一版配置都是从网上抄的 Demo 改出来的,于是密钥存放方式也跟着踩进了同一个坑。我先列三种最常见的明文写法,你可以现在就去检查自己的配置。
第一种是把密钥直接写在agent或model配置块里。比如:
model: provider: openai_compatible api_key: sk-xxxxxxxxxxxxxxxx base_url: https://api.example.com/v1第二种是写在.env文件里,但没注意格式问题,导致启动时整份文件被当作普通文本加载,甚至被日志输出。注意.env文件里面如果值包含特殊字符(#、空格、引号),却又没有正确加引号或转义,解析出来的值就是错的,看起来像"密钥没问题但一直鉴权失败"。
第三种是写在某个公开的setup、install、test脚本里,随手被提交到仓库。很多人部署完 OpenClaw 后会顺手写一个test_api.py用来验证模型连通性,密钥直接硬编码在里面。这个文件一旦被 push 到 GitHub,不管仓库是 public 还是 private,都属于高风险暴露。
1.2 密钥一旦泄露会发生什么
我见过的最小代价是账号被限流:有人拿你的 Key 去跑批量任务,服务商检测到异常直接封停。中等代价是账单暴涨:云端 API 按 token 计费,一个开放的 key 足够把月账单打到让人心疼的程度。最麻烦的代价是供应链污染:如果 OpenClaw 实例里接入了可执行工具链,泄露的密钥可能被别人用来反向探测你服务器上的其他服务,甚至修改你的模型配置指向恶意网关。
密钥泄露这事,最坑的点在于它不会立刻爆发。很多人发现密钥被滥用,已经是几天后收到账单或者日志里出现陌生调用记录的时候了。所以管理密钥的第一原则不是"出了事怎么补救",而是"从一开始就不要让它处于可以被轻易拿走的状态"。
2. OpenClaw 取钥方案:从环境变量到系统密钥库
2.1 环境变量是最简单也最通用的起点
OpenClaw 的配置体系里,我推荐的第一层方案是环境变量。环境变量就像给门配的钥匙孔:你不把钥匙挂在门上,而是在要开门的时候从口袋里掏出来。程序运行时从进程环境里读取密钥,配置文件里只留一个变量名。
在 OpenClaw 里最常见的一个做法是:
export OPENCLAW_OPENAI_API_KEY="sk-xxxx"然后在 YAML 配置里:
model: provider: openai_compatible api_key: ${OPENCLAW_OPENAI_API_KEY}这里的${...}占位符是 OpenClaw 配置加载器支持的环境变量引用语法。如果你用的是某个特定版本或分支,建议先查一下项目的 docs 确认占位符写法,但绝大多数二十年前之后的实现都支持类似语法。
为什么第一步一定是环境变量?因为它跨平台、不需要额外装服务、对新手最友好。Windows 下用 WSL2 跑 OpenClaw,环境变量可以直接写在~/.bashrc或~/.profile里;Ubuntu 服务器上写成export语句或/etc/profile.d/openclaw.sh;用 systemd 托管 OpenClaw 服务时,在 service 文件里指定EnvironmentFile指向一个权限收紧的密钥文件。这三种方式都不需要改 OpenClaw 代码,只是把密钥从"配置文件的可见明文"变成"进程空间的私有数据"。
2.2 OpenClaw 配置中引用密钥的推荐方式
不要小看配置格式的细节。YAML 里写${VAR}和写"${VAR}"有时候结果不一样,尤其是密钥值里带着#或冒号时。我吃过一次亏:某个网关的 key 末尾带了一个=,在无引号的 YAML 值里解析时被截断,导致每次调用都在下午某个固定时间点报错,排查了很久才发现是配置解析问题。
推荐的最小配置模板长这样:
model: provider: openai_compatible api_key: "${OPENCLAW_OPENAI_API_KEY}" base_url: "${OPENCLAW_API_BASE_URL}"注意值用双引号包起来。双引号在 YAML 里允许环境变量经过加载器替换,同时能保留特殊字符的完整性。单引号则是字面量,加载器不会替换,因此如果你写了'${OPENCLAW_OPENAI_API_KEY}',OpenClaw 会把那个占位符当字符串直接传给 API 网关,然后得到一个诡异的鉴权失败。
如果 OpenClaw 实例有多个 agent 或多个模型接入,不要把每个模型的 key 都单独写在config里。最省心的做法是定义一个密钥环境变量清单,统一注入。我自己的习惯是维护一份env.example:
# 复制为 .env 后填入真实值,且 .env 必须进 .gitignore OPENCLAW_OPENAI_API_KEY= OPENCLAW_OPENAI_BASE_URL=https://api.example.com/v1 OPENCLAW_OLLAMA_HOST=http://localhost:11434这样新同事或新环境配置时,只需要复制模板、填真实值,不需要逐个翻代码找哪里用了 key。
2.3 上生产后用系统密钥库
环境变量虽然解决了明文问题,但它还停留在"只要拿到服务器权限就能读走"的层面。如果你把 OpenClaw 部署在多人协作的服务器上,或打算跑更正经的业务,建议上系统级密钥库。
Linux 上可以用 Secret Service(通过secret-tool或libsecret访问),macOS 用 Keychain,Windows 上可以用凭据管理器。OpenClaw 不一定有内置的密钥库适配层,但你可以通过一层薄薄的包装脚本实现:启动 OpenClaw 前,脚本从系统密钥库取出密钥并注入环境变量,然后进程正常启动。这样密钥不会落盘成任何明文文件,即使有人拿到服务器的文件系统权限,也拿不到有效密钥。
我实际用过的方案是 KeePassXC 加keepassxc-cli,在 Ubuntu 服务器上跑一个 systemd service:
# /etc/systemd/system/openclaw.service 的部分内容 [Service] EnvironmentFile=/etc/openclaw/env-from-secrets ExecStart=/opt/openclaw/bin/openclaw startenv-from-secrets是一个权限为 600 的临时文件,由另一个定时任务从 KeePassXC 数据库导出生成。这个方案不算优雅,但胜在稳定,而且密钥轮换只改数据库里的一个条目就能批量更新。
2.4 .env 文件的自我保护
如果你暂时不想上系统密钥库,至少要把.env文件保护好。三个不要忘:
第一,.env必须写进.gitignore。这听起来像废话,但我真的见过有人把.env改成env.config之后忘记更新.gitignore,然后密钥被提交上去的例子。第二,文件权限要收紧,chmod 600 .env,避免同服务器的其他用户cat直接读走。第三,.env文件不要放在 OpenClaw 的 web 静态目录下,因为一旦 Web 服务配置了静态文件路由,.env可能被直接下载。
3. WSL2 环境下密钥加载失败:一个完整排查链路
3.1 现象:OpenClaw 启动时提示找不到密钥
有阵子不少人反映,在 Windows 上通过 WSL2 跑 OpenClaw,装好之后启动直接报错,提示环境有问题,还建议在 PowerShell 里运行wsl --status确认发行版状态。这个提示本质上是在确认 WSL2 环境是否完好,但很多人卡住的真实原因并不是 WSL 本身坏了,而是密钥加载的作用域不对。
典型的报错包括:
OPENCLAW_OPENAI_API_KEY is not set, please check your environment或者:
Error: Invalid api keyInvalid api key有时候是 key 本身错了,但有相当高的比例是程序读到的 key 是空字符串,或者是在 YAML 解析阶段把${...}留成了字面量。
3.2 排查链路:从 wsl --status 到进程环境变量
我先说排查的正确顺序。假设报错是"找不到环境变量",不要先去改 OpenClaw 配置文件的格式,而是按这个链路一步步查:
第一步,在 Windows PowerShell 里运行wsl --status,确认 WSL2 发行版本正常。这一步只是验证底层环境没问题,不是最终诊断。
第二步,进入 WSL 终端,运行echo $OPENCLAW_OPENAI_API_KEY,看看当前 shell 里能不能拿到值。如果你发现这里打印出来是空的,说明环境变量根本没有写进 WSL 侧的 shell 配置。
第三步,确认 OpenClaw 是从哪个进程启动的。如果你是在 PowerShell 里直接敲wsl openclaw start,那么环境变量可能来自 Windows 用户环境变量,而不是 WSL 的.bashrc。WSL2 默认会继承 Windows 的用户环境变量,但继承逻辑有坑:如果你只在 Windows 上设置了变量,而 WSL 侧也有同名的空变量,那 WSL 的空变量会覆盖继承值。
第四步,打印一下 OpenClaw 实际读到的值。用调试模式或加一行打印,比如在 Python 里跑:
import os print(repr(os.getenv("OPENCLAW_OPENAI_API_KEY")))这一步能立刻区分是"环境变量不存在"还是"环境变量存在但值是空/错的"。
第五步,如果确认环境变量存在但 OpenClaw 读取失败,把 zig 或 node 的配置加载逻辑拉到边上查一下:OpenClaw 到底是从EnvironmentFile读,还是只读启动进程的environ,还是只读 YAML 内嵌文字。这三种情况对应的修复方式完全不同。
3.3 这个坑为什么容易踩
根源在于双环境变量作用域。Windows 侧设置了 key,WSL 侧没设置,程序跑起来时应该能读到继承值;可一旦 WSL 侧的启动脚本里有一行unset OPENCLAW_OPENAI_API_KEY或者export OPENCLAW_OPENAI_API_KEY="",继承就被掐断了。很多人为了隔离环境变量,会在~/.bashrc里手写一堆 export,结果把自己坑了。
另一个隐藏很深的问题是换行符。Windows 下用记事本编辑.env,文件换行是\r\n,WSL 环境下读出来的值末尾会带一个\r。密钥值尾部多一个不可见字符,服务端验签必然失败。你会在日志里看到401 invalid_api_key,但肉眼完全看不出密钥哪里错了。
我的建议是:在 WSL 里部署 OpenClaw,就用 WSL 的~/.bashrc统一管理所有密钥相关变量,不要依赖 Windows 系统环境变量继承。Windows 侧最多留一个WSLENV开关专门放需要跨系统共享的变量,其余全部在 WSL 内定义,能少踩很多坑。
4. 接本地 Ollama 和云端 API 时的密钥配置差异
4.1 本地 Ollama 模型:密钥最小化原则
很多人跑 OpenClaw 是为了把 Obsidian 笔记或本地知识库关联到模型上,比如用 qwen2.5-3b 这类中小参数模型做本地总结。这时候你根本不需要 API 密钥,因为 Ollama 走的是本地 HTTP 接口。
正确的配置方式是让 OpenClaw 直连本地 Ollama:
model: provider: ollama base_url: http://localhost:11434 model: qwen2.5-3b注意两点。第一,base_url不要写成https://api.ollama.com,那是云端服务,需要额外鉴权。第二,如果 OpenClaw 和 Ollama 不在同一台机器上,比如 OpenClaw 在 WSL2、Ollama 在 Windows 原生侧,你可能会需要把localhost改成 WSL 访问 Windows 宿主机的网关 IP。这种情况下仍然不需要密钥,但需要带宽口和防火墙放行。
密钥最小化原则在这里的体现是:能不用密钥就别用。本地模型场景下,设一个OPENCLAW_OLLAMA_HOST环境变量就够了,千万不要为了"统一"给本地模型也配一个假的 api_key。
4.2 云端 API 接入:OpenClaw 配置示例
如果你打算用 OpenClaw 接云端模型,比如 OpenAI 兼容接口或各类中转网关,配置就变成了"密钥 + 接口地址 + 模型名"三件套。OpenClaw 通常要求你同时指定provider和base_url。举个例子:
model: provider: openai_compatible api_key: "${OPENAI_API_KEY}" base_url: "${OPENAI_BASE_URL}" model: qwen-plus这里的核心是base_url和api_key必须配套。如果你把某个中转网关的密钥填到了官方 OpenAI 的接口地址上,大概率会得到跨域或鉴权不匹配的错误。很多人只改了模型名,没改base_url,反复报错后跑来问"为什么 key 明明对但连不上",一查才发现是地址和密钥的服务商不一致。
4.3 多服务商场景下的密钥组织
当 OpenClaw 里同时接入多家服务商时,密钥的组织就变得重要了。我习惯用一张表把服务和密钥变量名理清楚,写到部署文档里,比如:
| 用途 | 服务商 / 网关 | 环境变量名 | 接口地址示例 |
|---|---|---|---|
| 主对话模型 | OpenAI 兼容网关A | OPENCLAW_GW_A_KEY | https://gw-a.example.com/v1 |
| 备用对话模型 | OpenAI 兼容网关B | OPENCLAW_GW_B_KEY | https://gw-b.example.com/v1 |
| 本地模型 | Ollama | 不需要 | http://localhost:11434 |
在.env里存这些变量时,要注意 key 名别起得太随意,比如KEY1、KEY2这种,时间一长根本记不住哪个对应哪个。我用过最省心的规则是服务名_用途_KEY,后缀一律_KEY,脚本处理和人工排查都方便。
5. 密钥轮换、权限收紧与日志治理的进阶做法
5.1 文件系统级别的权限收紧
如果 OpenClaw 所在的服务器上不只有你一个用户,权限设置就不能只靠"别人不会看"的自觉。一个完整的最小权限方案至少包含这几步:
- OpenClaw 专属运行用户,例如
openclaw,不给他 shell 权限。 .env或密钥导出文件的属主设为openclaw,权限 600。- 配置文件目录权限设成
700,其他用户无法ls。 - 托管的 systemd service 里指定
User=openclaw,防止 OpenClaw 进程以 root 身份运行。
很多人部署时图省事直接用 root 跑 OpenClaw,这会让密钥管理的意义大打折扣。一旦服务被攻破,攻击者拿到的直接是 root 可以读取的所有密钥,而不是受限用户权限下的一小部分。
5.2 Git 历史泄露后的应急处理
如果密钥已经进了 Git 历史,单纯删除文件、再 commit 一次是不够的。Git 的历史记录里仍然保留着旧版本,任何人都可以翻出来。标准做法是:
- 立即在服务商后台吊销泄露的密钥,生成新密钥。
- 修改
.env和 OpenClaw 配置,换上新密钥。 - 处理 Git 历史,用
git filter-repo重写历史,把包含密钥的提交清掉。 - 强制推送并通知所有协作者重新 clone。
- 如果仓库已经在 GitHub/GitLab 上公开,还要去平台的安全告警页面确认有没有被扫描到。
这里我想特别强调第一步最优先。很多人先花两小时清理 Git 历史,清完之后才去吊销密钥,结果在清理过程中密钥一直处于有效状态,等于把门开着然后慢慢擦脚印。正确顺序永远是先吊销、再清理。
5.3 定期轮换的实施清单
密钥轮换这件事说多了都是泪,因为真的容易懒。我的做法是把它变成一台每周自动运行的脚本:
#!/usr/bin/env bash # 每周从密钥库生成新 key 并重启 OpenClaw NEW_KEY=$(generate_new_key) secret-tool store --label=openclaw service openclaw key "$NEW_KEY" /opt/openclaw/bin/openclaw restart脚本本身不打印NEW_KEY的值,日志里只记录轮换时间和结果。服务商后台手动操作部分,比如把新 key 加进白名单,还是得人工确认。用这种方式,即使某个 key 在日志里泄露了,它的有效期也只剩几天,而不是无限期有效。
轮换时最容易漏掉的是"备用线程"。很多人只更新了 OpenClaw 主配置,忘了还有定时脚本、监控脚本、CI 工作流里引用了旧 key。建议轮换之前先全局搜索一下仓库和服务器上的OPENCLAW_相关变量,确认所有引用点都一起更新。
5.4 阿里云服务器部署时的额外注意
如果你在阿里云服务器上用 OpenClaw,密钥管理还要叠加云环境的安全习惯。至少注意三点:
第一,不要用主账号 AccessKey 作为运行凭据,尽量用 RAM 子账号,权限范围收敛到最小。OpenClaw 本身的 API 密钥同理,如果服务商支持子密钥/工作空间隔离,就申请一个专用子密钥而不是拿账号主密钥到处填。
第二,安全组入方向规则尽量收紧。OpenClaw 的管理端口不要直接暴露公网,如果需要通过本机管理,可以用 SSH 隧道或者只允许特定 IP 访问。密钥是鉴权凭证,端口暴露是入口敞口,两者要联动治理。
第三,服务器上的操作日志要保留。bash_history、systemd journal、OpenClaw 运行日志这些都值得开启。真出事的时候,日志能帮你快速判断密钥是在哪一步被读走的。
5.5 日志与调试输出里的小动作
最后聊一个隐蔽但常见的泄露入口:日志。OpenClaw 在 debug 模式下有时候会把请求头里的Authorization一并打印出来,密钥就这么进了日志文件。如果日志收集系统(比如你挂了 Loki、ELK 或云日志服务)没有做脱敏,密钥就等于间接公开。
我的习惯是在日志配置里加一层过滤规则,把Authorization、api_key、key这些字段替换成***。如果用 systemd journal,还可以在 catch-all 阶段做一次输出流过滤,避免调试代码不小心把敏感信息打出来。另外一个笨办法是给日志文件设短轮转周期,比如按天轮转、保留三天,降低敏感信息在磁盘上的滞留时间。
提示:任何时候不要为了调试方便在公开渠道贴出 OpenClaw 的完整配置,即使你手动替换了 key 字符串。配置里的
base_url、model、认证方式等元信息同样会帮助攻击者定向构造攻击载荷。
我自己的习惯,是在每台部署 OpenClaw 的服务器上放一份README-SECRET.md,里面写清楚三件事:密钥存在哪、怎么轮换、出事后第一步干什么。别小看这份文档,很多密钥灾难都是因为"当初配置的人不在现场,接手的人不知道密钥藏在哪个角落"。把流程文档化,比记住任何一招技巧都管用。