Agent 沙盒的文件系统权限设计:只读挂载、临时写入区、白名单目录——怎么防止数据泄露

我最早以为 Agent 沙盒是某种 AI 魔法——模型一调用工具,工具就被自动囚禁起来。直到我自己试着搭一个沙盒,才发现底层全是 Linux 的 mount 命令。而且我翻过车:明明把根文件系统设置了只读,结果 Agent 还是成功往 /etc 里塞了一个文件。排查了两小时,最后发现挂载参数的顺序写反了。

AI technology illustration

这件事让我明白:所谓“沙盒的文件系统权限设计”,不是什么新产品,而是把 Unix 几十年的隔离原语重新组合起来,解决一个新问题。今天想拆开讲三个核心组件:只读挂载、临时写入区、白名单目录

沙盒的底层:不是藏文件,而是决定文件在哪

在 Linux 上,一个进程能看见哪些文件,不是由“路径”决定的,而是由它所在的 mount namespace 和根目录决定的。mount namespace 文档说得很直白:每个 namespace 都有自己的挂载表。

沙盒要做的事,就是给 Agent 的进程一张新的挂载表。这张表里只有三样东西:只读的系统镜像、一个用完即焚的临时区域、几个你手动放进去的数据目录。除此之外,宿主上的 /home、/var、/tmp 统统不在表里。

换句话说,不是要“藏文件”,而是从进程的视野里直接删除这些路径。只要不在挂载表里,连路径都无法解析,更谈不上读写。

三套权限原语,解决三个不同的问题

初看这三个概念,容易觉得都是“限制写”。其实它们解决的泄漏模型完全不同。

权限方式 底层机制 核心动作 防止什么
只读挂载 bind + remount ro 禁止修改系统镜像 持久后门、篡改配置
临时写入区 tmpfs 写后即焚,不落盘 临时数据残留在宿主磁盘
白名单目录 bind mount 子目录 只暴露选中的数据集 越权读取宿主其他文件

三者的分工:只读决定了哪些东西不能变,tmpfs 决定了写入的东西会消失,白名单决定哪些东西能进来。

只读挂载:把系统盘变成一张只读光盘

最基础的一步是让系统文件“不可变”。做法一般是先 bind,再 remount 成只读:

mount --bind /rootfs /sandbox/rootfs
mount --remount,ro,bind /sandbox/rootfs

这样所有对 /sandbox/rootfs 的写操作都会收到 EROFS 错误。Agent 可以读、可以执行,但不能改。即使提示注入让 Agent 执行 rm -rf /,它也只能删除只读目录里的内容,删除不会生效。

但只读挂载不是万能的。如果可写区域挂载点(比如 /tmp)里面放了脚本,并且没有挂 noexec,那么依然可以运行任意代码。所以只读必须和临时写入区的挂载选项搭配。

临时写入区:能写,但写完等于没写

Agent 需要临时文件,因此必须给一个可写区域。最怕的是:可写区域落在宿主的 SSD 上,会话结束后留下大量垃圾文件。用 tmpfs 就没这个问题——数据写在内存里,容器或 namespace 一销毁,内容立刻归零。

Docker 的 –tmpfs 参数就是做这件事:

docker run --read-only --tmpfs /tmp:size=512M --tmpfs /work:size=256M,noexec,nosuid,nodev sandbox-image

注意第二个 tmpfs 我加了 noexec、nosuid、nodev。这是很多人忽略的——tmpfs 本身不设防,如果你允许执行其中的 setuid 文件,攻击者可能提权。挂载选项的每一个限制,都是权限设计的一部分。

我最早犯的错误是给 /tmp 挂了 noexec,结果 Python 的 multiprocessing 需要创建临时可执行文件,直接崩了。后来单独为缓存目录挂了一个可执行的 ramdisk,其他临时区保持 noexec。

白名单目录:只给你想给的那个抽屉

Agent 经常需要读取用户数据。比如上传一个 Excel,让它生成分析报告。这时你不能把整个 /home 挂给它,而应该用 bind mount 精确放行:

mount --bind /data/user-123/upload /sandbox/input
mount --remount,ro,bind /sandbox/input

设置之后,沙盒里的 /sandbox/input 就是宿主机 /data/user-123/upload 的影子。Agent 能看到这个目录,但看不到同一层级下的其他用户目录。目录本身还设成只读,避免 Agent 把计算结果回写到宿主。

但白名单目录有三个容易被忽视的坑:

  • 符号链接穿透:如果目录内有 symlink,指向沙盒之外的路径,正常情况下会因为路径不在挂载表里而失败。但若 symlink 指向了沙盒内其他可写区域,就可能导致数据被转移到可写缓存。所以挂载前要清理链接。
  • bind 默认是可写的:mount –bind 不会自动带上 ro,必须 remount,ro。很多人只 mount,不 remount,结果 Agent 一改,宿主机原文件也被改了。
  • 硬链接的逃逸:如果宿主上的文件允许硬链接到挂载文件,那么通过硬链接也等同于打开了另一个入口。所以宿主机也要开启保护。

