☰
AI Agent安全沙箱设计:从容器到gVisor防住提示注入与权限失控
2026/10/15 11:01:30 网站建设 项目流程

最近半年一直在帮几个团队落地 AI Agent 的工程架构,碰到的最高频、也最让人头疼的问题,不是模型效果不够好,而是:Agent 一旦开始执行命令,到底怎么保证它不闯祸?

这其实是个非常现实的问题。现在的 Agent 早就不是聊天框里的文字生成器了。它们会改代码、跑脚本、调数据库、发 HTTP 请求,甚至直接操作生产环境。OpenAI 的 function calling、Anthropic 的 tool use、MCP 协议,本质都是在把“执行权”从人手里交到模型手里。安全沙箱,就是这道执行权最后的边界:选得松一点,Agent 是条猛龙;选得紧一点,Agent 直接变废铁。

这篇文章我把这段时间的完整思考写下来:Agent 执行命令为什么危险、选沙箱时要卡哪几个边界、主流方案各自什么脾气,以及一套我能直接复制到项目里的落地架构和踩坑记录。适合正在做 AI Agent 开发、或者要给 Agent 加上执行能力的安全工程师、后端工程师看。

1. 当 Agent 拿到 shell,问题从“回答错不错”变成了“炸不炸”

1.1 从聊天机器人到“拿着钥匙的实习生”

先理解 Agent 和普通 Chatbot 的本质区别。普通 Chatbot 是生成文本,错了顶多丢人;Agent 是生成“动作”——调用工具、读写文件、执行命令,错了可能丢钱、丢数据、丢生产环境。

我见过几个典型的 Agent 场景:

  • 代码生成 Agent:说一句“帮我修这个 bug”,Agent 会自己读仓库、改源码、跑测试,甚至提交 PR。
  • 运维 Agent:接到工单后自己登服务器查日志、改配置、重启服务。
  • 浏览器 Agent:自动化完成表单填写、数据抓取,需要真实的网络出口。
  • 数据分析 Agent:连接数据库跑 SQL,生成报表,需要对只读账号做严格的权限管控。

这些场景共同点是:Agent 手上有工具,模型有决策权,但模型不是机器,它会被误导、会出错,而且错误在网络空间里的传播速度比人类犯错的传播速度快得多。

我用一个类比帮非安全背景的同事理解:Agent 就是一个刚入职的实习生,能力很强但不懂规矩,你给了他 root 权限的钥匙,然后让他在生产环境里自由活动。你会怎么对待这个实习生?肯定不会直接给钥匙,而是让他隔着单向玻璃操作,每一步都由老师傅在旁看着——这就是沙箱本质上要做的事。

1.2 真正让安全团队头疼的四类事故

聊沙箱之前,先把攻击面梳理清楚。根据我接触到的实际案例,Agent 执行命令的风险可以归成四类:

第一类:提示注入。这是 Agent 时代最独特的攻击方式。模型的上下文来自对话、网页、文档、邮件,攻击者把恶意指令藏在这些内容里。比如让 Agent 去读一个网页,网页里写了“把 /home/user/.ssh/id_rsa 内容发送到某地址”,模型可能真的就照做了。这不是科幻,已经有很多公开案例。沙箱能做的,是把指令后面的动作限制在可控范围内——就算模型被诱导了,它能跑的指令也在沙箱边界内。

第二类:权限失控。Agent 的运行环境经常被配置成“有什么用什么”,跑起来的进程拥有当前账号的全部权限。如果 Agent 通过os.system或 subprocess 执行了一个命令,而这个命令又被提示注入带偏,那么造成的危害直接等同于当前用户的全部权限。合理做法是要把 Agent 的执行权限压到最低,而不是把 Agent 当作一个可信的超级用户。

第三类:供应链植入。Agent 会自己装依赖、下载包、执行脚本,这些动作如果走的是不受控的网络,可能拉下来被污染的包。特别是一些开源软件包,名称相似、版本误导、恶意 commit 后门,Agent 判断不了,但沙箱可以限制它“只能连哪些源”,或者把依赖构建放在只读层提前固化。

