跳到主要内容

为具备代码编写能力的智能体构建沙箱:最小权限原则并非可选

· 阅读需 15 分钟
Tian Pan
Software Engineer

大多数团队在发布第一个可执行代码的智能体(Agent)时,只采取了一种安全控制措施:API 密钥范围限制(API key scoping)。他们给智能体一个具有 repo:read 权限的 GitHub 令牌,以及一个可以访问工作目录的 shell,然后就称其为“已沙箱化”。这种做法是错误的,其弊端只有在发生安全事故后才会变得显而易见。

能够编写和执行代码的智能体的威胁模型与 Web 服务器或 CLI 工具的威胁模型有着本质的区别。攻击面不再是协议边界,而是智能体读取的一切内容。这包括 git 提交记录、文档页面、API 响应、数据库记录以及它打开的任何文件。其中的任何输入都可能包含提示词注入(prompt injection),从而将你的研究型智能体转变为数据泄露管道。

本文涵盖了你真正需要的四层隔离措施:容器隔离、文件系统控制、网络出站策略和凭证范围限制,以及用于评估你是否做得正确的权限审计流程。我们的目标不是追求理论上的完整性,而是让你的智能体成为一个无趣的横向移动目标,而不是一个诱人的攻击目标。

为什么 API 密钥范围限制不是沙箱

最直观的第一道防线是限制智能体可以调用的资源:只读的 GitHub 令牌、禁止删除操作、限制在单个代码库内。这解决了直接的权限提升问题——拥有只读令牌的智能体无法推送代码。但它并没有触及最危险的攻击向量。

在 2025 年,一个被广泛使用的开源 AI 智能体平台存在一个未经身份验证的代码验证端点,允许远程代码执行(RCE)。在补丁发布之前,唯一防止被利用的是网络边界,而不是智能体执行环境内部的任何隔离措施。在另一类事件中,攻击者向对账智能体(reconciliation agents)发送了以常规业务任务形式表述的通配符查询;智能体认为这些请求在语义上是合理的并执行了它们,导致数千条客户记录被导出。即使采用更严格的 API 密钥范围限制,也无法阻止这两类事件。

根本问题在于你的智能体不仅仅是在执行你的代码。大语言模型(LLM)是在运行时生成代码。你控制着策略(即智能体被允许做什么),但无法控制具体的执行路径。如果恶意指令通过任何输入渠道到达智能体,而该智能体拥有一个通用的 shell 且没有额外的隔离措施,那么指令就会运行。API 密钥只能管控智能体通过身份验证 API 调用的内容,它无法管控生成的代码在宿主机上能做什么。

一个经过妥善沙箱化的智能体拥有四个截然不同的隔离层。削弱其中任何一层,你都需要依靠其他层来弥补。

第一层:容器隔离 —— 选择你的内核边界

第一个问题是智能体代码与你的宿主机基础设施之间存在什么样的内核边界。这里有四个实用的选项,每个选项都有不同的权衡点。

普通容器 (Docker/runc) 共享宿主机内核。逃逸容器命名空间的系统调用(syscall)会直接落到宿主机上。2025 年的三个 runc CVE 展示了挂载竞争条件,允许从容器内部向受保护的宿主机路径写入数据。只有在叠加了严格的 seccomp 和 AppArmor 配置文件的时,普通容器才是合适的——即便如此,只要存在一个内核漏洞,就离获得完整的宿主机访问权限仅一步之遥。

gVisor 在容器和宿主机内核之间介入了一个用户态内核。所有来自沙箱的系统调用在到达真正的内核之前都会经过 gVisor 的 “Sentry”。这消除了大部分内核逃逸路径,代价是 10–20% 的 I/O 开销,在系统调用密集型工作负载中开销更高。gVisor 非常适合性能开销可接受的通用 Python 工作负载,且你运行在 GKE 或兼容的 Kubernetes 环境中。

Firecracker microVMs 在 KVM 硬件虚拟化之上为每个沙箱提供独立的 Linux 内核。此时,逃逸沙箱需要的是虚拟机管理程序(hypervisor)逃逸,而不仅仅是系统调用逃逸——这是一个实质上更高的门槛。冷启动时间约为 125 毫秒;每个实例的内存开销约为 5MB。E2B 在独立的 Firecracker VM 中运行每个智能体沙箱,其在一年内从每月 4 万个会话增长到 1500 万个会话,这证明了运维开销是可控的。如果你正在大规模运行,并且在不使用裸机的情况下需要最强的隔离,那么 Firecracker 是正确的选择。

WebAssembly (WASM/WASI) 提供近乎零的冷启动和经过形式化验证的内存安全性,但目前的 Python 科学计算(如 NumPy、PyTorch 以及大多数 ML 工具)对 WASI 的支持尚不完整或完全不支持。WASM 适用于狭窄的纯计算任务,但不适用于通用的智能体工作负载。

实际基准:使用 Firecracker 或 gVisor 作为内核边界,并无论如何都要在顶层叠加 seccomp-BPF。一个经过良好调优的 seccomp 配置文件可以阻止 ptracemountpivot_root 和原始套接字(raw socket)的创建。即使在 microVM 内部,违反 seccomp 规则也会终止进程,从而在内核边界失效时提供深度防御。

第 2 层:文件系统命名空间 —— 每次路径访问都是一次决策

容器中的默认文件系统布局对于生成并运行代码的智能体来说过于宽松。生成的代码可能会遍历智能体本不该访问的目录、发现缓存在磁盘上的凭据、读取配置文件,或者写入在沙箱拆除后依然存在的持久化机制。

Linux 提供了精确修复此问题的工具。挂载命名空间(Mount namespaces)让你能为沙箱提供独立于宿主机的、专属的文件系统视图。Landlock(自 Linux 5.13 起可用)在内核级别提供基于权能(capability-based)的文件系统访问控制:你可以将智能体进程限制在具有特定访问模式的特定目录树中,且该限制会传播给由智能体生成代码所产生的所有子进程。

一个正确的智能体沙箱文件系统布局应该是:

  • 输入数据和参考文件:以只读方式挂载。智能体可以读取它们;生成的代码无法修改它们。
  • 工作空间输出目录:读写权限,范围限定在特定会话的路径(/workspace/{session-id}/)。此目录之外的任何内容都不可写。
  • /tmp:临时挂载,在沙箱拆除时清除。会话之间不保留持久化内容。
  • 系统目录:通过挂载命名空间设为不可访问 —— 智能体不需要看到 /etc/var/home
  • 机密信息和 API 密钥:在运行时通过机密管理器作为环境变量注入。严禁写入智能体文件工具可以读取的工作空间磁盘。

磁盘上的机密信息(secrets-on-disk)问题值得强调。一项 2025 年的 MCP 服务研究发现,当文件系统权限未限定在特定路径时,82% 的受测试服务器都容易受到路径遍历攻击。如果你的智能体拥有文件读取工具,且你的 API 密钥存在于宿主机文件系统的任何位置,一个足够聪明的提示词(prompt)就能找到它们。解决方法不是优化提示词 —— 而是将机密信息挂载为内存中的环境变量,并确保智能体的挂载命名空间无法访问工作空间以外的路径。

关键点:将阻断工作空间目录以外的所有写入操作视为硬性的内核级控制,而非仅仅是准则。持久化机制、沙箱逃逸和 RCE 准备阶段都需要向能够在会话结束后存留的位置写入内容。要从结构上移除这种能力。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates