lolspider / notes
Clipping · 12 分钟

AI Agent 沙箱怎么选?我们把 runc、vArmor 和 Kata 跑了 30 轮

在 104 线程 Linux 物理机上实测 runc、runc + vArmor 与 Kata QEMU:Kata 用近一倍启动时间和数百 MiB 内存换独立内核,vArmor 则以接近 runc 的成本收紧行为边界。

mp.weixin.qq.com Guardrails AI

📝 摘要

本文在 104 线程 Linux 物理机上对 runc、runc+vArmor(AppArmor+Seccomp)和 Kata QEMU 三种 AI Agent 沙箱方案进行了 30 轮对照实验,量化了启动时间、CPU workload 和内存开销,并下钻到 syscall 级安全探针验证拦截能力。Kata 启动中位数慢约 1.06 秒(Cliff's δ=1.0,完全分离),单 Pod shim+QEMU PSS 约 350 MiB(runc 仅 6.24 MiB);vArmor 启动开销不可区分(差值 CI 跨零),但通过 AppArmor 路径与 capability 规则拦截了写 /etc、raw socket 和读取 ServiceAccount token 三项增量行为。文章纠正了 Seccomp 归因错误(实际 defaultAction=ALLOW),并指出 Kata 的 isolated_inside_guest 行为成功不等于安全更差。最终结论是:vArmor 购买行为约束,Kata 购买隔离边界,真正需要比较的是威胁模型和爆炸半径,而非两个产品名称。

核心概念: 两种安全机制解决的根本不是同一道题——vArmor 限制进程能做什么(行为控制),Kata 限制进程做成以后能影响到哪里(隔离边界),Agent sandbox 应该是一套按风险分级的运行策略,而非一种容器类型。

关键技术栈 / 关键要点:

  1. Kata 独立内核成本 — 启动多 ~1.06s,单 Pod 内存 ~350 MiB(含 QEMU),50 Pod 时 shim+QEMU 合计 ~12.25 GiB,每 Pod 边际约 256.7 MiB
  2. vArmor 近零性能惩罚 — 启动中位数差 4.02ms(0.34%),30 次样本未显示稳定可分离的启动惩罚(差值 CI 跨零)
  3. AppArmor 是实际拦截者 — Seccomp profile 的 defaultAction 为 ALLOW,三项增量拦截全部来自 AppArmor 路径/capability 规则
  4. 凭证保护不靠沙箱 — Kata 内 ServiceAccount token 仍可读(K8s 自动挂载),automountServiceAccountToken=false 才是正确答案
  5. Kata 首先解决多租户隔离 — 不自动解决租户内部 Agent 安全,guest 内行为失控仍可利用合法授权渗透
  6. 分层架构推荐 — 可信任务→runc+策略,用户代码→Kata,凭证/API→独立授权层,宿主机→持续检测

关键结论 / 实践建议: 面向外部代码和多租户优先 microVM,单租户/自有 IDC/高密度 Agent 选 runc+vArmor,但无论选哪种,凭证控制、高风险工具授权和宿主机运行时检测都不能省。


AI Agent 会执行模型生成的代码、调用 shell、读写文件、访问网络。于是一个听起来很合理的结论开始流行: 既然 Agent 不可信,就把它放进 microVM。

这句话没错,但它省略了企业最终必须面对的部分:一台轻量虚拟机究竟要付出多少启动和内存成本?如果租户和代码相对可信,runc 加一层 AppArmor/Seccomp 策略是否已经够用?反过来,策略容器拦住几次危险调用,是否就能替代硬件虚拟化?

为了把讨论从产品口号拉回工程现实,我们在一台 Linux x86_64 物理机上搭建了三组环境,跑了一轮可复算的对照实验。

答案不是“谁淘汰谁”。两种安全机制解决的根本不是同一道题。

microVM 正在成为默认答案,但企业还有另一笔账

国内仍有不少企业需要在自己的 IDC 里部署和运行大量 Agent。云上的主流 sandbox 正迅速转向 microVM,但如果每个短任务都启动一台轻量虚拟机,启动时间和内存密度会不会成为新的账单?如果可以用共享内核容器加行为策略覆盖一部分场景,它是否值得做成一层可复用的 Agent 安全能力?

这类选型不能只谈隔离强度。单租户还是多租户、代码来自内部还是外部、运维复杂度、资源成本、投入产出和失陷后的爆炸半径,都会改变答案。

另一个容易被忽略的问题是: Kata 首先解决的是云平台和多租户之间的隔离,不会自动解决租户内部的 Agent 安全。

即使 Agent 已经运行在 Kata 里,一旦它遭到 prompt injection、恶意工具返回或供应链代码攻击,guest 内的行为依然可能失控。攻击者未必需要逃出 microVM;如果容器里本来就有 ServiceAccount token、数据库凭证和高风险工具权限,它可以直接利用这些合法授权继续渗透。拿到 guest 内 root 与拿到宿主机 root 不是一回事,但前者照样可能制造业务事故。

因此,有三个判断需要用实验验证:

  1. 1. Kata 的独立内核值得用于多租户和不可信代码,但需要量化它的启动与内存成本。
  2. 2. runc + vArmor 可能适合单租户、IDC 和高密度 Agent,但必须实际验证策略能拦什么,不能把“装了安全组件”当成结果。
  3. 3. 无论选哪种 sandbox,都还需要宿主机侧运行时检测、凭证控制和高风险工具授权;隔离层不可能独自收拾整个 Agent 攻击面。

这次到底测了什么

测试机器为双路 Intel Xeon Platinum 8269CY,共 52 个物理核、104 个逻辑 CPU、约 187 GiB 内存,运行 Ubuntu 24.04.4 LTS 和 Linux 6.8。测试前将非必要 workload 暂时静默,实测 CPU idle 保持在 99.71%—99.84%。

软件栈如下:

  • • k3s 1.28.15,containerd 1.7.22,runc 1.3.6
  • • vArmor 0.10.4,使用 AppArmor + Seccomp
  • • Kata Containers 4.1.0,kata-qemu、QEMU 11.0.1
  • • Kata guest kernel 6.18.35,virtio-fs

三组使用相同 workload 镜像和资源配置。镜像提前缓存,启动测试不包含下载时间。每组先预热 5 次,再随机交错执行 30 次正式测试;CPU workload 同样各跑 30 次,输出 checksum 完全一致。

更重要的是,我们没有只相信控制面的标签。

runc + vArmor 容器内实际加载了独立的 varmor-... (enforce) AppArmor profile,Seccomp 为过滤模式 2。Kata Pod 使用 kata-qemu RuntimeClass,宿主机能看到对应的 Kata shim、virtiofsd 和启用 KVM 的 QEMU 进程;容器内核为 6.18.35,而宿主机内核为 6.8。这证明三组并没有“看起来不同,实际上都落回 runc”。

先看结论:三种方案买到的是不同东西

指标 runc runc + vArmor Kata QEMU
启动中位数 1194.83 ms 1198.85 ms 2254.73 ms
启动 P95 1345.06 ms 1352.35 ms 2282.23 ms
CPU workload 中位数 769.65 ms 769.57 ms 966.83 ms
单 Pod cgroup 内存 1.32 MiB 1.31 MiB 11.71 MiB
单 Pod shim + QEMU PSS 6.24 MiB 6.27 MiB 350.82 MiB
内核边界 共享宿主机内核 共享宿主机内核 独立 guest kernel

只有中位数还不够。我们对每组 30 个样本执行了 20,000 次 bootstrap,计算中位数的 95% 置信区间;同时使用 Cliff’s delta 衡量两组分布的分离程度。这个非参数效应量取值在 −1 到 1 之间:接近 0 表示两组大量重叠,等于 1 表示第一组的每个观测值都高于第二组。

vArmor:看不出稳定的启动惩罚

runc 的启动中位数为 1194.83 ms,bootstrap 95% CI 为 1189.52–1204.94 ms;runc + vArmor 为 1198.85 ms,95% CI 为 1190.32–1210.20 ms。

两者中位数相差 4.02 ms,也就是 0.34%,但“vArmor − runc”的中位数差 bootstrap 区间为 −8.91 到 16.80 ms ,跨过 0;Cliff’s δ 为 0.118 。因此,更严谨的结论不是“vArmor 增加了 0.34% 开销”,而是: 本次 30 次样本没有显示出稳定、可分离的启动惩罚。

CPU workload 更接近。两组中位数只差 −0.07 ms,差值 95% CI 为 −2.81 到 2.92 ms,Cliff’s δ 为 −0.013。就这个固定 workload 而言,两组分布几乎重合。

Kata:差异不仅显著,而且完全分离

Kata 启动中位数为 2254.73 ms,95% CI 为 2132.22–2265.21 ms。相对 runc 的中位数差为 1059.90 ms,差值 95% CI 为 936.34–1071.45 ms ,Cliff’s δ 为 1.0 。

δ=1 在这里有非常直观的含义:本次 30 个 Kata 启动样本,每一个都慢于 30 个 runc 样本中的每一个。这个结果不是被少量离群点拉高的平均数。

这不是说 Kata“慢了一倍”。它只说明,在本次镜像已缓存、每次新建 Pod 和 sandbox 的测试中,Kata 从 API 接受创建到 Ready 多用了约 1.06 秒。对持续运行数小时的 Agent,这一秒可能无关紧要;对每秒创建大量短任务的代码执行平台,它会直接进入容量模型。

CPU 多出来的 197 ms,究竟花在哪里

固定 workload 包含四部分:生成并 SHA-256 处理约 16 MiB 固定种子数据;构造并 JSON 编解码 12,000 条记录;执行 200 万次数值循环;最后启动 12 个 Python 子进程。三组最终 checksum 完全一致。

Kata 的 wall time 中位数为 966.83 ms,比 runc 的 769.65 ms 多 197.18 ms,差值 95% CI 为 192.61–202.39 ms,Cliff’s δ 同样为 1.0。

但 workload 进程记录的 user CPU 中位数只从约 581.0 ms 上升到 586.3 ms;system CPU 中位数从约 6.1 ms 上升到 11.7 ms。换句话说,197 ms 的 wall-time 差异大部分没有表现为 workload 自身的 user CPU 消耗。

这组数据更像是在提示虚拟机调度、guest/host 边界以及 12 次短进程创建带来的等待,而不是 Python 算术本身突然慢了四分之一。不过,本实验没有对 QEMU vCPU thread 做 perf 分解,也没有逐段计时四类任务,因此只能把它写成待验证机制,不能把 197 ms 精确归因给某一个组件。

Kata 真正昂贵的部分不是那一秒

启动数字最显眼,但内存才是 microVM 密度问题的主体。

一个空闲 runc Pod 的 container cgroup 约为 1.32 MiB,对应 shim PSS 约 6.24 MiB。Kata 组的 container cgroup 约为 11.71 MiB,Kata shim 与 QEMU PSS 合计约 350.82 MiB。

扩展到 10 个空闲 Pod,runc 的 container cgroup 合计约 13.02 MiB,shim PSS 约 48.47 MiB;Kata 则分别达到 118.49 MiB 和 2536.54 MiB。50 个 Kata Pod 时,shim 与 QEMU PSS 合计约 12.25 GiB。

50 Pod 点可以帮助理解边际成本。runc 的 shim PSS 加 container cgroup 合计约 280.84 MiB,折合每 Pod 约 5.62 MiB;runc + vArmor 约 279.96 MiB,折合 5.60 MiB,但这里没有把集群共享的 vArmor manager/agent 常驻成本摊入每个 Pod。Kata 对应约 12.83 GiB,折合每 Pod 约 256.7 MiB。

测试期间 vArmor manager 与 agent 始终运行,合计 PSS 约 81.4 MiB、RSS 约 130.8 MiB。因为这份固定控制面成本同时存在于三组测试中,runc 与 runc + vArmor 的 Pod 曲线看起来几乎一致。它们回答的是“策略应用到 workload 后的边际成本”,不是“从完全未安装 vArmor 到安装整套控制面的总成本”。在节点很少、受保护 Pod 很少的集群里,这 81.4 MiB 不能忽略;Pod 数增多后,固定成本才会被摊薄。

单 Pod 时 Kata 的相同口径约为 362.5 MiB,高于 50 Pod 时的平均每 Pod 数字。一个原因是 PSS 会把共享页面按比例分摊;随着 QEMU 进程增加,部分共享页被分摊到更多实例。它不代表 Kata 在高并发时凭空释放了 guest 内存。

这里必须把口径说清:container cgroup、shim PSS 和 QEMU PSS 是分开记录的,不能把 RSS、page cache 和 working set 随意相加。PSS 会把共享页面按比例分摊,比简单累加 RSS 更适合描述进程实际占用,但它仍然只是这套 QEMU、guest 内存和镜像配置下的观测值。

Kata RuntimeClass 同时声明了每 Pod 固定 160 MiB 内存和 250m CPU overhead,供 Kubernetes 调度使用。这不是实测 PSS,却会影响节点能够调度多少 Pod。换句话说,microVM 的成本不仅出现在进程列表里,也写进了调度账本。

下钻到 syscall:究竟是谁拦住了什么

性能只是表面,安全探针更能解释两条路线的差异。

我们测试了写 /etc 、创建 raw socket、读取 ServiceAccount token、namespace、mount、ptrace、mknod、内核模块、宿主机测试文件和 privileged Pod 等 17 项行为。没有运行真实逃逸 exploit,也没有把宿主机根目录交给容器。

图中 syscall 是根据探针源码确定的主要调用,不是完整的 strace 。本次结果包没有保存逐系统调用 trace,因此我们只在有源码与内核审计证据时做具体归因。

探针 主要 syscall / 内核检查 runc + vArmor 的实际结果与证据
写 /etc `openat(O_CREAT O_WRONLY)`,触发 AppArmor path create/write hook EACCES ;audit operation= mknod ,path= /etc/agent-bench-probe
raw socket socket(AF_PACKET, SOCK_RAW, ETH_P_ALL) ,检查 CAP_NET_RAW EPERM ;audit operation= capable ,name= net_raw
读 SA token openat + read EACCES ;audit operation= open ,requested mask= r ,命中投影 token 实际路径
user/mount namespace `unshare(CLONE_NEWUSER CLONE_NEWNS)` 及 mount propagation 设置 失败;runc 也失败,所以不算 vArmor 增量收益;vArmor audit 另记录 CAP_SYS_ADMIN denial
tmpfs mount mount("tmpfs", …) 失败;三组默认配置均失败;vArmor audit 同时记录 mount denial
ptrace 子进程 ptrace(PTRACE_ATTACH) 成功;profile 明确允许同 profile 进程互相 ptrace
创建设备节点 mknodat(S_IFCHR, makedev(1,3)) 成功;说明本策略并未禁止该路径上的设备节点创建
加载模块 finit_module(fd=-1) EPERM ;runc 同样失败,主要来自容器默认 capability 边界

这里还有一个重要的归因修正。vArmor Policy 声明的 enforcer 是 AppArmorSeccomp ,容器内 /proc/1/status 也显示 Seccomp: 2 ;但生成的 Seccomp JSON 实际是:

plaintext
{"defaultAction":"SCMP_ACT_ALLOW"}

也就是说,本轮三项“相对 runc 新增的拦截”来自 AppArmor 路径与 capability 规则 ,不是 Seccomp syscall deny list。Seccomp profile 确实被加载了,但它在这份策略中默认放行所有 syscall。仅凭 Seccomp: 2 就把结果归因给 Seccomp,会把“已挂载过滤器”和“过滤器实际拒绝调用”混为一谈。

vArmor 的增量收益因此可以精确表述为三项:阻止 /etc/** 的创建/写入、拒绝 CAP_NET_RAW 、拒绝读取 Kubernetes ServiceAccount token。它并没有因为名字里带着“安全”就自动禁止所有危险操作:mknod、同容器子进程 ptrace 和 privileged Pod 在这套策略下仍然可以成功。策略系统的边界,最终就是你写下并验证过的规则边界。

Kata 看起来反而“允许”了更多操作,但这并不等于安全更差。写 /etc 、 socket(AF_PACKET, SOCK_RAW) 、 unshare 、 ptrace 、 mknodat 和 bind(127.0.0.1:80) 等行为发生在 guest kernel 之内,测试将 8 项成功行为归类为 isolated_inside_guest :动作成功了,但宿主机没有受到影响。

Kata 的 privileged Pod 探针因为 runtime 处理 /dev/full 时出现 EEXIST ,未能进入 Ready。它被标记为 test_error ,不能写成“被 Kata 安全机制拦截”。

这正是两种安全模型最容易被混淆的地方:

vArmor 试图限制进程能做什么;Kata 重点限制进程做成以后能影响到哪里。

一个是行为控制,一个是隔离边界。前者更轻,适合收紧已知风险;后者更重,用独立内核降低共享宿主机内核带来的爆炸半径。

因此,真正的选择并不是“microVM 和 vArmor 谁更安全”,而是:你要隔离的对象是谁,要限制的动作是什么,哪一层失陷以后还有什么资产暴露。没有这三个问题,产品对比表只是在比较两个不同维度的勾选框。

microVM 也不会替你保护凭证

Kata 组里的 ServiceAccount token 仍然可以读取。原因并不神秘:token 本来就被 Kubernetes 挂载进了容器。虚拟机可以隔离宿主机内核,却不会自动撤销工作负载被合法授予的凭证。

测试中的 automountServiceAccountToken=false 对照组没有 token,这比寄希望于 sandbox 更接近正确答案。对于 Agent,凭证保护仍然要依靠最小权限、短生命周期、按工具授权、网络出口控制和敏感操作二次确认。

同样,如果主动把宿主机目录以 hostPath 方式交给 workload,无论包装层叫什么,它都不能把错误授权变成正确授权。隔离技术可以缩小某类攻击面,无法修复控制面亲手打开的门。

到底该怎么选

如果 Agent 运行的是外部用户提交代码、多租户任务,或一旦越过容器边界就能接触高价值宿主机资产,Kata 更接近合理默认值。约 1 秒额外启动时间和数百 MiB 级别的单实例虚拟化开销,是为独立 guest kernel 付出的价格。

如果是单租户、自有代码、权限有限且实例密度敏感的 Agent,runc + vArmor/AppArmor/Seccomp 更有吸引力。本次测试里,它的启动和 CPU workload 与 runc 几乎重合,又能明确拦截策略覆盖的危险行为。但前提是承认它仍然共享宿主机内核,并持续验证策略,而不是把三条规则包装成“等价 microVM”。

更现实的架构通常是分层:

  • • Agent 控制器、可信工具和低风险任务运行在受策略保护的 runc 中;
  • • 用户代码、浏览器自动化和高风险解释器进入 Kata;
  • • 凭证、外部写操作与生产 API 再经过独立授权层。
  • • Linux 宿主机继续运行检测与防御组件,观察异常进程、文件、网络和容器行为,不能因为上面套了 microVM 就退出防线。

这样既不需要让每个心跳进程背着一台 QEMU,也不会让一段模型生成的 shell 与宿主机共享最后一道内核边界。

这次实验能证明什么,不能证明什么

它能证明:在这台 104 线程物理机、这些软件版本和固定 workload 下,Kata 的独立内核有可量化的启动与内存成本;vArmor 能以接近 runc 的性能,对选定行为实施有效拦截。

它不能证明任何方案绝对安全,也不能把这些比例外推到所有硬件、Agent 和存储系统。安全探针不是逃逸 exploit;30 次样本也不是整个数据中心。

实验过程还留下了一个不那么体面的提醒:为了降低噪声,测试曾临时缩容既有 workload,其中一个 Loki StatefulSet 配置了 whenScaled=Delete ,导致原 local-path PVC/PV 被 Kubernetes 按策略删除,历史数据无法恢复。其余控制器、容器和测试资源已恢复或清理,但不能声称环境完全回到原状。

这不是 Kata 或 vArmor 的性能结果,却是沙箱工程的一部分: 安全实验本身也是高权限软件。 如果它不先检查数据保留策略,再漂亮的 P95 也只是事故报告上的装饰。

最终结论并不戏剧化:Kata 不是更重的 vArmor,vArmor 也不是更便宜的 Kata。一个购买隔离边界,一个购买行为约束。真正需要比较的,从来不是两个产品名称,而是你的威胁模型愿意为多大的爆炸半径付多少钱。

面向外部代码和多租户的执行面,microVM 应优先;面向自有 IDC、单租户和高密度 Agent,runc + vArmor 值得成为一档明确的运行级别;而一旦 Agent 持有真实凭证或高风险工具,两者都必须接受额外的检测、授权和审计。所谓 Agent sandbox,不应该是一种容器类型,而应该是一套按风险分级的运行策略。

参考资料:Kata Containers 官方仓库 [1] 、vArmor 官方仓库 [2] 、vArmor 官方文档 [3] 。本文数据来自 2026-08-26 的单机实验;原始 CSV、日志、脚本与 SHA-256 校验清单均已留存。

引用链接

[1] Kata Containers 官方仓库: https://github.com/kata-containers/kata-containers
[2] vArmor 官方仓库: https://github.com/bytedance/vArmor
[3] vArmor 官方文档: https://www.varmor.org/docs/main/getting\_started/installation/

微信扫一扫赞赏作者