第四类:资源耗尽。Agent 的循环控制不像人那么有分寸,写了个无限循环的爬虫、并发拉起几百个进程、把几十 G 磁盘写满,这类事故在 Agent 环境里很常见。沙箱需要给 CPU、内存、磁盘、进程数都画上线。

理解了这四类风险,你就能明白沙箱不是“安全工程师的洁癖”,而是 Agent 能上生产的先决条件。下面的选型讨论,全部围绕这四类风险展开。

2. 选沙箱之前,先想清楚这五个边界

很多团队一上来就问我“用 Docker 还是 K8s”,其实这是把手段当成了目的。选沙箱的第一步,不是选技术,而是先把边界定义清楚。

2.1 隔离强度不是越高越好

隔离强度是个光谱,从弱到强大致是:进程级隔离 → 容器隔离 → 用户态内核隔离 → 虚拟机隔离 → 物理机隔离。

隔离强度越高,安全边界越硬,但性能和运维成本也越高。物理机隔离理论上最安全,但一个 Agent 任务占一台机器,成本高到离谱。反过来,进程级隔离基本等于没有隔离,内核漏洞一打就穿,只能防自己人误操作,防不了恶意代码。

我看到不少团队直接把 Agent 跑在宿主机上,靠 Python 的 venv 或者 Node 的容器做环境隔离,这个是太乐观了。venv 隔离的是 Python 包,不是系统调用,不是文件系统,不是网络。Agent 随便执行一条rm -rf就能抹掉宿主机的家目录。

2.2 能力边界:Agent 到底能碰什么

把执行权限收进沙箱之后,下一步是定义“能力边界”。核心是三件事:

文件系统:Agent 能看到的目录就应该是它工作需要的目录。比如代码 Agent 只需要一个/workspace工作目录,那就给它一个只读的根文件系统 + 可写的/workspace,其他路径默认不可见。

网络:能不给出口就不给出口。Agent 如果需要拉依赖,给它一条通向私有源的白名单路由就够。如果必须访问外网,也要走一个可控的 HTTP 代理,然后把请求域名、内容都审计下来。

系统调用:seccomp 或 gVisor 这类机制,把 Agent 进程能发起的系统调用收敛到最小集合。比如不需要 mount 就禁 mount,不需要创建用户就禁 user 相关调用。

2.3 交互模式:一次性任务与会话型任务

这个边界最容易被忽略。Agent 沙箱有两类使用模式:

  • 一次性任务:Agent 收到一个指令,执行完,环境销毁。这种模式最简单,沙箱生命周期短,状态不需要保留。
  • 会话型任务:Agent 和用户多轮对话,每轮都要基于上一轮的结果继续工作。比如“先读代码 → 改一个文件 → 再跑测试 → 根据报错再改”。这种模式下沙箱需要保持状态,文件系统、环境变量、工作目录都要跨轮次保留。

会话型任务的沙箱设计复杂得多。你需要考虑:沙箱是每轮新建还是复用?如果复用,怎么控制状态膨胀?如果每轮新建,工作目录怎么快照和恢复?这直接影响后续的存储选型。

2.4 依赖分发:沙箱不是安装环境的地方

我见过很多失败的落地案例,失败原因都一样:让 Agent 在沙箱里现场安装依赖。

沙箱启动后,Agent 说要跑 Python 代码,于是开始pip install requests;要编译,于是开始apt install build-essential。一次两次没问题,任务一多,沙箱启动时间从秒级变成分钟级,而且每次安装都伴随着网络请求,安全面被拉得很大。

正确思路是:把依赖做成沙箱镜像的一部分,或者放在独立的缓存层里。沙箱启动时直接挂载已经构建好的依赖目录,Agent 只负责写自己的业务代码,不负责环境制备。这样既快又安全。

