)
OpenSandbox 多租户隔离实战基于 Kubernetes Namespace 与 API Key 的 Multi-Tenancy 架构解析OSEP-0014【免费下载链接】OpenSandboxSecure, Fast, and Extensible Sandbox runtime for AI agents.项目地址: https://gitcode.com/GitHub_Trending/ope/OpenSandbox本文依据 OpenSandbox 仓库中的 OSEP-0014 提案《Multi-Tenancy Support for Kubernetes Runtime》撰写并结合server/目录下的真实实现代码、测试用例与 docs/guides/multi-tenancy.md 用户指南进行源码级印证。文章聚焦如何在单套 OpenSandbox Server 部署上通过tenants.toml将 API Key 映射到独立 Kubernetes Namespace实现租户级隔离这一主题读者将掌握多租户模式的触发条件、TenantProvider 抽象、热加载机制、鉴权与命名空间解析链路以及从单租户迁移的完整方案。为什么需要多租户单一集群共享的痛点在引入多租户之前OpenSandbox 的默认部署形态是一套 Server 一个 API Key 一个 Kubernetes Namespace所有沙箱消费者共用同一份资源边界。这带来两个层面的问题工作负载无隔离所有沙箱挤在同一个 Namespace 中任何一个异常消费者都可能影响其他人ResourceQuota、NetworkPolicy、LimitRange这些 Kubernetes 原生的隔离手段无法做到按消费者生效。凭据无隔离共享一个 API Key 意味着没有按消费者的审计轨迹、无法单独吊销某个消费者的密钥、也无法针对单个消费者做限流。OSEP-0014状态implemented提出的解决思路是保持单套 Server 部署不变让每个租户拥有自己独立的 Kubernetes Namespace 和独立的 API Key 集合由 Server 在运行时把请求路由到对应租户的 Namespace。该提案的完整文本见 oseps/0014-multi-tenancy.md。目标与非目标Goals在独立配置文件tenants.toml中定义租户对server.toml零改动每个租户对应一个专属 K8s Namespace每个租户支持多个 API Key支持无停机密钥轮换支持热加载hot-reload无需重启即可生效当tenants.toml不存在时单租户模式完全保持原样Docker runtime 明确不支持若runtime.type docker且配置了租户Server 启动时直接报错退出而不是静默忽略。Non-GoalsDocker runtime 下的多租户Docker 没有 Namespace 概念这是启动错误而非忽略Ingress 网关层租户隔离ingress 是数据面路由层故意不感知租户代理层的隔离依赖不可猜测的沙箱 ID 签名 token K8s NetworkPolicy通过 REST API 动态增删租户留待未来 OSEPServer 层的按租户限流委托给 K8s/ingressServer 侧资源配额委托给 K8s ResourceQuota迁移工具手工操作文档化。总体设计一份新配置两种运行模式多租户是opt-in特性。核心设计是引入一个独立的tenants.toml把API Key → 租户 → Namespace的映射关系从server.toml中剥离出来┌───────────────────────────────┐ │ server.toml (unchanged) │ │ [server] api_key ... │ │ [kubernetes] namespace ... │ └───────────────────────────────┘ ┌───────────────────────────────┐ │ tenants.toml (new, optional) │ │ [[tenants]] │ │ name team-a │ │ namespace ns-a │ │ api_keys [key1, key2] │ └───────────────────────────────┘ FileTenantProvider (initial backend) TenantProvider interface (extension point)请求路由流程Server startup │ ├── runtime.type docker AND tenants.toml exists? │ └── YES → FATAL: exit with error. Docker has no namespace isolation. │ └── runtime.type kubernetes (or Docker without tenants.toml) │ Request with OPEN-SANDBOX-API-KEY header │ ├── tenants.toml exists? │ ├── YES → lookup key in tenant api_keys │ │ ├── found → inject tenant context, route to tenant.namespace │ │ └── not found → 401 │ └── NO → validate against server.api_key (legacy single-tenant) │ ├── valid → route to kubernetes.namespace │ └── invalid → 401需要说明的是提案文档中的触发条件是tenants.toml是否存在在落地实现中多租户的开关演化为server.toml中的[tenants]配置段见 server/opensandbox_server/config.py 的TenantsConfig模型与 docs/guides/multi-tenancy.md 的说明FileTenantProvider再按环境变量SANDBOX_TENANTS_CONFIG_PATH或默认路径~/.opensandbox/tenants.toml加载租户文件——两条路径缺一不可。关键约束Docker runtime 硬拒绝若runtime.type docker且启用了租户Server 启动即报致命错误退出。Docker daemon 没有 Namespace 概念多租户隔离无从谈起这是硬性拒绝而非静默跳过。server.api_key在多租户模式下被禁用必须把它迁移进tenants.toml作为某个租户的条目。无 Server 侧配额委托给每 Namespace 的 K8s ResourceQuota / LimitRange。热路径无文件 I/O配置在启动和 fsnotify 事件时加载进dict[str, TenantEntry]内存映射。TenantProvider 抽象认证与配置源解耦租户解析被封装在一个TenantProvider接口后面认证中间件只依赖该接口、不依赖任何具体配置源。这样初版可以用一个简单的文件型 Provider 上线同时为企业级部署租户已经托管在外部 IAM 或租户管理系统中留下干净的扩展点。接口定义server/opensandbox_server/tenants/provider.pyruntime_checkable class TenantProvider(Protocol): def lookup(self, api_key: str) - Optional[TenantEntry]: ... def list_tenants(self) - List[TenantEntry]: ... # 启动校验用 supports_enumeration: bool # 是否能在启动时枚举完整租户集 def ready(self) - bool: ... # 是否已加载初始状态 def start(self) - None: ... # 启动后台资源watcher/poller def close(self) - None: ... # 关闭时释放资源 def on_reload(self, callback) - None: ... # 配置变更通知可选supports_enumeration是一个值得注意的属性文件型 Provider 在启动时能枚举全部租户配置而按 Key 查询的 HTTP Provider 只能感知到曾经查询过的租户其启动列表为空、不能作为校验依据——这一点直接决定了启动时 Namespace 校验是否执行详见下文启动守卫。lookup的返回类型是TenantEntryserver/opensandbox_server/tenants/models.pydataclass(frozenTrue) class TenantEntry: name: str namespace: str api_keys: Tuple[str, ...] field(default_factorytuple)未来的 Provider 形态接口为以下扩展保留了空间提案中明确不在本 OSEP 范围内但接口可以承载HTTPTenantProvider——从内部 IAM API 轮询或流式获取租户元数据、密钥轮换、启用/禁用全部由外部系统管理该 Provider 已在当前仓库中落地见下文K8sConfigMapProvider——跨 Namespace 监听 ConfigMap 或 Secret组合/链式 Provider——如文件 外部 API 合并的 fallback 方案。启动装配伪代码见 server/opensandbox_server/main.py 的_build_tenant_providerif [tenants] 配置存在: provider FileTenantProvider(path) # 或 HTTPTenantProvider if not provider.ready(): → SystemExit (parse error, duplicates, etc.) else: provider None # single-tenant mode认证中间件只依赖TenantProvider接口未来切换后端无需改动认证代码。FileTenantProvider配置模型与热加载机制这是初版实现新包 server/opensandbox_server/tenants/。它读取tenants.toml并在文件变更时热加载。数据模型与校验FileTenantProvider的解析逻辑位于 server/opensandbox_server/tenants/file_provider.py每项校验都对应一条启动错误校验项结果namespace缺失或非字符串ValueError启动失败api_keys不是列表ValueErrorapi_keys为空ValueErrorTenant X has no api_keys configuredkey 为空串或非字符串ValueError同一 key 出现在多个租户ValueErrorDuplicate api_key across tenants: A and B解析成功后通过_build_lookup_dict构建dict[api_key → TenantEntry]内存映射file_provider.py后续每次lookup都是 O(1) 字典查询热路径上没有任何文件 I/O。加载与热更新流程配置文件路径解析优先级file_provider.py显式传入的 path 参数环境变量SANDBOX_TENANTS_CONFIG_PATH对应常量TENANTS_CONFIG_ENV_VAR默认路径~/.opensandbox/tenants.tomlDEFAULT_TENANTS_CONFIG_PATH。启动时start()先做一次全量加载文件不存在则抛FileNotFoundErrorServer 保持单租户或退出随后启动一个名为tenant-file-watcher的守护线程。热更新行为细节实现采用每 2 秒轮询文件 mtime的方式file_provider.py 的_watch_loopwhile not self._watcher_stop.wait(timeout2.0)并在此基础上设计了一套失败安全的语义新增 key→ 下一次轮询后立即生效下一次lookup即可命中删除 key→ 立即失效返回 401重载时解析出错→ 记录 warning保留旧配置无停机文件被删除→ 清空全部条目所有租户 key 失效统一 401所有读写都通过threading.Lock保护重载时在锁内原子替换_entries与_lookup。上述行为都有对应的单元测试佐证server/tests/test_tenants.py 中的test_file_provider_hot_reload新 key 生效、旧 key 失效、test_file_provider_reload_bad_file_keeps_previous坏文件保留旧状态、test_file_provider_reload_deleted_clears删除即清空。需要指出的是提案中描述的是基于 fsnotify 的事件驱动监听并特别提到Watcher monitors parent directory for ConfigMap atomic symlink swap——即兼容 Kubernetes ConfigMap 挂载时更新 原子替换符号链接的行为当前实现以轮询 mtime 落地同样兼容该场景。多副本场景下每个 Pod 的 watcher 独立感知自身挂载文件的更新kubelet 同步 ConfigMap 约需 1 分钟从而实现配置变更传播到所有副本且无需重启。HTTP 租户提供方实现落地扩展虽然 OSEP-0014 将HTTPTenantProvider列为未来扩展当前仓库已经把它落地实现server/opensandbox_server/tenants/http_provider.py。它适用于租户由外部 IAM 或租户管理系统托管的场景配置方式见 docs/guides/multi-tenancy.md[tenants] provider http endpoint https://iam.internal/opensandbox/tenant-lookup max_stale_seconds 300.0 timeout_seconds 5.0 auth_header X-Internal-Auth auth_token bearer-token-here各字段与默认值TenantsConfigserver/opensandbox_server/config.py字段默认值说明providerfilefile或http配置为 http 时必须提供endpointendpoint—HTTP Provider 必填Server 用 GET 请求解析 keymax_stale_seconds300.0端点不可达时最多继续提供过期缓存多少秒timeout_seconds5.0HTTP 请求超时auth_header/auth_token—向端点发起请求时附加的 Provider 级认证HTTP 端点契约请求GET {endpoint} Header: OPEN-SANDBOX-API-KEY: api_key响应200{ namespace: sandbox-alpha, ttl: 60 }响应401{ code: UNAUTHORIZED, message: Invalid API key }该 Provider 按 key 缓存结果缓存时长由服务端下发的ttl决定TTL 过期后同步重新拉取。实现细节包括按 key 的锁与 singleflight同一 key 并发只发一个上游请求其他请求等待结果见_fetch_and_cache、端点不可达且超过max_stale_seconds时抛TenantProviderUnavailable对应认证层的 503、401 视为无效 key 并清除缓存条目。另外start()时会检查端点是否 HTTPS非 HTTPS 会告警API keys will be transmitted in cleartext。认证中间件多租户模式下的鉴权流程认证中间件位于 server/opensandbox_server/middleware/auth.py头部常量SANDBOX_API_KEY_HEADER OPEN-SANDBOX-API-KEY。模式检测构造AuthMiddleware时传入tenant_providermain.py在app.add_middleware(AuthMiddleware, configapp_config, tenant_providertenant_provider)处注入见 main.pyProvider 非 None → 多租户模式中间件依赖的只有TenantProvider接口本身Provider 为 None → 单租户模式按server.api_key校验。认证与租户上下文注入核心流程auth.py健康检查等豁免路径/health、/version、/docs、/redoc、/openapi.json直接放行缺失OPEN-SANDBOX-API-KEY头 →401 MISSING_API_KEY多租户模式await asyncio.to_thread(self.tenant_provider.lookup, api_key)解析租户——lookup本身是同步字典查询通过to_thread避免阻塞事件循环命中 →set_current_tenant(tenant)写入request.state.tenant与ContextVar放行未命中 →401 INVALID_API_KEYProvider 抛TenantProviderUnavailable如 HTTP 端点不可达且缓存过期→503 TENANT_PROVIDER_UNAVAILABLE。租户上下文通过ContextVar在异步任务中传递下游通过get_current_tenant()读取server/opensandbox_server/tenants/context.py。提案要求API key 比较必须使用常量时间比较落地时单租户路径使用内存 key 集合的成员判断self.valid_api_keys而代理路由 token 的校验使用了hmac.compare_digest见 server/opensandbox_server/api/proxy.py。代理路由的租户语义变化单租户模式下代理路由/sandboxes/{id}/proxy/{port}/...默认绕过 API key 校验——终端用户访问沙箱时并不携带OPEN-SANDBOX-API-KEY。这一点在实现中体现为if self._is_proxy_path(request.url.path) and not self._is_multi_tenant: return await call_next(request)多租户模式下代理路由同样要求认证目的是先确立租户上下文、再路由到正确的 Namespace。对应测试 server/tests/test_auth_middleware.py 验证了多租户模式无 key 访问代理路径 → 401带租户 key → 200。同时_is_proxy_path的正则只接受数字端口形式并显式拒绝..路径穿越test_auth_middleware_is_proxy_path_rejects_traversal覆盖。代理 WebSocket 连接在多租户模式下同样需要租户认证_authenticate_websocket_tenantserver/opensandbox_server/api/proxy.py。Sandbox 服务命名空间运行时解析所有 K8s API 调用不再固定使用配置里的self.namespace而是运行时解析server/opensandbox_server/services/k8s/kubernetes_service.pydef _resolve_namespace(self) - str: tenant get_current_tenant() return tenant.namespace if tenant else self.namespace受影响的四个生命周期方法create_sandbox、list_sandboxes、get_sandbox、delete_sandboxlist只返回当前认证租户 Namespace 内的沙箱天然完成租户间的数据隔离get/delete用租户 A 的 key 操作租户 B 的沙箱 → 404Namespace 内找不到不泄露存在性。针对后台任务如 renew worker、代理路径等没有租户上下文的场景实现额外提供了跨 Namespace 兜底解析_find_sandbox_namespace(sandbox_id)先在默认 Namespace 查找再遍历tenant_provider.list_tenants()中所有租户 Namespace 逐个查找_resolve_namespace_for_lookup(sandbox_id)有租户上下文时直接用租户 Namespace否则走上述兜底。这个设计同时体现了提案中Informer 内存增长控制的缓解措施租户相关的资源按需惰性创建只为活跃沙箱服务。代理层的隔离模型代理路由/sandboxes/{id}/proxy/{port}/...不做租户上下文注入——ingress 网关故意不感知租户。代理层的隔离依赖三层防御不可猜测的沙箱 ID随机 UUID——知道一个租户的沙箱 ID 无法推断出另一个租户的签名路由 tokenOSEP-0011 Secure Access Endpoint——限时、加密绑定单个沙箱K8s Namespace 隔离——即使流量到达 PodNetworkPolicy 也会限制跨 Namespace 的 Pod 间通信。租户性在生命周期 API 边界create/get/list/delete强制执行而不是在数据面代理边界。沙箱租户标签提案设计在创建沙箱时附加opensandbox.io/tenant tenant_name标签并通过_resolve_tenant_name()返回tenant.name或default。从当前kubernetes_service.py的代码看标签写入最终汇聚到统一的ensure_metadata_labels/ workloadlabels合并路径kubernetes_service.py 及 L1472-L1497 的 label patch租户名解析以get_current_tenant()的实际返回为准。启动守卫fail-fast 校验提案要求三类启动守卫当前实现在validate_tenant_config与validate_tenant_namespaces中落地见 server/opensandbox_server/tenants/init.py在 main.py 与 lifespan 启动阶段调用validate_tenant_startup(): 1. Docker tenants 配置 → SystemExit 2. 租户 Namespace 缺失或不可访问 → SystemExit列出全部失败项 3. server.api_key 与 tenants 配置共存 → SystemExitvalidate_tenant_config的具体错误信息Docker[tenants] configured but runtime.typedocker. Multi-tenancy requires Kubernetes namespaces.冲突 keyserver.api_key must be removed from server.toml when using [tenants].validate_tenant_namespaces会遍历所有租户条目对每个 Namespace 调用core_v1_api.read_namespace()聚合所有失败项404 不存在 / 401·403 不可访问 / 其他异常一次性抛出方便运维单次修完配置。该函数仅对可枚举的 Provider 执行supports_enumeration True如文件型HTTP Provider 因无法在启动时枚举全部租户会跳过校验并打 warning运维需自行确保 Namespace 就绪后再发放 key。部署变更ConfigMap 拆分与 RBAC 升级提案的部署侧改动为三类拆分 ConfigMapopensandbox-server承载 server.tomlopensandbox-tenants承载 tenants.toml互不干扰Deployment 挂载两个 ConfigMap 都挂载进 Pod并设置环境变量SANDBOX_TENANTS_CONFIG_PATH指向 tenants.toml 的挂载路径RBAC 升级从单 Namespace 的Role升级为ClusterRoleClusterRoleBinding——多租户场景下 Server 必须能访问多个 Namespace。用户指南进一步说明官方 Helm chartopensandbox-server默认就部署了带跨 Namespace 访问能力的 ClusterRole ClusterRoleBinding因此多租户场景无需额外修改 RBAC。租户隔离模型交给 Kubernetes 的机制Server 本身不强制配额与网络策略隔离完全委托给 Kubernetes按 Namespace 生效隔离维度K8s 机制作用范围资源配额ResourceQuota每 Namespace 的 CPU、内存、存储默认限额LimitRange每 Namespace 的容器默认资源网络策略NetworkPolicy每 Namespace 的 ingress/egress沙箱数量count/batchsandboxesviaResourceQuota每 Namespace 的 CR 数量API 访问RoleBinding每 Namespace 的 API 权限集群管理员在租户上线前创建带ResourceQuotaLimitRange的 Namespace。用户指南中给出了可直接落地的示例apiVersion: v1 kind: ResourceQuota metadata: name: tenant-quota namespace: sandbox-alpha spec: hard: requests.cpu: 8 requests.memory: 16Gi limits.cpu: 16 limits.memory: 32Gi pods: 20配额耗尽的行为用户指南补充的实战细节当 Namespace 配额耗尽时POST /v1/sandboxes会快速失败并返回403 KUBERNETES::QUOTA_EXCEEDED而不是等到沙箱创建超时再报KUBERNETES::POD_READY_TIMEOUT。其中 count 配额由 K8s admission 立即拒绝计算类配额requests.cpu等由控制器在秒级内把ReconcilerError写入工作负载状态后由 Server 感知。该 fail-fast 契约目前适用于agent-sandboxworkload provider默认的batchsandboxprovider 下仍会等待创建超时。另外两个隔离相关注意点egress 边车资源若LimitRange设置了默认值[egress]中的requests/limits配置需要满足最小值避免边车继承为沙箱工作负载设计的默认值Pool API 不按租户隔离/pools路由运行在 Server 配置的默认 Namespace 而非认证租户的 NamespacePool 是共享资源需要按租户隔离 Pool 时应拆分部署。测试计划与验证提案的测试计划在仓库中均有对应落地单元测试server/tests/test_tenants.py跨租户重复 API Key → 解析期ValueErrortest_parse_tenants_file_duplicate_key空 key 列表 →ValueErrortest_parse_tenants_file_empty_keys热加载文件删除 → 条目清空新 key → lookup 立即命中坏文件 → 保留旧条目test_file_provider_hot_reload等路径解析优先级显式参数 环境变量 默认路径test_resolve_tenants_path_*。认证中间件测试server/tests/test_auth_middleware.py多租户模式下代理路径必须认证test_auth_middleware_proxy_requires_auth_in_multi_tenant_mode畸形代理端口非数字→ 401 而非 422test_auth_middleware_requires_key_for_malformed_proxy_port..路径穿越不被当作代理路径test_auth_middleware_is_proxy_path_rejects_traversal。集成/E2E 场景提案定义租户 A key 创建 → 沙箱落在 ns-a租户 A 的 list → 只返回 ns-a 的沙箱租户 B key get/delete 租户 A 的沙箱 → 404热加载新 key 免重启生效、删除 key 后 401回退删除 tenants 配置后server.api_key重新生效Key 轮换加新 key → 双 key 并存 → 删旧 key多副本更新 ConfigMap 后所有副本 60s 内生效。权衡与备选方案已知不足Drawbacks需要两份配置文件——由清晰的启动日志明确打印当前处于哪种模式缓解需要 ClusterRole——RBAC 范围比单 Namespace 的 RoleBinding 更宽这是多租户的固有成本按资源类型收敛范围无动态租户 CRUD——静态配置REST API / CRD 留待未来 OSEP。被否决的备选方案方案否决原因把租户塞进server.toml租户变更必须重启 Server认证直接耦合tenants.toml文件格式把企业级部署租户已在 IAM/外部系统挡在门外TenantProvider接口规避此问题用 SQLite 存租户单机方案破坏多副本每租户一套 Server 实例N 个进程运维成本高软多租户标签 单 Namespace没有 K8s 原生隔离ResourceQuota/NetworkPolicy 无法按租户生效每租户仅一个 API Key无法轮换换 key 即停机从单租户迁移与回滚单租户 → 多租户迁移与 docs/guides/multi-tenancy.md 一致创建目标 Namespace集群管理员预先创建并建议配置 ResourceQuota LimitRange编写tenants.toml把现有 key 作为某个租户条目、指向现有 Namespace[[tenants]] name default namespace your-existing-namespace api_keys [your-existing-api-key]在server.toml中添加[tenants]段[tenants] provider file删除server.toml[server]段中的api_key部署——旧 key 继续可用现在它是租户 key按需继续添加更多租户。回滚删除server.toml中的[tenants]段、恢复server.api_key、重启即可回到单租户模式server.api_keykubernetes.namespace兜底。无需数据迁移——现有沙箱留在原 Namespace 中。密钥轮换零停机在tenants.toml中添加新 key → 等待热加载文件型约 2 秒HTTP 型等 TTL 过期→ 客户端切换到新 key → 移除旧 key。过渡窗口期新旧 key 同时有效。常见故障速查症状原因修复启动报 Multi-tenancy requires Kubernetes namespacesruntime.type docker与[tenants]并存改用 Kubernetes runtime 或删除[tenants]启动报 Remove server.api_key from server.tomlserver.api_key与[tenants]并存从[server]段删除api_key启动报 namespace not found租户 Namespace 不存在启动前kubectl create namespace ns配置修改后合法 key 返回 401热加载尚未生效等 2 秒文件型或 TTL 秒HTTP 型503 TENANT_PROVIDER_UNAVAILABLEHTTP 端点不可达且缓存过期超过max_stale_seconds修复端点连通性或调大max_stale_seconds启动报 Duplicate api_key同一 key 分配给多个租户保证每个 key 全局唯一小结OSEP-0014 为 OpenSandbox 在 Kubernetes 上的多租户能力提供了完整的设计骨架独立的租户配置文件、接口化的 TenantProvider 抽象、基于 ContextVar 的租户上下文传播、运行时命名空间解析、fail-fast 启动守卫以及**隔离全部委托给 Kubernetes 原生机制**的清晰边界。实际代码在提案基础上进一步落地了 HTTP 租户 Provider、代理路由的多租户认证、WebSocket 租户认证与跨 Namespace 兜底查找等能力形成了一套可以直接上生产的多租户隔离方案。想深入了解的用户可以继续阅读提案全文oseps/0014-multi-tenancy.md操作指南含完整配置示例与配额耗尽行为docs/guides/multi-tenancy.mdProvider 接口与实现server/opensandbox_server/tenants/provider.py、server/opensandbox_server/tenants/file_provider.py、server/opensandbox_server/tenants/http_provider.py认证中间件server/opensandbox_server/middleware/auth.py命名空间解析server/opensandbox_server/services/k8s/kubernetes_service.py启动守卫与配置模型server/opensandbox_server/tenants/init.py、server/opensandbox_server/config.py测试用例server/tests/test_tenants.py、server/tests/test_auth_middleware.py【免费下载链接】OpenSandboxSecure, Fast, and Extensible Sandbox runtime for AI agents.项目地址: https://gitcode.com/GitHub_Trending/ope/OpenSandbox创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考