OpenClaw IV. 认证与安全(4)Multi-Account Patterns
本篇目标:把"一个人/一个团队同时拥有多个账号(或多个 Bot / 多个 Workspace / 多个 Provider credentials)"时,OpenClaw 侧应该怎么建模、怎么隔离、怎么切换、怎么审计,说清楚。
适用读者:在同一套 OpenClaw 部署里接入多个机器人账号/多个工作区(Slack/Feishu 等)/多个环境(staging & prod)/多租户 SaaS,且希望做到"不串线、可追责、可控回退"。
你会学到:
- 多账号在 OpenClaw 里通常对应哪些"对象"(accountId / channel / identity / session / workspace)
- 4 类最常见模式(单人多号、团队多号、环境多号、多租户)怎么落地
- 账号选择/切换策略(显式/隐式)、默认账号、回退账号
- 安全边界与最小权限:token 存储、scope、数据隔离、审计字段
- 常见坑:账号串线、跨号读 memory、误把 admin 号当默认、回调混淆等
1. 为什么 Multi-Account 是"认证与安全"问题
增量说明(2026-03-08):本节新增"生产级强隔离落地蓝图"(第 14 节),把多账号从概念讲解推进到:可直接复制的配置模板 + 入口分流 + sessionKey 编码 + memory namespace + 审计 schema + 熔断/回滚清单。
很多系统把多账号当成"易用性需求":能登录多个账号、随时切换。
在 OpenClaw 里,多账号首先是安全问题,因为它天然引入:
- 权限边界变多:不同账号的 token scope 不同,允许的动作不同。
- 数据面变复杂:不同账号对应不同 DM、不同群、不同 workspace、不同联系人目录。
- 审计与追责变难:同一句话/同一次工具调用,到底是哪个账号执行的?
- 回调与状态容易串:webhook 回来时,必须能精确映射到"哪个账号实例"。
所以,本篇用"安全工程"的方式处理多账号:
- 先建模(哪些实体必须唯一、哪些可以共享)
- 再给切换机制(如何选择执行账号)
- 最后定义隔离与审计(哪些数据绝对不能跨)
2. OpenClaw 里与"账号"相关的核心对象
不同 channel/provider 的叫法不同(Telegram bot、Slack app、Feishu tenant...),但在 OpenClaw 侧建议统一成下面几个概念。
2.1 accountId(执行身份)
accountId 是最关键的抽象:
- 代表"一套可执行动作的凭据集合"
- 绑定到一个或多个 channel connector(例如:Telegram bot token、Slack bot token、Feishu app credentials)
- 在工具层面,很多工具都支持
accountId参数(或通过 session 选择),用来明确指定"用哪个账号发/查"。
经验规则:
- 只要凭据不同,就应该是不同 accountId。
- 如果只是同一凭据的不同展示(昵称/头像),不需要拆。
2.2 channel(消息入口)
channel 是"消息从哪里来/要发到哪里去"。
多账号往往意味着:
- 同一种 channel(如 Slack)下多个 bot/app
- 同时接入多种 channel(Slack + Telegram + Feishu)
注意:channel != accountId。
- channel 是类型/路径
- accountId 是在该 channel 下的一套具体凭据
2.3 identity(对端用户/群的身份)
身份映射需要考虑:
- 同一个人,可能在不同平台有不同 id
- 同一平台内,在不同 workspace/tenant 内 id 也可能不同
因此 identity 的主键通常是:
(channel, tenant/workspace, userId)
并且 identity 绝对不应该在不同 tenant/workspace 间"合并",除非你有明确的跨租户绑定流程。
2.4 session(对话状态容器)
session 是 agent loop 的工作单元。
关键原则:
- session 必须绑定一个确定的 accountId(执行身份)
- 同时绑定一个"对端 identity / chatId / threadId"等路由信息
否则会遇到典型事故:
- A 账号收到的消息,却用 B 账号去回复
- 在同一个 session 中混入两套 token,导致权限不一致、审计不可追
2.5 workspace / memory namespace(数据隔离域)
OpenClaw 的 memory / files 是最容易"串线"的地方。
多账号系统里建议把 memory 的命名空间至少分层:
tenant/workspace(组织/空间)accountId(执行身份)conversation/session(会话)
最小可行方案:
- 按 accountId 切 memory 根目录 (例如
memory/<accountId>/...)
更成熟方案:
memory/<tenant>/<accountId>/<conversationKey>.md
3. 四种最常见 Multi-Account 模式
模式 A:单人多号(个人主号 + 小号 / 工作号)
需求
- 同一个人希望用"主号"处理日常事务,用"工作号"处理公司群/客户
- 或者主号绑定个人支付/敏感权限,小号用于低风险自动化
典型风险
- 默认账号选错:把高权限号当默认
- memory 串线:把工作群对话写进个人 session 的长期记忆
建议落地
- 账号选择:明确让用户指定默认账号(
/account default work),且默认账号一定是低权限 - sessionKey 编码 accountId:
<channel>:<tenant>:<accountId>:<chatId> - memory namespace:至少按 accountId 拆
模式 B:团队多号(多个 bot / 多个 workspace)
需求
- 同一套 OpenClaw 服务同时为多个团队/多个 workspace 服务
- 各团队希望自己的 bot 只访问自己的数据
典型风险
- "共享存储/共享缓存"导致跨团队泄漏
- webhook 回调混淆(同一路径接所有团队)
建议落地
- ingress 分流:路径或子域携带
ingressKey,做到一眼定位 account - 强制 tenantId:所有 trace/audit 必填 tenantId
- storage 硬过滤:任何查询必须携带 tenantId/accountId 过滤条件,否则 fail-closed
模式 C:环境多号(staging/prod 分离)
需求
- staging 用测试 bot,prod 用生产 bot
- 希望同一套代码/配置体系支持切换
典型风险
- staging token 写进 prod 配置
- prod 会话误用 staging 的工具/模型
建议落地
- env 作为一级分区:
env必须进入 sessionKey / trace / namespace - 禁止跨 env fallback:prod 不允许 fallback 到 staging(反之可允许)
- release gate:上线前跑"入口唯一性 + session 绑定一致性 + namespace 隔离"合约测试
模式 D:多租户 SaaS(Tenant-based)
需求
- 一个 OpenClaw SaaS 给很多客户(tenant)用
- 每个 tenant 都有自己的一套 channel credentials 或 workspace
典型风险
- tenant 识别错误导致"误把 A 的消息当成 B"
- 共享模型/工具导致的权限穿透
建议落地
- tenantId 从 ingress 决定(不可来自用户输入)
- 任何高风险工具要求 explicit delegation(需要租户侧审批)
- 合约测试:随机抽样多 tenant 入口,验证不可能映射到同一 account
4. 账号选择与切换:显式 vs 隐式
4.1 显式选择(推荐)
显式选择指:用户或系统在请求中明确指定要用哪个 accountId。
- 命令式:
/use account=work - UI:选择器
- API:
accountId字段
优点:可审计、可解释、可 fail-closed。
4.2 隐式选择(谨慎使用)
隐式选择常见策略:
- 根据入口(ingressKey/webhook path)
- 根据 chatId/workspaceId 映射
- 根据 session 绑定
隐式选择必须满足:
- 决策过程可追责(写入 audit)
- 决策结果可复现(sessionKey 可恢复)
- 失败时宁可拒绝也不要猜(fail-closed)
4.3 默认账号与回退账号
默认账号是事故高发点:
- 默认必须是最低权限
- 回退账号不等于"更高权限账号"
建议定义:
defaultAccountId: 只允许"只读/低风险"工具fallbackAccountId: 仅用于同权限域的可用性提升(例如同 tenant 的备用 bot),禁止跨 tenant、禁止提升 scope
5. 最小权限(Least Privilege)在多账号里的落点
多账号不是"多几套 token"那么简单,它是把权限切成了多个桶。
5.1 token scope 分层
建议按能力把账号分层:
- L0:只读(查询、总结)
- L1:低风险写(发消息、写文档)
- L2:高风险写(删改、转账、发邮件/发帖)
- L3:管理员(配置变更、密钥轮换、kill switch)
5.2 账号与工具能力的绑定
工具层面建议做 capability gating:
accountId -> allowedTools/toolScopes- policy engine 在执行前检查 account 的 scope
5.3 不允许"隐式升级"
典型坏味道:
- "这个账号没权限,那我换个有权限的账号帮你做"
正确做法:
- fail-closed + 提示需要授权/切换
- 或走 delegation 流程(见第 12 节)
6. memory / cache / vector index:最容易串线的三兄弟
多账号的安全边界,80% 的事故来自存储层。
6.1 memory namespace 的强制规则
最低要求:
- memory key =
tenantId/accountId/sessionKey - 任何读写必须携带这三个字段
6.2 cache 的 key 设计
常见坑:
- cache key 只用
userId,导致不同 tenant 命中同一个缓存
正确做法:
- cache key 至少包含
tenantId + accountId + userId
6.3 vector index 的硬过滤
向量检索最危险,因为"看起来像在搜相似内容"。
必须:
- 每条 embedding 都带
tenantId/accountId - query 必须加 filter(metadata filter),否则拒绝
7. webhook / ingress:入口映射必须唯一
多账号最核心的工程事实:入口决定身份。
7.1 ingressKey(推荐)
把每个账号的入口定义成一个不可猜测的 key:
/webhook/<ingressKey>/slack/webhook/<ingressKey>/feishu
7.2 二次验签(推荐)
入口 key 只是路由,仍需要 provider 的签名验签。
- Slack: signing secret
- Feishu: event verification token
7.3 唯一性合约
必须保证:
- ingressKey 全局唯一
- ingressKey -> accountId 映射不可变(至少不能 silent change)
8. sessionKey:把 accountId/tenant 编进"可恢复主键"
建议 sessionKey 规则:
env:<env>|ch:<channel>|t:<tenantId>|a:<accountId>|c:<conversationId>
关键好处:
- 任何一次恢复都能校验 accountId 是否一致
- 事故排查能直接 grep sessionKey
9. 审计(Audit):追责必须能回答 3 个问题
每一次工具调用/消息发送必须能回答:
- 用的哪个 accountId?
- 代表哪个 tenant/workspace?
- 是谁/什么触发的(user / cron / webhook / tool)?
建议 auditEvent 字段:
timestamp, env, tenantId, accountId, sessionKey, ingressKeyactorType (user|system|cron|webhook|agent)action (tool:message.send)resource (chatId/docId/...)decision (allow|deny|degrade|require_confirm)
10. 常见事故与防呆
10.1 账号串线(A 收到消息,B 回复)
根因:session 没绑定 accountId 或恢复时没校验。
对策:
- session 创建时写死 accountId
- 每次处理入站时校验
resolvedAccountId == session.accountId,不一致直接 quarantine
10.2 误把 admin 号当默认
对策:
- 默认账号必须低权限
- admin 号只能显式调用
10.3 webhook 回调混淆
对策:
- ingressKey path 分流
- provider 侧签名验签 + tenant/workspace id 双重校验
11. 账号切换 UX:你要的不是"切换",是"解释"
产品层面,切换必须可解释:
- 我现在用的是哪个账号?
- 为什么用这个?
- 我能不能换?换了会带来什么权限变化?
建议提供:
/whoami:返回 accountId/tenant/env + scopes/accounts:列出可用账号与权限等级/use <accountId>:显式切换
12. delegation:唯一允许的"跨账号/跨权限"路径
如果你允许跨账号做事,必须是"显式委派",而不是 agent 自己猜。
最小 delegation 流程:
- 低权限账号收到请求
- policy 判断需要更高权限
- 生成 delegation request(可审计)
- 用户/管理员批准后,才允许用高权限 account 执行
delegation event 必须写入 audit,并绑定:
- fromAccountId -> toAccountId
- requestId
- approvedBy
13. 反例:不要做这些
- 不要把多账号做成"一个 session 里随时换 token"
- 不要让 agent 自己决定用哪个账号(除非规则极其明确并可审计)
- 不要共享 memory/vector index(哪怕你觉得数据不敏感)
- 不要让 fallback 去更高权限域
14. 生产落地:多账号"强隔离"实现蓝图(可直接抄)
这一节是把前面 1~13 的原则落到工程结构上:你复制完以后,系统在默认情况下就"很难串线"。
14.1 Account Registry:单一真相表(SSOT)
目标:所有账号、入口、tenant/env、secretRef 都在一个 registry 里声明;任何路由/恢复/审计都以它为准。
registry 里至少包含:
envtenantIdaccountIdingress.keys[](全局唯一)secrets.*Refcapabilities(允许的工具/权限等级)
14.2 Ingress → accountId:不可逆映射
- 用
ingressKey路由到 account - 同时做 provider 签名验签(防伪造)
- tenant/workspace id 作为二次校验(防错配)
原则:映射失败=拒绝处理;宁可丢消息,也不要串线。
14.3 sessionKey 编码 + 恢复一致性校验(fail-closed)
- sessionKey 必须编码
env/tenantId/accountId - 恢复 session 时必须校验这些字段与 ingress resolve 结果一致
- 不一致直接 quarantine(并触发告警)
14.4 namespace:memory/cache/vector index 一路带到底
- memory root:
memory/<env>/<tenantId>/<accountId>/... - cache key:
env:tenant:account:user:... - vector metadata filter:
{env, tenantId, accountId}必填
14.5 tool wrapper:强制注入 accountId + policy gate + audit
把工具调用包一层:
- 强制写入 trace ctx(env/tenantId/accountId/sessionKey/ingressKey)
- policy 先判定(allow/deny/degrade/require_confirm)
- 写 auditEvent(字段缺失=bug)
14.6 两类熔断(最小版)
- account mismatch:resolvedAccountId != session.accountId → quarantine ingress + kill session
- privilege escalation:请求触发高权限工具但无 delegation → deny + 告警
14.7 上线前 30 分钟最小落地清单
- ingressKey 全局唯一(合约测试)
- sessionKey 编码 env/tenant/account
- storage namespace 硬隔离(vector filter 必填)
- auditEvent 字段齐全(缺字段=bug)
- kill switches 可用(account/ingress/degrade)
15. 小结
多账号不是"多放几套 token",而是把系统拆成多个"安全域"。
你要的结果是:
- 任何请求都能被唯一映射到 accountId/tenant
- 任意一次恢复都不会换身份
- 任意一次工具调用都可追责
- 出事故时能一键止血(kill/quarantine/degrade)
16. 把 Multi-Account 做成可观测、可验证、可回滚的系统
这节是把"强隔离"再向前推进一步:把它变成"可持续运营"的能力。
16.1 Account Trace Context(统一字段集)
强烈建议把以下字段作为"全链路必填":
envtenantIdaccountIdsessionKeyingressKey
16.2 Account Mismatch Sentinel(绑定一致性哨兵)
- 比对 resolvedAccountId 与 session.accountId
- 不一致:fail-closed + 写 audit + 告警
16.3 Privilege Escalation Sentinel(防隐式升级)
- 禁止隐式跨 accountId 调用
- 只有 delegation 才能跨
16.4 Multi-Account Contract Tests(合约测试)
最小要测:
- ingress key 唯一
- accountId/env/tenant 组合唯一
- session 恢复绑定一致
- namespace 不可跨读写
- audit 字段齐全
16.5 三类 Kill Switch
- Account kill:停用某个 accountId 的所有执行
- Ingress quarantine:隔离某个 ingressKey
- Degrade:整体降级(只读/只总结)
16.6 5 分钟事故止血路径
- 发现串线风险 → 立即 quarantine ingress
- kill 对应 account session
- degrade 到只读
- 取证:用 trace ctx 还原链路
- 修复后跑 contract tests + policy regression harness
16.7 最小落地包:你应该复制粘贴的 4 个文件
config/registry.json(SSOT)src/resolveAccount.ts(ingress→accountId)src/traceContext.ts(统一 trace ctx 注入)tests/multiAccount.contract.test.ts(合约测试)
16.8 你可以直接抄的最小模板(4 个文件)
16.8.1 config/registry.json
json
{
"version": 1,
"accounts": [
{
"env": "prod",
"tenantId": "acme",
"accountId": "slack-acme-bot",
"ingress": { "keys": ["k_prod_acme_slack_01"] },
"secrets": {
"slackBotTokenRef": "secret://slack/acme/botToken",
"slackSigningSecretRef": "secret://slack/acme/signingSecret"
},
"capabilities": {
"tier": "L1",
"allowedTools": ["message.send", "doc.write"],
"denyTools": ["drive.delete", "admin.rotateSecret"]
}
}
]
}
16.8.2 src/resolveAccount.ts
ts
import registry from "../config/registry.json";
export function resolveAccountIdFromIngressKey(ingressKey: string) {
for (const a of registry.accounts) {
if (a.ingress.keys.includes(ingressKey)) return a.accountId;
}
throw new Error("UNKNOWN_INGRESS_KEY");
}
16.8.3 src/traceContext.ts
ts
export type TraceCtx = {
env: string;
gatewayId?: string;
tenantId: string;
accountId: string;
sessionKey: string;
ingressKey: string;
providerRequestId?: string;
};
export function withTrace<T extends object>(ctx: TraceCtx, extra?: T) {
return { ...extra, trace: ctx };
}
16.8.4 tests/multiAccount.contract.test.ts(7 条规则的最小版)
用你习惯的测试框架即可;这里用伪代码表达断言要点:
ts
import registry from "../config/registry.json";
test("ingress keys are unique", () => {
const all = registry.accounts.flatMap(a => a.ingress.keys.map(k => [k, a.accountId] as const));
const seen = new Map<string, string>();
for (const [k, id] of all) {
if (seen.has(k)) throw new Error(`DUP_INGRESS ${k} -> ${seen.get(k)} & ${id}`);
seen.set(k, id);
}
});
test("no cross-tenant accounts share ingress", () => {
// 如果你允许复用,明确写 allowlist;默认不允许
});
这 4 个文件的价值不在"代码多优雅",而在于:入口映射唯一 + session 绑定稳定 + 存储命名空间硬隔离 + 审计字段可追责。先把事故面收敛,再谈体验优化。