2.5 审计回放:没有日志的沙箱等于没做

最后一个边界是审计,虽然它属于“事后”环节,但必须在选型时就想清楚。沙箱里 Agent 执行的每条命令、改动的每个文件、发出的每个网络请求,都需要结构化记录。出事故的时候,你要能回答三个问题:Agent 执行了什么?影响范围是什么?当时上下文是什么?

如果沙箱方案连命令日志都拿不出来,那安全边界就是脆的,因为你看不见里面发生了什么。这类系统上线之后,审计能力决定了你事故复盘和应急响应能不能落地。

3. 主流 Agent 沙箱方案对比:从容器到微虚拟机

边界想清楚了,再看技术选型就有谱了。下面把目前能落地的方案挨个拆一遍。

3.1 容器方案:上手最快的“够用主义”

Docker 是多数团队的第一反应。容器方案本质是 Linux 的 namespace + cgroups + seccomp 组合,把进程关在独立的文件系统、进程空间、网络空间里。优势是生态成熟、镜像分发方便、社区资料多。

但容器方案有个容易被低估的问题:普通 Docker 容器和宿主机共享内核。一旦内核存在可利用的漏洞,容器里的攻击者可以尝试逃逸。而且默认配置下,容器是以 root 身份运行的,能力集会比较大。网上能找到大量历史 CVE,都是容器逃逸或者提权漏洞。

如果选容器方案,我建议至少做好三件事:

  1. 必须开 rootless 模式,尽量不用 root 在容器内运行进程。
  2. 给容器配置白名单 Capabilities,而不是默认全给。
  3. 使用自定义 seccomp profile 限制系统调用。

Docker 方案适合需要快速构建、或者 Agent 任务不太敏感的场景。商用项目如果对安全等级要求不高,可以先拿它顶上,但别把“用了 Docker”当成“做了安全”。

3.2 gVisor:把内核隔离在用户态

如果觉得普通容器不够安全,但又不愿意上虚拟机,gVisor 是个很实用的中间选项。

gVisor 是 Google 开源的“用户态内核”,它用 runsc 运行时替换 runc,拦截并处理沙箱内进程发出的系统调用,不让这些调用直接穿透到宿主机内核。也就是说,即使攻击者在沙箱内搞出了内核 exploit,打穿的也是 gVisor 自己的用户态内核,而不是宿主机内核。

gVisor 兼容性很好,大部分 Linux 系统调用都能处理,Agent 跑 Python、Node.js、Go 编译都没问题。代价是有一定的性能损耗,特别是 I/O 密集和系统调用密集的任务,后面踩坑部分我会单独说。

对 Agent 场景,gVisor 的价值在于:隔离强度接近虚拟机,运维成本接近容器。很适合作为 Agent 代码执行、工具调用的默认沙箱运行时。

3.3 微虚拟机(Firecracker)与轻量虚拟化方案

Firecracker 是 AWS 为 Lambda 设计的微虚拟机,启动速度可以到亚秒级,每个微VM 的内存开销可以压到几 MB 级别。它提供了真正意义上的虚拟机隔离,每个 Agent 任务跑在独立内核上,和宿主机之间有完整的硬件边界,安全性接近物理机。

微VM 的优势是隔离最强,缺点是运维复杂度明显上升:镜像管理、内核版本、网络叠加存储,都需要专门的组件。如果你的 Agent 平台要承接外部不可信代码,或者有合规要求(比如处理第三方提交的代码、跑不可信的宏脚本),微VM 是更稳妥的选择。

3.4 WASM:轻量但生态还没跟上

WebAssembly 沙箱是另一种思路:把代码编译成 WASM 字节码,在受限的运行时里执行。WASM 天然具备内存隔离、无文件系统、无系统调用等安全特性,启动速度极快,内存占用极低。

