1. 为什么 OpenClaw 多租户隔离在 K8s 上总翻车
OpenClaw 多租户隔离,说白了就是让多个团队或客户共用一套 K8s 集群跑 Agent,但彼此的资源、数据、凭证完全看不见。它适合已经把 OpenClaw 部署到 K8s、开始接第二个业务方的团队。我见过太多人一上来就复制粘贴一个 Namespace 就以为隔离完了,结果 A 租户的 Agent 能读到 B 租户的 PVC,或者一个批量任务把整个集群的 CPU 吃满,全员响应变慢。
问题的根子在于:K8s 的 Namespace 只是「逻辑分组」,它默认不拦网络、不拦资源、不拦凭证。你不显式加 NetworkPolicy,Pod 之间就是通的;你不加 ResourceQuota,一个租户能占满节点;你不做 RBAC,默认 ServiceAccount 可能带着过高权限。OpenClaw 的 Agent 工作负载又比较特殊——它要挂载 workspace、要调模型 API、要执行技能脚本,这三件事分别对应存储隔离、出口网络隔离、凭证隔离,缺一个都算漏。
所以这篇不聊虚的架构图,直接给你能kubectl apply的清单:Namespace + RBAC + ResourceQuota + NetworkPolicy 四件套,再讲怎么用 TaoToken 把多租户的模型调用入口收敛成一个统一 Key/API 通道,避免每个租户各自管一堆 Key 导致审计黑洞。预期达成:租户间资源互不挤占、网络互不可达、数据互不可见、调用可追溯。
先明确一个前提:你已经完成 OpenClaw 在 K8s 的部署,集群版本 1.24+,有 cluster-admin 权限做初始配置。下面所有 YAML 都可以直接改租户名复用。
2. TaoToken 前置:把多租户模型调用入口收敛
多租户场景下最容易被忽略的是「出口」。每个租户的 Agent 都要调模型,如果每个租户配一套自己的 Key,你会面临三个问题:Key 散落在各租户 Secret 里无法统一轮换、用量无法按租户归集、某个租户 Key 泄露影响面不可控。我的做法是把所有租户的模型调用都指向 TaoToken 的统一 API 通道,租户侧只拿到一个受控的 Key,真正的上游模型凭证由平台侧管理。
TaoToken 在这里扮演的是「统一 Key/API 通道」的角色:它兼容 OpenAI 风格的接口,OpenClaw 的 model provider 配置里把 base URL 指过去就行。这样租户的 Agent 配置里不出现任何上游厂商的原始凭证,平台侧换模型、调额度、做审计都在一个入口完成。
你需要先拿到两样东西:一个 API Key,以及确认接入文档里的 Base URL 格式。Key 在控制台的 API Keys 页面创建,建议按租户维度建多个 Key,命名带上租户标识,方便后续按 Key 归集用量。接入文档里有完整的 endpoint 说明和参数示例,配置前扫一遍能省很多试错。
具体操作路径:
- 打开 https://taotoken.net/api-keys 创建 Key,命名如
tenant-a-openclaw,创建后立即复制保存,页面不会二次展示完整 Key。 - 打开 https://taotoken.net/doc 确认当前 Base URL 和模型 ID 命名规范,不同模型 ID 写法有差异。
- 如果你要验证某个模型是否可用,用 https://taotoken.net/chat 直接对话测试,比在集群里 debug 快得多。
- 长期跑 Agent 编码任务、需要稳定额度的,看 https://taotoken.net/coding-plan ,比按量计费更适合持续负载。
这里有个关键设计:每个租户一个 TaoToken Key,但共享同一个 Base URL。租户的 Secret 里只存自己的 Key,平台侧通过 Key 前缀或标签区分租户。这样既做到了凭证隔离(A 租户的 Key 泄露不影响 B),又做到了入口收敛(所有调用都经过同一个通道,审计和限流有统一落点)。
配置到 OpenClaw 时,模型 provider 的写法大致是这样(具体字段名以你部署版本的文档为准):
{ "model": { "primary": "openai/gpt-4o-mini", "providers": { "openai": { "baseUrl": "https://taotoken.net/api", "apiKey": "${TAOTOKEN_API_KEY}" } } } }注意apiKey用环境变量引用,不要硬编码。这个环境变量从租户自己的 Secret 注入,下一节的 Deployment 里会体现。把这段配置和后面的 K8s 清单配合起来,租户的模型调用就完全走统一通道了。
3. 可复制配置:Namespace/RBAC/ResourceQuota/NetworkPolicy 四件套
这一节是全文的核心,所有 YAML 都可以直接改tenant-a复用。我按「先建边界、再限资源、后锁网络、最后绑权限」的顺序给,你照着 apply 就行。
3.1 Namespace 与租户标签
Namespace 本身不隔离,但它是所有其他策略的挂载点。关键是打上tenant标签,后面 NetworkPolicy 靠它做选择器。
apiVersion: v1 kind: Namespace metadata: name: tenant-a labels: tenant: tenant-a isolation-level: standardisolation-level这个标签是我自己加的,用来区分普通租户和高合规租户,后面可以针对不同值套不同策略。
3.2 ResourceQuota 与 LimitRange
ResourceQuota 管的是「这个 Namespace 总共能用多少」,LimitRange 管的是「单个 Pod 默认给多少」。两个都要配,只配 Quota 的话,一个 Pod 不写 resources 就能无限申请。
apiVersion: v1 kind: ResourceQuota metadata: name: tenant-a-quota namespace: tenant-a spec: hard: requests.cpu: "20" requests.memory: 50Gi limits.cpu: "30" limits.memory: 100Gi persistentvolumeclaims: "10" pods: "50" --- apiVersion: v1 kind: LimitRange metadata: name: tenant-a-limits namespace: tenant-a spec: limits: - default: cpu: "1" memory: 2Gi defaultRequest: cpu: "500m" memory: 1Gi type: Containerpods: "50"这个限制很重要,防止租户用大量小 Pod 绕过 CPU 配额。persistentvolumeclaims: "10"防止 PVC 无限创建占满存储。
3.3 RBAC:租户内 ServiceAccount 与 Role
OpenClaw 的 Agent Pod 用独立的 ServiceAccount,只给租户 Namespace 内的最小权限。绝对不要用 default ServiceAccount,它的权限边界不清晰。
apiVersion: v1 kind: ServiceAccount metadata: name: openclaw-agent namespace: tenant-a --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: openclaw-agent-role namespace: tenant-a rules: - apiGroups: [""] resources: ["configmaps", "secrets"] verbs: ["get", "list"] - apiGroups: [""] resources: ["pods"] verbs: ["get", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: openclaw-agent-binding namespace: tenant-a subjects: - kind: ServiceAccount name: openclaw-agent namespace: tenant-a roleRef: kind: Role name: openclaw-agent-role apiGroup: rbac.authorization.k8s.io这个 Role 只允许读自己 Namespace 的 configmap/secret 和 pod,不能跨 Namespace,不能改资源。如果你的 Agent 需要动态创建 Job,再单独加batch组的权限,别一股脑给 cluster-admin。
3.4 NetworkPolicy:默认拒绝 + 白名单放行
这是隔离的命门。K8s 默认所有 Pod 互通,你必须先「默认拒绝」,再按需放行。下面这条策略实现:租户 A 的 Pod 只能和同租户 Pod 通信,出口只能走 DNS 和公网模型 API。
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: tenant-a-isolation namespace: tenant-a spec: podSelector: {} policyTypes: - Ingress - Egress ingress: - from: - namespaceSelector: matchLabels: tenant: tenant-a egress: - to: - namespaceSelector: matchLabels: tenant: tenant-a - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system ports: - protocol: UDP port: 53 - ports: - protocol: TCP port: 443注意最后那条port: 443的 egress 规则没有to选择器,意思是允许访问任意 IP 的 443 端口——这是给模型 API 出口用的。如果你要更严格,把to换成具体的 CIDR 或域名解析后的 IP 段。DNS 那条必须放行,否则 Pod 解析不了域名,模型调用直接失败。
3.5 租户 Secret 注入
把 TaoToken 的 Key 存成租户自己的 Secret,Pod 通过环境变量引用。
apiVersion: v1 kind: Secret metadata: name: taotoken-credential namespace: tenant-a type: Opaque stringData: TAOTOKEN_API_KEY: "sk-替换成你在控制台创建的租户Key"创建命令建议用kubectl create secret generic配合--from-literal,避免 Key 写进 YAML 文件被提交到 Git:
kubectl create secret generic taotoken-credential \ --namespace tenant-a \ --from-literal=TAOTOKEN_API_KEY=sk-你的租户Key然后在 OpenClaw 的 Deployment 里引用:
env: - name: TAOTOKEN_API_KEY valueFrom: secretKeyRef: name: taotoken-credential key: TAOTOKEN_API_KEY到这里,一个租户的完整边界就建好了。复制这套 YAML,把tenant-a全局替换成tenant-b,再 apply 一遍,两个租户就物理隔离了。
4. 验证请求:确认租户间真的互不可见
配置写完不代表隔离生效,必须实测。我一般分四步验证:资源配额生效、网络隔离生效、RBAC 生效、模型调用走通。
4.1 验证 ResourceQuota
在租户 A 的 Namespace 里尝试创建一个超出配额的 Pod:
kubectl run quota-test --image=nginx --namespace tenant-a \ --requests=cpu=100 --restart=Never预期报错:
Error from server (Forbidden): error when creating "quota-test": pods "quota-test" is forbidden: exceeded quota: tenant-a-quota, requested: requests.cpu=100, used: requests.cpu=0, limited: requests.cpu=20看到exceeded quota就说明配额生效了。再查一下当前用量:
kubectl get resourcequota tenant-a-quota -n tenant-a -o yamlstatus.used字段会显示实际占用。
4.2 验证网络隔离
这是最关键的一步。在租户 A 起一个测试 Pod,尝试访问租户 B 的 Service:
kubectl run nettest --image=busybox --namespace tenant-a \ --restart=Never -- sleep 3600 kubectl exec -n tenant-a nettest -- \ wget -qO- --timeout=5 http://openclaw.tenant-b.svc.cluster.local预期结果是超时或连接被拒。如果返回了内容,说明 NetworkPolicy 没生效,检查两点:集群的 CNI 插件是否支持 NetworkPolicy(Calico、Cilium 支持,Flannel 默认不支持),以及策略是否真的 apply 到了正确的 Namespace。
再验证同租户内通信正常:
kubectl exec -n tenant-a nettest -- \ wget -qO- --timeout=5 http://openclaw.tenant-a.svc.cluster.local这条应该能通,否则说明你的 ingress 规则写错了。
4.3 验证 RBAC
用租户 A 的 ServiceAccount 尝试读租户 B 的 Secret:
kubectl auth can-i get secrets \ --namespace tenant-b \ --as=system:serviceaccount:tenant-a:openclaw-agent预期返回no。如果返回yes,说明有 ClusterRoleBinding 把权限放大了,去查一下有没有全局绑定。
4.4 验证模型调用走通
最后确认 Agent 能通过 TaoToken 调通模型。在租户 A 的 Pod 里直接 curl 测试:
kubectl exec -n tenant-a nettest -- \ wget -qO- --timeout=10 \ --header="Authorization: Bearer $TAOTOKEN_API_KEY" \ https://taotoken.net/api/v1/models能返回模型列表就说明出口网络和凭证都对了。如果超时,回到 NetworkPolicy 检查 443 出口规则;如果返回 401,检查 Key 是否正确注入。
四步全过,隔离就算落地了。任何一步失败,下一节的排查清单能帮你快速定位。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
多租户环境下的报错往往和单租户不一样,因为多了网络策略和 RBAC 的干扰。下面是我踩过的坑,按报错原文对照。
5.1 401 Unauthorized
{"error":{"message":"Invalid API key provided","type":"invalid_request_error"}}这个在租户场景下 90% 是 Secret 没注入对。排查顺序:先确认 Pod 里环境变量存在kubectl exec -n tenant-a <pod> -- env | grep TAOTOKEN;再确认 Secret 的 key 名和 Deployment 里secretKeyRef.key完全一致(大小写敏感);最后确认 Key 本身没被控制台删除或过期。如果环境变量在但值不对,检查是不是stringData和data混用了——data里的值必须是 base64 编码。
5.2 local proxy failed
Error: local proxy failed: dial tcp 10.x.x.x:443: i/o timeout这是 NetworkPolicy 出口规则没放行导致的。local proxy failed说明 OpenClaw 的出口代理连不上目标。检查你的 egress 规则里有没有放行 443 端口,以及 DNS 的 53 端口。特别注意:如果你的集群用了 NodeLocal DNSCache,DNS 请求的目标 IP 是节点本地地址,namespaceSelector那条可能匹配不到,需要额外加一条针对169.254.0.0/16或节点 CIDR 的规则。
5.3 reading choices 相关报错
Error: reading choices: unexpected end of JSON input这个通常不是隔离问题,而是模型返回体被截断或格式不对。在多租户场景下,常见诱因是租户的 ResourceQuota 把内存限得太死,Agent 处理大响应时 OOM 被 kill,导致流式响应中断。检查kubectl describe pod有没有OOMKilled。如果是,把 LimitRange 的默认内存从 2Gi 提到 4Gi,或者给这个租户单独放宽配额。
5.4 OAuth 相关报错
Error: oauth token exchange failed: invalid_grant如果你用的是需要 OAuth 的模型 provider,多租户下每个租户的 token 必须独立。常见错误是把平台侧的 OAuth 凭证共享给了所有租户,导致 refresh token 互相覆盖。正确做法是每个租户在 TaoToken 侧用独立 Key,OAuth 的刷新逻辑由通道侧统一处理,租户侧只持有静态 Key。这样就不会出现invalid_grant。
5.5 跨租户访问尝试的告警
如果你在审计日志里看到某个租户的 Pod 尝试访问另一个租户的 Service,别慌,这往往不是攻击,而是配置错误——比如某个 Agent 的配置里硬编码了另一个租户的 endpoint。检查该租户的 ConfigMap,把所有跨租户的 URL 改成自己 Namespace 内的 Service 名。NetworkPolicy 会拦住请求,但日志里的告警值得清理。
排查完这些,你的多租户环境基本就稳了。记住一个原则:任何跨租户的连通性都是 bug,不是 feature。
6. 长期编码与 Agent 场景的入口选择
多租户隔离落地之后,下一个问题是:租户多了,模型调用量上来了,怎么管额度和成本。按量计费适合波动大的场景,但如果你的租户里有长期跑编码 Agent、持续消耗 token 的,按量计费月底账单会很难看。
这种场景我建议看 Coding Plan:https://taotoken.net/coding-plan ,它更适合持续负载,额度可预期,配合前面的按租户 Key 设计,每个租户的消耗能单独归集。控制台 https://taotoken.net/console 里能看用量明细,做月度报表直接导出。
如果你还在验证阶段,先用模型对话 https://taotoken.net/chat 快速测通再上集群。接入细节以文档 https://taotoken.net/doc 为准,API Keys 在 https://taotoken.net/api-keys 创建。官网入口 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 有完整的方案说明。
最后给一个实操建议:租户 Key 的命名一定要带租户标识和创建日期,比如tenant-a-20260101。等你管到几十个租户的时候,没有命名规范的 Key 列表就是灾难。轮换 Key 时先建新的、灰度切换、观察一周再删旧的,别直接删——正在跑的 Agent 会立刻 401。