我来讲个事故。你在跑一个 SaaS 平台,用户把各自的 Agent 部署上来。某天用户 B 提工单:他的私有目录被清空了。你查审计日志,发现是用户 A 的 Agent 执行的 rm -rf /tmp/b。为什么 A 能访问 B 的目录?因为你们把两个用户的 Agent 放进了同一个未隔离的沙盒。

这个事故听起来低级,但在 Agent 平台早期很常见。我最早做这块时也犯过同样的错——以为把 Agent 丢进 Docker 容器就万事大吉,直到用户 A 的 Agent 读取了用户 B 的环境变量。今天这篇文章,我想把 Agent 沙盒的多租户隔离这件事讲透:为什么容器不够用,什么方案更靠谱,以及 SaaS 场景下你真正要防的是什么。
Agent 沙盒到底在隔离什么?
Agent 和普通后端服务不一样。普通服务只响应固定接口,Agent 却会自己决定执行什么命令、访问什么文件、调用什么工具。给用户开放 Agent 开发,相当于让陌生人在你服务器上跑步,而且还允许他们带道具——你必须给他们画一个边界。
这个边界就是沙盒。多租户隔离是在边界之上再加一道墙:不同的租户之间不能互相看见、抢占资源、传递数据。拆开来看,你需要隔离四样东西:文件系统、进程、网络、资源。
| 隔离维度 | 不隔离的后果 | 常见实现 |
|---|---|---|
| 文件系统 | A 的 Agent 能读 B 的私有文件 | 根文件系统只读 + 挂载命名空间 |
| 进程 | A 的 Agent 能 kill 掉 B 的进程 | PID 命名空间 |
| 网络 | A 的 Agent 能直接访问 B 的内部服务 | NetworkPolicy + 独立网络栈 |
| 资源 | A 跑满 CPU,B 的 Agent 卡成 PPT | cgroups 配额 |
听起来很直接,对不对?但真正的坑在于:容器只帮你做了前两层的部分工作。默认情况下,容器共享宿主机的内核和网络栈。你可以通过配置把它调成接近隔离,但默认配置远不够多租户安全。
为什么说容器不是为多租户沙盒设计的
我踩过最深的一个坑,就是过早相信了 Docker 的安全性。有一次我为了图省事,在宿主机上直接暴露了 Docker socket,然后让 Agent 通过 socket 启动一个新的容器来执行任务。结果呢?任何用户只要拿到 Docker socket 权限,就能创建带宿主机挂载的容器,直接读到宿主机的 /etc/shadow。这个设计简直是在给攻击者递钥匙。
Docker 的隔离机制包括 Linux namespace、cgroups、seccomp 和 AppArmor,官方文档也承认,默认配置旨在减少意外操作,而不是作为强隔离的安全边界。它防的是“用户 A 不小心删了用户 B 的文件”这种无意的犯错,防不住“用户 A 蓄意逃逸”这种有意的攻击。
更麻烦的是,容器和宿主机共享同一个内核。内核有个漏洞,比如 CVE-2022-0185,攻击者就能通过容器逃逸到宿主机。你也许会说:那我持续打补丁不就好了?在 SaaS 平台里,你打的补丁得能覆盖所有内核版本,还得在用户没感知的情况下热更新。这句话说出来,运维兄弟已经想打人了。
所以在多租户 Agent 场景,真正的边界往往要更深一层。
微虚机和用户态内核:为不可信代码而生
容器之上还有两道防线,分别是微虚机(Firecracker、Kata Containers)和用户态内核(gVisor)。这两者的思路不一样,但目标一致:即使 Agent 是恶意代码,也拿不到宿主机的访问权。
Firecracker 是 AWS 为 Lambda 设计的微虚机,它用硬件虚拟化隔离每个租户,每个 Agent 独占一个极小的虚拟机。虚拟机有独立内核,算上网络初始化也就几百毫秒启动速度。缺点是内存占用比容器高,因为每个虚拟机都要跑一个精简内核。
gVisor 的做法更特别:它在用户态实现了一个“影子内核”。Agent 发起的系统调用不会直接到达宿主机内核,而是先被 gVisor 拦截、检查、再模拟执行。这样即使 Agent 试图利用内核漏洞,打的也是 gVisor 自己实现的那一层,而不是真实内核。gVisor 的隔离强度取决于这个影子内核的完善度,官方文档说得很直白:不是代理,不是容器,而是为用户态实现的系统调用层。
实际用起来很简单。你只需要把 gVisor 装成 Docker 的 runtime,然后启动 Agent 容器时指定 runtime:
docker run --runtime=runsc --name agent-sandbox --read-only --network=none --memory=512m --cpus=0.5 your-agent-image
粗看这些参数和普通容器一样,但 runsc 底层已经把所有系统调用接管到了用户态。即使 Agent 退出容器,它连宿主机的内核都没碰到过。
对我们来说,选型的时候可以参考这张表:
| 方案 | 隔离强度 | 启动速度 | 内存开销 | 典型案例 |
|---|---|---|---|---|
| 普通容器 | 低(共享内核) | 毫秒级 | 低 | 个人项目内部隔离 |
| gVisor | 中(用户态内核) | 百毫秒级 | 中 | 多租户 CLI 执行环境 |
| Firecracker | 高(硬件虚拟化) | 亚秒级 | 高(每 VM 一个内核) | 无服务器函数,强隔离需求 |
我的经验:如果你做的 Agent 平台允许用户上传任意代码,那 Firecracker 或 Kata Containers 更稳;如果只是执行模型生成的固定命令,gVisor 可能够用。
SaaS 场景下,隔离不只是“沙盒”
有一个问题很容易被忽略:沙盒隔离的是“运行时”,但 Agent 还有模型上下文、工具凭据、记忆存储这些“数据面”。多租户隔离必须覆盖到数据面,否则你的 Agent 之间虽然没有互杀进程,但模型读到的用户历史被混淆,照样爆炸。
我在真实项目里遇到过:用户 A 的 Agent 和用户 B 的 Agent 共用了同一个 PostgreSQL 实例,表名不小心撞了,结果 B 的对话记忆串到了 A 的会话里。要命的是,这种问题很难通过监控发现,只能靠租户隔离的严格设计来避免。
具体来说,你要做三层隔离:
- 控制面隔离:每个租户独立的 API Key、独立的配置标识,避免误用他人凭据。
- 数据面隔离:要么每个租户独立数据库 schema,要么所有表都带 tenant_id 并且查询条件强制带上。我倾向于前者,因为 schema 隔离不会出现“忘加 tenant_id”这种 bug。
- 网络面隔离:租户的 Agent 只能访问允许的域名和内部服务,用 Kubernetes NetworkPolicy 或云厂商的安全组实现。
Kubernetes 官方的多租户文档也强调:租户之间不仅要隔离资源,还要隔离信任边界。如果你的租户之间可能互相攻击,那就不能共享 Node,除非你做了强隔离。
一个常见的偷懒方案:给每个租户一个 Kubernetes Namespace
很多团队觉得,直接给每个租户一个 Namespace,配合 ResourceQuota 和 NetworkPolicy 就行了。但 Namespace 本身不是安全边界,它只是资源分组。Pod 之间默认是互通的,NetworkPolicy 需要你手动写对。更麻烦的是,如果共享 Node,一个逃逸的容器就能看到其他 Namespace 的流量。所以 Namespace 只能作为“管理边界”,不能作为“信任边界”。
最容易翻车的资源隔离:CPU 抢占和“邻居噪音”
最后一个坑,也是最难察觉的。即使你做了文件、进程、网络的彻底隔离,只要多个租户共享同一批物理机,资源抢占就会造成隐性影响。用户 B 的 Agent 正在生成重要报告,用户 A 的 Agent 突然开始跑一个大型编译任务,B 的延迟瞬间从 200ms 变成 2 秒。
解决办法是给每个租户设置 cgroup 配额,限制 CPU 和内存上限。但如果只限上限而不限制抢占权重,也会出现饥饿的情况。你需要同时设置 CPU 请求和限制,让调度器知道这个租户的最低保障和最高上限。
在 Agent 场景,另一个微妙问题是:模型推理本身也是共享资源。如果你把推理服务的并发限制和沙盒的 CPU 配额割裂开,一个租户的 Agent 可能会把推理队列塞满,导致其他租户的 Agent 等不到模型响应。所以资源隔离必须端到端地看,不仅看沙盒,还要看沙盒依赖的所有内部服务。
所以,防住互相影响的关键是什么?
我自己的认知过程是:起初以为“多租户隔离”就是“把容器跑起来”,后来发现要加网络策略,再后来发现要上微虚机,最后才意识到——真正要防的不是技术漏洞,而是设计层面的边界模糊。只要边界清晰,就算某个环节有漏洞,你还有第二层防线。
现在的我认为,一套合格的 Agent 沙盒多租户隔离至少要满足以下条件:
- 每个租户独立的身份体系和密钥管理,不能出现“一个 token 走天下”的默认配置。
- 沙盒根文件系统只读,写操作只落在租户自己的持久化卷里。
- 网络出口有白名单,内部服务通过 metadata header 识别租户身份。
- 每个租户的资源配额独立,并且允许监控到是哪个租户在消耗。
- 所有 Agent 操作有审计日志,日志本身要按租户隔离。
还有一点,你必须接受:绝对隔离是一种理想,现实中不存在。共享 CPU 缓存的侧信道攻击、内核漏洞的未知变体、供应链上的恶意依赖——这些都能穿透你精心设计的边界。所以隔离的重点不是“永远不会被穿透”,而是“穿透一层之后,还有一层”的纵深防御。你每一层都做好审计和告警,才能在被突破时及时发现并止血。
这也就是为什么我不建议你只依赖一个容器运行时或一个虚拟化方案。你可以先用命名空间和 cgroup 做第一层隔离,再上 gVisor 或微虚机做第二层,最后在数据面用 schema 隔离兜底。三层下来,互相影响的风险会低到可以接受。
Agent 沙盒的多租户隔离,本质上是一个工程取舍问题。不是“用哪种技术最安全”,而是“你愿意为安全付出多少成本”。看懂了这一层,你就不会被营销话术带着走,也不会在下一次安全事故时手足无措。
原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/537.html