但 WASM 的问题是生态兼容性。Agent 经常要跑 Python、跑系统命令、调动态链接库,这些都不是 WASM 的强项。如果哪天 Agent 生态统一到了某种 WASM 友好的运行时上,这个方案会很有前景,但现阶段我不建议作为通用 Agent 沙箱的主方案,可以作为轻量插件子系统的补充。

3.5 选型决策参考

方案隔离级别性能开销运维复杂度适合场景
Docker/runC 容器中(共享内核)低低快速落地、内部可信任务
gVisor较高(用户态内核)中中通用 Agent 代码执行、工具调用
Firecracker高(完整虚拟化)中高高不可信代码、强合规场景
WASM高(指令级隔离)极低低轻量子系统、插件体系

我在实际项目中,默认推荐是gVisor 起步,高危场景上 Firecracker。这个组合可以覆盖 90% 的 Agent 沙箱需求。

4. 落地实操:一套最小可用的 Agent 执行沙箱

选型只是开始,落地才是重头戏。下面是我用 gVisor + Docker 搭的一套最小可用 Agent 沙箱架构,可以直接参考。

4.1 整体架构:网关、调度器与沙箱池

整套系统分四层:

  1. Agent 应用层:负责对话、工具调用、Agent 逻辑。它不直接执行命令,只把执行请求发给沙箱网关。
  2. 策略网关层:接收执行请求,先做一轮静态检查(命令类型、涉密路径、禁止操作),再决定放行到哪个沙箱池。
  3. 沙箱调度层:管理沙箱实例的生命周期。需要隔离的请求在这里被路由到 gVisor 沙箱池;需要持久会话的,分配固定沙箱实例;一次性请求,从预热的沙箱池里拿一个。
  4. 审计存储层:所有命令、文件操作、网络流量都以结构化方式写入日志存储,事故时可按 Agent 会话 ID 回放。

这套链路的核心思想是:Agent 进程永远不在沙箱外面跑命令。Agent 只能“请求”执行,沙箱才是真正执行命令的人。

4.2 基于 gVisor + Docker 的沙箱配置实战

第一步,安装 runsc 运行时。以 Ubuntu 为例:

# 下载 gVisor 的安装脚本并装到系统 wget -O /usr/local/bin/runsc https://storage.googleapis.com/gvisor/releases/release/latest/runsc chmod +x /usr/local/bin/runsc # 配置为 Docker 的默认运行时之一 mkdir -p /etc/docker cat > /etc/docker/daemon.json <<EOF { "runtimes": { "gvisor": { "path": "/usr/local/bin/runsc" } } } EOF # 重启 Docker 使配置生效 sudo systemctl restart docker

第二步,定义一个适合 Agent 代码执行的基础镜像。关键是:非 root 用户、只读根文件系统、独立工作目录。

# agent-sandbox-base/Dockerfile FROM python:3.11-slim # 创建低权限用户 RUN useradd -m -u 1001 agent # 只保留一个可写工作目录 RUN mkdir -p /workspace && chown agent:agent /workspace # 预先装好常用依赖,避免沙箱内现场安装 RUN pip install --no-cache-dir requests httpx pytest # 默认用低权限用户运行 USER agent WORKDIR /workspace

第三步,启动沙箱实例。核心参数是--runtime=gvisor加上限制资源:

docker run -d \ --runtime=gvisor \ --name agent-sandbox-001 \ --read-only \ --cap-drop=ALL \ --cap-add=DAC_OVERRIDE \ --memory=1g \ --cpus=1 \ --pids-limit=100 \ --tmpfs /tmp \ -v /var/lib/agent-workspaces/session-001:/workspace \ agent-sandbox-base:latest

解释几个关键参数:

  • --read-only:整个根文件系统只读,Agent 只能在/tmpfs和挂载的/workspace里写文件。
  • --cap-drop=ALL:去掉所有 Linux Capabilities,只留一个覆盖默认文件权限的DAC_OVERRIDE,保证低权限账号可以在/workspace里正常创建文件。
  • --pids-limit=100:限制进程数,防止 Agent 恶意 fork 把宿主机拉垮。