但文件系统隔离只是半边天

很多人一聊防泄漏,眼睛全盯在文件系统上。实际上,数据泄露最常见路径不是“Agent 写文件”,而是“Agent 读文件后通过 HTTP 发出去”。文件系统做得再精细,只要网络是通的,你的数据照样可能被送进别人的服务器。

所以真正生产级的沙盒,绝不会只配 mount flags。以 gVisorFirecracker 为代表,还会做四层隔离:

  • 网络 namespace:默认禁止出站连接,必须通过显式代理或白名单域名。
  • 丢弃 capability:尤其 CAP_SYS_ADMIN。没有它,容器内无法 mount / remount。
  • seccomp / Landlock:过滤 mount、ptrace、socket 等高危系统调用。
  • 如果是多租户场景,直接用微虚拟机,而不是容器。

Firecracker 的设计更极端——它不模拟完整 PC,只保留必要的虚拟设备,让客户机内核的漏洞也摸不到宿主。这和我们用只读挂载 + tmpfs 的思路一致,只是把隔离边界从内核空间推到了硬件边界。

容器隔离 vs 微虚拟机:强度不在一个量级

如果你只是给自己本地的 Agent 用,mount namespace 加 tmpfs 已经足够。但如果是多租户服务,让陌生用户的 code 在你的宿主机上执行,容器级隔离就有点悬了。原因很简单:容器仍然共享宿主内核,mount namespace 只能隔离视图,不能隔离内核漏洞。一旦攻击者利用内核提权漏洞,sockets 都可能成为逃逸通道。

这就是为什么很多 Agent 平台会跑在 Firecracker 或 gVisor 上。Firecracker 是一个专门为无服务器场景设计的 micro VM,每个沙盒是一个极小的虚拟机,有自己的内核,和宿主只有虚拟化边界。它把 Agent 逃逸到宿主的概率压缩到“突破 KVM”这个级别,这比突破容器内核难几个数量级。

gVisor 则走另一条路:在用户态重新实现了一个“仿真内核”,把所有系统调用拦截下来,翻译成宿主安全操作。它的兼容性不如真内核,但隔离性比容器强,且开销比虚拟机低。两者都值得在架构里考虑。

实战三步,把设计落到实处

我建议你无论如何先按这个顺序思考,而不是先选技术栈:

  1. 建一个挂载表:把所有非必需的宿主任意目录从表里摘掉。
  2. 给可写区加栅栏:用 tmpfs 限制大小,加上 noexec、nosuid、nodev。
  3. 给外部数据开一个口子:用 bind mount 精确导入一个目录,并 remount 成只读。绝不放多个口子。

如果你用的是容器运行时,以上可以实现为:禁止设备、禁止新权限、只读 rootfs。我在一个内部服务里就是这样给一个可执行代码的 Agent 做了沙盒,几天内看到多次尝试读取 /etc/passwd 和往宿主 /var/tmp 写文件的日志,全部被 EROFS 挡住。老实说,那一刻很爽。

三个最容易问错的“为什么”

为什么不用 chroot?chroot 只改根目录,但不改挂载表。进程如果持有文件描述符,仍可访问外面的目录;而 mount namespace 配合 pivot_root 才能真正把外部路径从视野里移除。

tmpfs 会拖慢性能吗?恰恰相反,tmpfs 数据在内存中,读写极快。问题在内存会随写量增长,所以必须用 size 限定,否则一个失控的 Agent 能把宿主机内存写满。

只读挂载是不是和 r 权限位一样?不是。即使文件权限是 444,root 用户依然可以强制写入。只读挂载是在 VFS 层拦截一切写操作,连 root 也没办法直接破。

tmpfs 能防止其他进程读取吗?不能。tmpfs 只是不落盘,但同 host 上其他进程如果有权限,依然可以读。所以还需要严格的 mount namespace 和权限位配合。

这套设计的边界在哪?

它的边界就是“隔离”和“持久化”的矛盾。tmpfs 保证了会话结束数据即销毁,但如果你想让 Agent 在多步长任务里保留状态,就必须在宿主的持久层单独划一块配额目录,并加上审计。这个目录已经不在文件系统挂载的范畴,而是数据生命周期管理。

另外,文件系统隔离只能防 agent 破坏宿主文件,防不了 agent 把读到的机密原样写进聊天输出。后者要靠输出过滤和权限控制,比如限制模型对敏感字段的访问,而不仅仅靠 mount flags。

所以最后我的判断是:只读挂载、临时写入区、白名单目录是沙盒的地基,但绝不是安全设计的终点。它们真正解决的问题是让“某个进程无法以非预期的方式改变宿主”,而完整的数据防泄露,还需要网络隔离、系统调用过滤、输出审计一起构成纵深防御。永远不要假设一个 mount 参数可以锁死一切。

原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/467.html

(0)
上一篇 4天前
下一篇 4天前

相关推荐