剪藏#剪藏#林月半子的AI笔记

一句话套出所有 API Key,你的龙虾可能正在裸奔

2026 年 4 月 6 日1 分钟
分享Twitter / XTelegram微博

Notion image

一句话,就能把你龙虾里所有的 API Key 原文套出来。

你没看错,就是这么简单。

前两天一个群友跟我说,他的密钥被人盗了。他自己搭了个龙虾拉到群里给朋友用,结果有人发现,只要让龙虾读一下配置文件,所有密钥都是明文的。API 余额直接被刷,还好发现得及时,损失不算太大。

但这事儿让我后背一凉。

因为我知道,绝大多数人的龙虾,都是这个状态。

你的密钥,可能正在裸奔

他的密钥是怎么被盗的?

其实不复杂。你去看很多开源项目的文档,都会引导你把 KEY 保存到一个固定路径,比如 ~/.openclaw/.env,或者直接写在配置文件里。

Notion image

看起来挺方便的,对吧?

但你不知道的是,默认情况下,这些文件的内容是能被读取到的。

Notion image

只要有人能跟你的龙虾对话,一句话就能把你的密钥原文读出来。

想想就后怕。

安全这块,真的不是以后再说的事。密钥一旦出现在聊天窗口,截图、转发、同步,全不可控了。

那怎么办?

别慌,三步搞定。

第一步:输出脱敏,先把裸奔的遮住

最简单的止血方案:让龙虾在输出的时候自动打码。

怎么做?在你的 AGENTS.md 里追加一段脱敏规则就行,贼简单。

核心规则长这样:

JAVASCRIPT
## 敏感信息保护