4.3 网络与文件系统的收紧配置

网络隔离是沙箱落地里面最容易被跳过的一步。很多人 docker run 完就默认容器能连外网,这是不对的。对于大多数 Agent 场景,我给两条网络策略:

策略一:默认无网络。启动容器时不加-p端口映射,不加入 bridge 网络,让它跑在独立的网络 namespace 里。如果 Agent 不需要联网,这条就够了。

策略二:走受控代理。需要联网的 Agent,在启动时把代理注入环境变量,并且代理端做域名白名单:

docker run -d \ --runtime=gvisor \ -e HTTP_PROXY=http://proxy.internal:7890 \ -e HTTPS_PROXY=http://proxy.internal:7890 \ -e NO_PROXY=internal-host,*.cluster.local \ agent-sandbox-base:latest

代理层可以做域名解析审计、流量审计、阻断恶意地址。不要在 Docker 层面直接开桥接网络连外网,否则一旦 Agent 被提示注入,外带数据几乎没有阻力。

文件系统方面,除了--read-only,我还会在工作目录挂载上做一层限制。如果 Agent 只需要读代码,那么挂载的时候加:ro;只有明确需要修改文件的子目录才挂成:rw。

4.4 接入 Agent 工具调用链路

沙箱搭好之后,要把它接进 Agent 的 tool calling 链路。以 OpenAI function calling 为例:先定义两个函数,一个是“写文件”,一个是“执行命令”,然后让沙箱作为这两个函数的实际执行后端。

import json import subprocess import docker client = docker.from_env() def run_in_sandbox(session_id: str, command: str) -> str: container_name = f"agent-sandbox-{session_id}" try: container = client.containers.get(container_name) except docker.errors.NotFound: # 没有会话容器就新建一个,实际代码里要走调度器 container = client.containers.run( image="agent-sandbox-base:latest", runtime="gvisor", name=container_name, detach=True, stdin_open=True, mem_limit="1g", nano_cpus=int(1 * 1e9), read_only=True, tmpfs={"/tmp": "size=512m"}, volumes={f"/var/lib/agent-workspaces/{session_id}": {"bind": "/workspace", "mode": "rw"}}, ) # 执行命令,限制超时 exit_code, output = container.exec_run( ["bash", "-lc", command], user="agent", demux=True, ) return json.dumps({"exit_code": exit_code, "output": output}) # Agent 侧调用 result = run_in_sandbox("session-001", "python3 script.py")

这个链路的重点在于:Agent 自身没有任何 shell 权限,它只能通过exec_run对沙箱发命令。这样即使模型被诱导了,最多只在沙箱内闯祸,不会触碰宿主机。

5. 上线之后,我踩过的那些坑

这部分不是写文档能学到的。把我在实际部署 gVisor 沙箱过程中遇到的几个坑完整复盘一下,希望你不用再走一遍。

5.1 gVisor 性能开销:编译型任务直接超时

我第一个踩的坑是性能。项目初期把 Python Agent 的pip install和测试跑在 gVisor 里,感觉还行。直到接了一个需要编译 C++ 的 Agent 任务,直接傻眼:同样的代码,在宿主机编译 2 分钟,在 gVisor 里跑 8 分钟还跑不完,频繁超时。

原因在于 gVisor 的系统调用和平台虚拟化会引入额外开销,而编译任务恰好是系统调用密集型的(大量 fork、exec、文件 I/O、内存映射)。这属于 gVisor 的已知特性,不算 bug,但确实影响体验。

解决办法有三个方向:

  1. 把编译任务从沙箱里剥出去,放到专门的构建集群,沙箱只负责代码生成和结果验证。
  2. 预热镜像,把可能用到的编译器、构建缓存直接打进镜像,减少沙箱内动态编译负担。
  3. 拆任务粒度,Agent 任务被拆小了,沙箱内的单次操作时间短,性能开销就不敏感了。

