跳到主要内容

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

· 阅读需 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 准备阶段都需要向能够在会话结束后存留的位置写入内容。要从结构上移除这种能力。

第 3 层:网络出站流量 —— “默认拒绝”是唯一可接受的默认设置

拥有不受限出站网络访问权限的智能体可能会将数据外泄到攻击者控制的基础设施、建立反向 shell、调用产生费用的外部 API,或利用基于 DNS 的外泄(将数据编码在 DNS 查询的子域名中)。在提示词注入攻击后,不受限的出站流量会将原本可控的事件演变为数据泄露。

控制栈按深度排序如下:

网络命名空间隔离为每个沙箱提供独立的网络栈。出站连接需要显式的路由规则 —— 不存在通往互联网的默认路由。

DNS 白名单限制沙箱可以解析的域名。这切断了基于 DNS 的数据外泄,并防止智能体在运行时通过查询攻击者基础设施的公共 DNS 来发现新的出站端点。

带有域名白名单的 HTTP 代理将所有 HTTP/S 流量通过一个显式代理路由。沙箱没有直接的互联网访问权限 —— 它只能与代理通信,由代理强制执行白名单。对未列出域名的请求将返回 403。这是大规模运营中最方便的控制方式,因为白名单可以集中管理且可审计。

按任务分配的出站策略仅对需要网络访问的沙箱应用该策略。代码格式化智能体根本不需要网络访问。网络调研智能体需要出站 HTTP 访问,但不需要 PyPI 访问。应根据任务类型而非智能体类别来建模所需的出站暴露面。

一个常见的反模式:在环境设置期间(为了安装包)授予广泛的网络访问权限,随后却没有撤销。正确的方法是在初始化阶段向包注册表(PyPI、npm、apt)开放出站访问,对环境进行快照,然后在智能体开始执行用户指令的代码之前重新锁定出站访问。初始化之后,除非任务明确需要,否则智能体的沙箱不应能够访问包注册表或任何其他外部端点。

第 4 层:凭据范围限定 —— 动态智能体需要动态凭据

传统软件具有静态凭据需求。你可以在部署时审计它们、限定凭据范围,然后就大功告成了。智能体系统在推理阶段根据 LLM 的推理调用工具 —— 为给定任务进行的 API 调用的确切集合无法完全预测。静态凭据范围限定是必要的,但还不够。

以下三种模式可以应对智能体凭据使用的动态特性:

每个会话的短期令牌。智能体进行的任何集成 —— MCP 服务器连接、外部 API 调用、数据库访问 —— 都应该使用随会话过期的令牌,而不是长期的 API 密钥。泄露一个有效期 10 分钟的令牌,其爆炸半径是有限的;而泄露一个 API 密钥则不然。

即时提权(Just-in-time elevation)。智能体最初拥有一组只读凭据。对于需要更高权限的操作 —— 删除、发布、部署 —— 智能体会针对特定操作申请提权,获得一个仅对该单次调用有效的受限一次性令牌,且提权过程会作为审计事件记录下来。这种借鉴自 PAM(特权访问管理)的模式在智能体场景中虽不常见,但直接适用。

每个智能体一套凭据。在智能体之间共享凭据意味着任何一个智能体被攻破,所有智能体都会被攻破。编排智能体不应同时持有所有下游智能体的生产环境 API 密钥。每个智能体或插件都应获得由机密管理器在会话开始时颁发的独立的低权限凭据。

MiniScope 框架(伯克利,2025 年)将自动权限最小化规范化:它根据工具调用之间的关系构建权限层级图,并求解在仍能完成任务的情况下的最小凭据集。其开销比原生工具调用仅高出 1%–6%。实际的启示是,你可以自动执行凭据范围限定决策,而不是完全依赖人工审计 —— 但你仍然需要审查并批准生成的范围。

能力审计:你如何知道自己做得对不对?

上述四个层级是控制措施。能力审计则是你验证这些措施是否奏效,以及是否授予了超出必要权限的方法。

静态审计:列举智能体(agent)可以 访问 的每一个工具,无论它是否调用过。如果你无法提供这个列表,那么你已经违反了最小权限原则。这个列表是审计的起点——每一项都需要说明智能体为什么需要它。

动态审计:在具有代表性的生产任务分布中运行智能体。记录每一次工具调用。将实际调用的分布图与声明的工具集进行对比。在任何任务分布中都从未出现在调用日志中的工具,都是应该移除的对象。出现频率极低的工具则是缩小权限范围的对象。

基于追踪的策略生成:像 Progent(2025 年 4 月)这样的工具允许你使用 JSON DSL 来表达运行时策略,从而限制哪些工具调用在哪些智能体状态下是允许的。策略可以由 LLM 根据任务描述生成,然后由人工审核。执行是确定性的——而不是容易被破解的 LLM 护栏——在保持任务完成率的同时,将标准化基准测试中的攻击成功率降至接近于零。

OWASP 针对智能体应用的最小代理权限审计标准将测试框架简化为三个问题:智能体能否接触到其任务范围之外的工具?这些工具运行时的权限是否超出了任务所需?高影响且不可逆的操作能否在没有人工审核的情况下进行?如果任何一个问题的答案是“是”,审计就不及格。对于高风险操作,人工在环(human-in-the-loop)审批是一种安全控制手段,而不是一种用户体验的权衡。

最后,为每个智能体标记自主级别——从完全受监督(每个动作都需要确认)到完全自主(无人工参与)。更高的自主级别需要成比例地进行更严格的沙箱化、更频繁的重新审计以及更低的异常告警阈值。这创建了一个运营框架,用于推理哪些智能体需要最强的隔离,而哪些可以运行在较轻的控制之下。

那些从未发生的事故

这一套技术栈的具体价值在于,它将一类事故从“可能发生”转变为“从架构层面杜绝”。一个运行在 Firecracker 虚拟机中、拥有命名空间化文件系统、默认拒绝出站流量以及会话作用域凭证的智能体,无法通过网络外泄数据——内核不会路由这些数据包。它无法在工作区之外写入持久化机制——挂载命名空间阻止了它。它无法获取它不持有的生产环境凭证——凭证管理器从未向它发放过。提示词注入仍然会影响智能体,但它只是运行在一个没有出口的盒子里。

在完成所有四个层级的防御后,剩余的攻击面会小得多:攻破沙箱编排器本身、利用 Hypervisor 漏洞或是在允许的工具范围内操纵智能体的行为。这些是更难解决的问题,需要不同的缓解措施——Hypervisor 加固、编排器访问控制和行为异常检测。但至少你是在解决难题,而不是把容易的漏洞暴露在外。

处理敏感工作负载的智能体系统开发团队,已经学到了企业软件在 2000 年代初期学习深度防御(defense-in-depth)时的同样教训:通常是在发生了一次大到需要写复盘报告的事故之后。隔离技术栈已经存在,工具链也已经成熟到可以用于生产环境。唯一的变量是,你是在需要向别人解释为什么你的智能体成为了横向移动向量之前,还是之后构建它。

References:Let's stay in touch and Follow me for more thoughts and updates