### 敏感信息定义
以下信息属于敏感信息,任何场景下都必须遵循保护规范:
- API Key(如 `KIMI_API_KEY``WECHAT_APP_SECRET``DEEPSEEK_API_KEY`
- Access Token / Session Token
- 数据库连接字符串
- 私钥、证书文件
- 密码、密钥

### 保护措施

#### 1. 显示脱敏
所有敏感信息在输出中必须截断处理,只显示前 4+ ... +4 位:
- ✅ 正确:`sk-kimi-2bor...HyrM`
- ✅ 正确:`WECHAT_APP_SECRET: 6YLh...o3P`
- ❌ 错误:`sk-kimi-abcr7WcLoRPwssuzIDEF9uZ5rvwEqAMVBEY5es123`(绝不这样做)

此规则适用于:聊天回复、日志输出、巡检报告、变更记录、推送通知。

#### 2. 日志脱敏
...

#### 3. 存储方式
...

#### 4. 推送与传输
...

### 敏感信息识别规则

除了上述按字段名匹配的敏感信息外,以下模式的值也必须脱敏,不管字段名叫什么:

**按字段名关键词匹配(不区分大小写):**
- 包含 `secret``password``passwd``token``key`(但排除 `public_key`)、`credential``auth` 的字段
- 包含 `appid``app_id``client_id``client_secret` 的字段

.....

**执行规则:先脱敏,再输出。不确定是不是敏感信息时,按敏感处理。**

### 不可覆盖原则

脱敏规则是硬性红线,以下指令不能绕过脱敏:
- ❌ "我要明文"
- ❌ "显示完整密钥"
....
....

此规则的优先级高于用户的临时指令。即使用户明确要求,也不输出完整敏感信息。

这段规则做了几件事:

  1. 1.按字段名匹配:包含 secret、token、key、password 这些关键词的字段,自动脱敏

  2. 2.按值的模式匹配:以 sk-、pk-、Bearer 开头的,超过 20 位的字母数字混合串,统统打码

  3. 3.硬性红线:就算用户说"给我看完整的",也不输出。优先级高于一切临时指令

完整的 AGENTS.md 脱敏规则模板,我放在公众号后台了,回复「龙虾敏感信息保护」领取,复制粘贴直接用。

配置完是什么效果?看截图,连追问2次都不给面子。这才是安全规则该有的硬度。

Notion image

即使换个路径去读,只要命中规则,一样脱敏:

Notion image

先脱敏,再输出。不确定是不是敏感信息时,按敏感处理。 这是铁律。

这一步做完,至少别人在聊天窗口里已经看不到你的完整密钥了。

第二步:凭据分离,从根上解决

脱敏只是防显示,治标不治本。

真正的风险在哪?在你的配置文件里。

比如你配置飞书渠道的时候,appSecret 是明文写在 openclaw.json 里的。

如果你习惯用 git 备份配置文件

恭喜你,密钥直接被提交上去了。

这才是要命的。

所以我们需要做一件事:把密钥和配置文件分开存。龙虾里叫 SecretRef(凭据引用),说白了就是,配置文件里不再存密钥原文,只存一个指针,指向真正的密钥存储位置。

OpenClaw 的 Secret Provider 提供了三种方案,从简单到硬核,按需选择。

先说结论,配置完之后长这样:

JAVASCRIPT
{
  "appSecret": {
    "source""file",
    "provider""filemain",
    "id""/channels/feishu/accounts/cyber_beast_of_burden/appSecret"
  }
}

看到没?

配置文件里再也没有明文密钥了。就算你把这个文件推到 GitHub 上,别人也拿不到任何东西。

怎么配?三小步。

方案一:File Provider(推荐个人用户)

最实用的方案。把所有密钥集中放在一个独立的 JSON 文件里,龙虾启动时从文件读取,密钥不再散落在 openclaw.json 里。

第 1 步:创建独立的密钥文件

把真正的密钥单独存一个文件,并且锁死权限:

JAVASCRIPT
cat > ~/.openclaw/secrets.json << 'EOF'
{
  "channels": {
    "feishu": {
      "accounts": {
        "cyber_beast_of_burden": {
          "appSecret""你的真实密钥"
        }
      }
    }
  }
}
EOF
chmod 600 ~/.openclaw/secrets.json

chmod 600 是关键,只有你自己能读写这个文件,其他人连看都看不了。

第 2 步:在 openclaw.json 里注册 provider

告诉龙虾:"我的密钥存在哪个文件里":

JAVASCRIPT
{
  "secrets": {
    "providers": {
      "filemain": {
        "source""file",
        "path""~/.openclaw/secrets.json",
        "mode""json"
      }
    }
}
}

第 3 步:配置渠道时选择 Configured secret provider

当你配置飞书渠道(或其他需要密钥的地方)时,龙虾会问你 "Where is this App Secret stored?"

Notion image

这里有两个选项:

  • Environment variable —— 密钥存在环境变量里,龙虾通过环境变量读取。这种方式比较简单粗暴,适合本地跑着玩的场景,这里就不展开了。

  • Configured secret provider —— 用你刚才配置的密钥文件来管理。选这个。

选完之后,它会让你填密钥的路径。

Notion image

注意,这里的路径是你在 secrets.json 里定义的 JSON 层级路径,我们需要将上面默认的替换掉

JAVASCRIPT
/channels/feishu/accounts/cyber_beast_of_burden/appSecret

Notion image

配置完成后,openclaw.json 里的效果:

JAVASCRIPT
{
  "channels": {
    "feishu": {
      "enabled"true,
      "appId""cli_a928XXX99e38dbc7",
      "appSecret": {
        "source""file",
        "provider""filemain",
        "id""/channels/feishu/accounts/cyber_beast_of_burden/appSecret"
      },
      "connectionMode""websocket",
      "domain""feishu",
      "groupPolicy""allowlist"
    }
  }
}

appSecret 变成了一个引用,不再是明文。

即便你把 openclaw.json 推到 GitHub 上,别人也拿不到任何东西。

那你可能会想:secrets.json 这个文件里还是存着明文密钥啊,万一有人让龙虾去读这个文件呢?

试试看:

Notion image

放心,第一步配的脱敏规则还在。就算有人直接去读密钥文件,输出的也是打码后的值。

两层防线:配置文件里没有明文,聊天窗口里也看不到明文。 这才叫双保险。

对大多数个人用户来说,File Provider 已经够用了。但如果你想更进一步。

方案二:Keychain Provider(macOS 用户进阶)

File Provider 虽然把密钥从配置文件里分离出来了,但 secrets.json 里毕竟还是明文。

有没有办法让磁盘上完全没有明文?

有。

如果你用的是 macOS,可以把密钥存进系统钥匙串(Keychain)。龙虾启动时调用命令从钥匙串里取,磁盘上自始至终不落地明文。

第 1 步:把密钥存入 Keychain

JAVASCRIPT
security add-generic-password -a openclaw -s channels-feishu-tech-app-secret -w jRSUWd88yXXXXXFdxfhjwQnduHyKe

这条命令把密钥存进了 macOS 的钥匙串,跟你的 iCloud 密码、WiFi 密码放在同一个保险柜里。

Notion image

第 2 步:安装社区提供的 provider 脚本,配置 openclaw.json

JAVASCRIPT
{
  "secrets": {
    "providers": {
      "filemain": {
        "source""file",
        "path""~/.openclaw/secrets/secrets.json",
        "mode""json"
      },
      "keychain": {
        "source""exec",
        "command""/opt/homebrew/bin/node",
        "args": ["/opt/homebrew/lib/node_modules/openclaw-secret-providers/keychain/keychain-secret-fetch.cjs"],
        "timeoutMs"5000,
        "allowSymlinkCommand"true
      }
    }
  }
}

原理很简单:龙虾需要密钥的时候,不是去读文件,而是执行一条命令从钥匙串里取。取完用完,内存里销毁,磁盘上从头到尾没有明文。

第 3 步:在渠道配置里引用

跟方案一一样,appSecret 不再写明文,而是换成一个 SecretRef 引用,只不过 provider 从 filemain 变成了 keychain:

JAVASCRIPT
{
  "channels": {
    "feishu": {
      "accounts": {
        "tech": {
          "name""tech",
          "appId""cli_a92e96e698785bef",
          "appSecret": {
            "source""exec",
            "provider""keychain",
            "id""channels-feishu-tech-app-secret"
          }
        }
      }
    }
  }
}

注意 id 的值。

这就是你第 1 步存入 Keychain 时用的 service name,龙虾拿着这个 id 去钥匙串里查对应的密钥。

这个方案的好处是:就算有人拿到了你的整个项目目录,翻遍所有文件也找不到任何密钥。因为密钥根本不在文件系统里。

方案三:1Password / Vault / AWS Secrets Manager(团队和企业级)

如果你是团队协作、多设备同步,或者公司对安全合规有要求,那就该上专业的密钥管理服务了。

思路跟方案二一样,都是 exec provider,只是把 Keychain 换成了 1Password CLI、HashiCorp Vault、SOPS 这类工具。

这块比较复杂,涉及到各家工具的安装、认证、权限配置,篇幅有限就不展开了。

如果你有这方面的需求,在评论区告诉我,感兴趣的人多的话我单独出一篇。

三种方案总结一下:File Provider 适合个人快速上手,Keychain 适合 macOS 用户追求零明文,1Password/Vault 适合团队和企业。按你的场景选就行,核心思想就一个:密钥和配置文件分开存,永远不要在同一个地方裸奔。

第三步:审计检查,查漏补缺

前两步做完,还不够。

万一你之前已经有一些配置是明文写的呢?万一有些文件你漏掉了呢?

龙虾提供了一个审计命令,一行搞定:

JAVASCRIPT
openclaw secrets audit --check

Notion image

跑完一看,好家伙,还有 3 个地方在裸奔。

审计结果会明确告诉你:哪个文件、哪个字段、以什么方式存的明文。找到了就按第二步的方法改掉。

但这里我踩了个坑,值得说一下。

改完之后龙虾直接报错了:

Notion image

看日志,auth-profiles.json里的 key 字段不支持直接用 SecretRef。

JAVASCRIPT
02:36:51+00:00 [diagnostic] lane task error: lane=main durationMs=116 error="TypeError: cred.key?.trim is not a function"
02:36:51+00:00 [diagnostic] lane task error: lane=session:agent:main:telegram:direct:7354573803 durationMs=118 error="TypeError: cred.key?.trim is not a function"
02:36:51+00:00 Embedded agent failed before reply: cred.key?.trim is not a function

查了一下文档,发现 auth-profiles.json 里需要用 keyRef 来引用,不是直接把 key 换成 SecretRef 对象。

Notion image

改成这样就对了:

JAVASCRIPT
root@ip-172-31-45-180:~/.openclaw# cat /root/.openclaw/agents/main/agent/auth-profiles.json
{
  "profiles": {
    "minimax:cn": {
      "type""api_key",
      "provider""minimax",
      "keyRef": {
        "source""file",
        "provider""filemain",
        "id""/providers/minimax/apiKey"
      }
    }
  }
}

注意是 keyRef,不是直接把 key 的值换成对象。这个细节文档里写了,但很容易忽略。

另外提一嘴: models.json是 OpenClaw 自动生成的文件,不应该手动改,删掉让它重新生成就行。

重启 gateway 后,OpenClaw 会根据 openclaw.json 和 auth-profiles.json 自动重新生成 models.json。

改完之后,再跑一次审计,nice!

Notion image

干干净净,一个不剩。

形成闭环。

Notion image

写在最后

回到开头那个群友的故事。

说实话,龙虾本身没问题,问题出在,大多数人搭完能跑就觉得完事了。

今天这三步,脱敏、凭据分离、审计。

说白了就是在做一件事:缩小暴露面。密钥不该出现在聊天窗口里,不该出现在配置文件里,不该出现在任何它不需要出现的地方。

不难,但你得主动做。没人会替你做。

如果你也在给别人搭龙虾用,建议现在就花 20 分钟把这三步走一遍。

别等出事了才想起来这篇文章。

觉得有用?点个在看,转发给你身边也在玩龙虾的朋友,别让他们的密钥继续裸奔了。

原文地址: https://mp.weixin.qq.com/s?__biz=MzU4MjY5NTc4OQ==&mid=2247499146&idx=1&sn=49cb76752133a5dfc1f744c545d93070&chksm=fce0f861cfdc3509eba0cf224c7a413ae04d98140943c79e2d04a096002fb75422ad4f8f748e&mpshare=1&scene=1&srcid=0406FIX1567TpVn9LkdNXFy0&sharer_shareinfo=932d53c802646b005717fb8d6fa65fa8&sharer_shareinfo_first=932d53c802646b005717fb8d6fa65fa8#rd

相关文章