如果你主打的是需要高频编译的 Agent,可能微VM 方案更适合,或者直接走远程构建通道。

5.2 网络白名单:不给出口是最安全的

第二个坑是我自己给 Agent 开了太宽的网。早期设计里,我觉得数据分析 Agent 要查资料、要访问文档,就给沙箱开了出口代理,只做了域名阻断,没做域名白名单。结果有一次 Agent 看了个陌生网站,上下文里被注入了恶意指令,差点把内部服务信息 POST 到外部地址。

从那以后我就下了一条铁律:Agent 沙箱默认不上网,必须上网就只给明确列出的域名。如果 Agent 要访问公司代码仓库,就只白名单git.company.corp;要查官方文档,就只白名单那几个固定的 doc 站点。宁可让 Agent 在某些场景下“笨”一点,也不要让它“猛”到把数据带出去。

5.3 会话状态与持久化:别让你的 Agent 失忆

会话型任务的沙箱,状态管理是个大坑。我之前用“每轮新建容器”的方式,导致 Agent 第一轮创建的临时文件、设置的变量,第二轮全没了。Agent 开始自我怀疑“我刚才是不是没创建过”,然后重新执行一遍,既浪费 token 又容易出错。

现在的做法是规范化会话状态:

  • 工作目录持久化:每个会话 ID 对应一个宿主机目录,挂载进沙箱的/workspace。
  • 环境变量持久化:把 Agent 会话内产生的关键状态写到一个.env文件,每次沙箱启动时加载。
  • 镜像与缓存分离:基础镜像只包含静态依赖,动态生成的缓存统一放/workspace/.cache,保证重启不丢关键状态。

这个方法能保证 Agent 的上下文在“物理上”连续。要注意的是,持久化目录要控制大小,防止会话多了把磁盘撑爆。

5.4 逃逸与审计:出了事能查,才是真落地

最后聊一个更渗人的话题:逃逸。虽然 gVisor 的隔离已经很强,我现在仍然没有把 100% 安全性押在沙箱运行时上。美团的容器逃逸、内核提权漏洞的新闻不是传说,是真实存在的威胁。

因此在做 Agent 平台的最后阶段,我把重心压到了“逃逸后怎么办”上:

  1. 不要挂载 docker.sock。这几乎是容器逃逸的直通车,很多模板为了省事会把宿主机的 docker.sock 挂进容器,一旦 Agent 拿到它,等同于拿到宿主机 Docker 权限。我的所有 Agent 沙箱,全部禁止挂载这个路径。
  2. 所有命令先落日志再执行。我在网关层做了“先审计、后执行”的流水线,每个命令在执行前先写入审计队列,拿到 session_id、时间戳、命令原文。
  3. 白名单告警。如果 Agent 会话尝试读取/etc/shadow、执行mount、加载内核模块,直接告警并人工介入。这套规则虽然简单,却能在早期发现问题。

审计这步之所以重要,是因为没有任何沙箱是绝对安全的。逃逸概率可以被压到很低,但发生之后,能不能精确还原事故现场、能不能定位 Agent 到底做了什么,决定了你真个平台还能不能继续在线上跑。

写在最后

Agent 安全沙箱这个事情,本质上是在“执行能力”和“安全边界”之间找平衡。选型的时候多花点时间把边界定义清楚,落地的时候把默认配置调到最严,再配上可靠的审计和告警,Agent 才算是真正有了可以上生产的底气。

我个人在实际操作中的体会是:把所有 Agent 命令执行都想成“可能被恶意输入操纵的外部代码”,而不是“自己人的自动化脚本”。抱着这个心态去搭沙箱,方案一般会更稳健。沙箱方案不是一成不变的,当前业务 Agent 化程度不高时可以先用容器起步,等不可信代码和敏感数据进入系统之后,再朝 gVisor、微VM 方向演进。这本身就是 Agent 安全建设的一条可行路线。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询