拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI Agent 沙箱隔离实战:namespace、seccomp 与权限收窄

AI Agent 沙箱隔离实战:namespace、seccomp 与权限收窄 1. 从一个真实事故说起为什么“能干活”的 Agent 反而最危险去年年底我帮一个朋友排查过一起挺典型的数据事故。他团队做了一个内部用的 AI Agent定位是“全能工作助手”——能读本地文件、能跑脚本、能连数据库、能调内部 API还能帮运营同学自动整理报表。上线两周一切正常第三周出事了Agent 在一次“清理临时文件”的任务里把项目根目录下一个名字里带tmp的源码目录整个删了连带把一份还没入库的客户数据 CSV 也清掉了。事后复盘问题根本不在模型“理解错了”而在于这个 Agent 从头到尾就是以当前登录用户的完整权限在跑它想删什么就能删什么想读什么就能读什么。这件事让我彻底改变了对 AI Agent 架构的看法。很多人搭 Agent 的时候脑子里想的全是“怎么让它能力更强”——接更多工具、给更多权限、能操作更多系统。但真正决定一个 Agent 能不能上生产的往往不是它有多能干而是它干坏事的时候能被限制到什么程度。这就是沙箱Sandbox存在的意义。所谓沙箱说白了就是给 Agent 划一个“圈”圈里的东西随便折腾圈外的碰都碰不到。它不是一个功能而是一整套隔离机制的组合涉及文件系统隔离、进程隔离、网络隔离、权限降级、系统调用过滤等多个层面。热搜词里出现的seccomp、namespace、权限、代码沙箱其实都是这套机制里的具体零件。这篇文章我想聊的不是“怎么从 0 到 1 搭一个 Agent”那种入门内容而是更偏工程落地的一个问题当你的 Agent 已经能干活了怎么给它套上合适的笼子让它在拥有“工作助手”能力的同时不至于变成一颗随时会炸的雷。适合已经动手写过 Agent、准备往生产环境推、或者正在被权限问题折磨的开发者看。如果你还在纠结选哪个模型、用哪个框架这篇可能稍微超前了一点但提前了解隔离思路绝对不亏。2. 拆解核心问题Agent 的权限模型到底哪里出了岔子2.1 传统软件的权限边界为什么在 Agent 面前失效了传统软件有个基本假设代码的行为是确定的。你写了一个删除文件的函数它只会在你调用它的时候删你指定的文件。权限系统就是围绕这个假设设计的——给进程分配一组权限进程在这个权限范围内做确定的事。但 Agent 打破了这个假设。Agent 的行为是模型在运行时动态决定的。你给它一个“帮我整理一下项目目录”的指令它可能决定去读文件、可能决定去跑ls、可能决定写一个脚本再执行、甚至可能决定调用某个你根本没预料到的工具。行为不确定意味着你没法在编码阶段就把权限卡死——你只能给它一个“能力集合”然后祈祷它在这个集合里规规矩矩。问题就出在这个“能力集合”上。大部分 Agent 框架的默认做法是Agent 进程继承宿主进程的权限。也就是说你用哪个用户启动 AgentAgent 就有那个用户的全部权限。如果你在开发机上用管理员账号跑那 Agent 就是管理员如果你在服务器上用 root 跑容器那 Agent 就是 root。热搜里那个“你需要来自 administrators 的权限才能删除”的报错反过来想就是——当 Agent 真的拿到了 administrators 权限它能删的东西远超你的想象。2.2 三类典型越权场景每一个都真实发生过我把实际见过和听过的问题归成三类基本覆盖了 Agent 权限失控的主要形态。第一类是文件系统越权。Agent 为了完成任务往往会获得读写文件的能力。如果没有目录级隔离它可能读到~/.ssh下的密钥、读到.env里的数据库密码、读到其他项目的源码。更糟的是写权限——误删、误覆盖、把敏感文件写到公开目录都是常见事故。我见过一个案例Agent 在“导出数据”时把结果写到了 Web 根目录结果那份含用户手机号的 CSV 被直接公网访问了。第二类是进程与系统调用越权。Agent 如果能执行 shell 命令那它能做的事就几乎等于一个登录用户能做的事。它可以curl外网、可以nc开端口、可以chmod改权限、可以kill别的进程。热搜里的seccomp就是专门用来限制“一个进程能调用哪些系统调用”的机制比如禁止ptrace、禁止mount、禁止创建原始套接字。没有这层过滤Agent 跑的一段“看起来无害”的脚本可能就在偷偷做别的事。第三类是网络与凭据越权。Agent 常常需要调用外部 API于是你给它配了 token、配了密钥。如果这些凭据是长期有效的、权限很大的那 Agent 一旦被诱导比如通过一段恶意输入去调用敏感接口后果就很严重。热搜里的minio mc 给 buckets 设置 public 权限、行级权限这些词本质上都是在讨论“怎么把权限收窄到刚好够用”。2.3 沙箱不是“加个 Docker”就完事了很多人一听沙箱第一反应是“那我用 Docker 跑不就行了”。Docker 确实是沙箱的一层但它远不是全部而且默认配置下的 Docker 隔离强度经常被高估。Docker 默认用的是 Linux 的 namespace 做隔离——PID namespace 让容器看不到宿主进程mount namespace 让容器有独立的文件系统视图network namespace 让容器有独立的网络栈。这些确实有用。但默认情况下容器里的 root 用户和宿主机的 root 用户在很多场景下是“近似等价”的尤其是当容器以--privileged启动、或者挂载了宿主目录、或者没开 user namespace 的时候。热搜里docker 容器怎么赋予目录读写权限、docker 权限错误怎么解决这些问题背后往往就是隔离和便利之间的拉扯。真正的沙箱是多层叠加的namespace 做资源视图隔离cgroup 做资源用量限制seccomp 做系统调用过滤capabilities 做特权能力裁剪再加上文件系统只读挂载、网络出口白名单、临时目录配额等等。少一层风险就多一分。下面我会一层一层拆开讲。3. 沙箱的核心零件namespace、seccomp、capabilities 到底各管什么3.1 namespace让 Agent 活在一个“看起来完整、其实被裁剪”的世界里namespace 是 Linux 内核提供的一种隔离机制它让一组进程拥有独立的全局资源视图。你可以把它理解成“给进程戴上一副特制眼镜”透过这副眼镜它看到的是一个被重新编排过的世界。对 Agent 沙箱来说最关键的几个 namespace 是这些namespace 类型隔离的内容对 Agent 沙箱的意义Mount文件系统挂载点让 Agent 只看到指定的目录树看不到宿主其他路径PID进程 ID 空间Agent 看不到也影响不了宿主进程Network网络设备、端口、路由可以完全断网或只放行特定出口User用户和组 ID 映射容器内 root 映射成宿主普通用户降权关键IPC进程间通信资源防止通过共享内存等方式越界通信UTS主机名和域名隔离主机标识避免信息泄露其中User namespace是最容易被忽略但最重要的一层。没有它容器里的 root 就是真 root有了它容器里的 root 会被映射成宿主上的一个普通用户即使 Agent 在容器里“提权成功”到了宿主上也只是个普通账号。这就是所谓的“rootless 容器”思路。实操上用unshare可以快速体验 namespace 隔离# 在一个新的 user mount pid namespace 里启动一个 shell unshare --user --map-root-user --mount --pid --fork --mount-proc /bin/bash # 进去之后你会发现 whoami 是 root但这是映射出来的假 root # 尝试访问宿主的一些路径会失败因为 mount namespace 是独立的注意--map-root-user只是把当前用户映射成 namespace 内的 root并不是真的提权。这个区别非常关键很多人第一次用会误以为自己“变成 root 了”。3.2 seccomp把 Agent 能调用的系统调用砍到只剩需要的namespace 管的是“能看到什么”seccomp 管的是“能做什么”。它工作在系统调用层面可以精确到“允许read、write、openat但禁止socket、ptrace、mount”。为什么这个重要因为 Agent 执行代码时代码最终都会落到系统调用上。一段 Python 脚本看起来只是“读个文件”底层可能是openatreadclose但如果这段脚本被诱导去连外网就会多出socketconnect。如果你用 seccomp 把socket禁掉那无论上层代码怎么写网络这条路就是堵死的。Docker 默认已经带了一套 seccomp profile会禁用大约 40 多个危险系统调用。但默认 profile 是“通用”的对 Agent 场景往往还不够严。你可以自定义 profile比如{ defaultAction: SCMP_ACT_ERRNO, architectures: [SCMP_ARCH_X86_64], syscalls: [ { names: [read, write, openat, close, fstat, mmap, exit_group], action: SCMP_ACT_ALLOW } ] }这段配置的意思是默认拒绝一切系统调用只放行白名单里的这几个。这是最严格的白名单模式适合那种“只让 Agent 做纯计算和有限文件读写”的场景。实际用的时候白名单会列得长一些但核心思路就是“默认拒绝、按需放行”。提示seccomp 白名单调起来很痛苦因为漏掉一个系统调用程序就直接崩。建议先用SCMP_ACT_LOG模式跑一段时间把实际用到的系统调用记录下来再据此生成白名单。3.3 capabilities把 root 的“超能力”一项项收走传统 Linux 里 root 是个“全有或全无”的概念。capabilities 机制把 root 的特权拆成了几十项独立能力比如CAP_NET_ADMIN管理网络、CAP_SYS_ADMIN系统管理、CAP_DAC_OVERRIDE绕过文件权限检查。对 Agent 沙箱来说原则很简单能不给就不给必须给的给最小集。一个只做文件处理的 Agent理论上不需要任何 capability一个需要绑定低端口的服务可能只需要CAP_NET_BIND_SERVICE。Docker 里可以用--cap-dropALL先全部丢掉再--cap-add按需加回docker run --cap-dropALL --cap-addCAP_NET_BIND_SERVICE my-agent-image这一条命令的效果比很多人想象的要大。默认 Docker 容器是带着一批 capability 启动的全丢掉之后容器内即使有 root 身份能造成的破坏也小得多。3.4 三层机制怎么配合一个完整的隔离思路把这三层串起来看逻辑是这样的namespace 决定 Agent 能看到哪些资源capabilities 决定 Agent 有哪些特权seccomp 决定 Agent 能发起哪些系统调用。三者叠加才构成一个相对完整的沙箱。我一般会按这个顺序来配先定 namespace文件系统视图、网络视图、用户映射再砍 capabilities默认全丢、按需加回最后上 seccomp默认拒绝、白名单放行。顺序不能反因为 seccomp 白名单要根据前两层确定的能力范围来定——你都不知道 Agent 要访问哪些文件就没法确定它需要哪些系统调用。4. 从零搭一个带沙箱的 Agent 执行环境4.1 整体架构Agent 主进程和沙箱执行器要分开一个常见的错误设计是Agent 主进程直接在自己进程里执行模型生成的代码。这样做的问题是代码和 Agent 共享同一个权限上下文沙箱根本无从谈起。正确的做法是把“决策”和“执行”拆开。Agent 主进程负责和模型交互、规划任务、生成代码或命令沙箱执行器是一个独立的、权限被严格限制的进程或容器负责真正跑这些代码。两者之间通过一个明确定义的接口通信比如传一段代码进去、拿一个结果出来。[Agent 主进程] --(代码/命令)-- [沙箱执行器] --(stdout/stderr/结果)-- [Agent 主进程] | | 完整权限 受限权限 隔离这个拆分带来的好处是即使沙箱里的代码被恶意诱导它能影响的范围也被死死限制在沙箱内。Agent 主进程的凭据、宿主的关键文件都在沙箱的视野之外。4.2 用 Docker 搭一个最小可用沙箱下面是一个可以直接抄的 Docker 沙箱配置思路。假设我们要跑的是 Python 代码只允许读写/workspace目录不允许联网资源限制在 1 核 512M。docker run \ --rm \ --networknone \ --cap-dropALL \ --security-optno-new-privileges \ --read-only \ --tmpfs /tmp:rw,noexec,nosuid,size64m \ --mount typebind,src/host/agent-workspace,dst/workspace,rw \ --memory512m \ --cpus1 \ --pids-limit64 \ --user1000:1000 \ python:3.11-slim \ python /workspace/task.py逐条解释一下这些参数为什么这么设--networknone完全断网。Agent 跑代码时如果需要联网应该由主进程代理而不是让沙箱直接连。--cap-dropALL丢掉所有 capability沙箱内没有任何特权。--security-optno-new-privileges禁止通过 setuid 等方式提权堵死一条常见逃逸路径。--read-only根文件系统只读防止 Agent 往系统目录写东西。--tmpfs /tmp:...给一个临时可写目录但设了noexec不能执行文件和nosuid并且限制大小。--mount ... rw只把工作目录挂进去其他宿主路径一律不可见。--memory/--cpus/--pids-limit资源限制防止 Agent 跑出死循环或 fork 炸弹把宿主拖垮。--user1000:1000以非 root 用户运行进一步降权。注意--read-only加上--tmpfs的组合很关键。很多人只设了只读结果程序因为没法写临时文件直接报错然后就把只读去掉了。正确做法是保留只读单独给一个受限的临时目录。4.3 更轻量的方案进程级沙箱不是所有场景都值得上 Docker。如果 Agent 只是跑一些简单的表达式求值或者受限的 Python 代码用进程级沙箱更轻。Python 生态里可以用RestrictedPython做语言级限制或者用subprocess配合resource模块做资源限制。import subprocess import resource def run_in_sandbox(code: str, timeout: int 5): def preexec(): # 限制 CPU 时间 5 秒 resource.setrlimit(resource.RLIMIT_CPU, (5, 5)) # 限制内存 256MB resource.setrlimit(resource.RLIMIT_AS, (256 * 1024 * 1024,) * 2) # 限制进程数 resource.setrlimit(resource.RLIMIT_NPROC, (16, 16)) # 限制文件大小 10MB resource.setrlimit(resource.RLIMIT_FSIZE, (10 * 1024 * 1024,) * 2) result subprocess.run( [python, -c, code], capture_outputTrue, timeouttimeout, preexec_fnpreexec, cwd/tmp/agent-sandbox, ) return result.stdout.decode(), result.stderr.decode()这段代码用resource.setrlimit在子进程启动前设好各种上限。preexec_fn会在fork之后、exec之前执行是设置资源限制的标准位置。配合一个专门的沙箱工作目录能挡住大部分“跑飞了”的情况。但要清楚进程级沙箱的隔离强度远不如容器。它挡得住资源滥用挡不住文件系统越权子进程还是能读宿主文件也挡不住网络访问。所以它适合“代码来源相对可信、只是怕它跑飞”的场景不适合“代码可能被恶意诱导”的场景。4.4 网络出口怎么管白名单比断网更实用完全断网最安全但很多 Agent 确实需要联网——查资料、调 API、下载依赖。这时候就需要网络出口白名单。Docker 原生不支持按域名做出口白名单通常的做法是让沙箱走一个代理代理上配置白名单。或者用iptables在宿主层面限制容器的出站流量。更现代的做法是用支持网络策略的容器运行时直接声明“只允许访问 api.example.com”。不管用哪种方式核心原则是一样的默认拒绝所有出站只放行明确需要的目标。热搜里那些关于“权限设置”“访问被拒绝”的讨论很多都指向同一个结论——白名单模式虽然配置麻烦但它是唯一能真正控制住出口的方式。5. 实操中踩过的坑与排查技巧5.1 沙箱配太严导致 Agent“变傻”这是最常见的矛盾。你把 seccomp 白名单收得很紧结果 Agent 跑稍微复杂点的代码就崩报一堆Operation not permitted。然后你开始一项项加白名单加着加着发现白名单已经长得跟默认配置差不多了隔离形同虚设。我的经验是分层配置。把 Agent 的任务按风险分级纯计算类任务用最严的沙箱文件处理类任务放宽文件相关系统调用需要联网的任务单独走一个带代理的沙箱。不要指望一套配置打天下。另外用SCMP_ACT_LOG先观察再收紧比一上来就白名单要省心得多。5.2 挂载目录的权限陷阱--mount挂载宿主目录时容器内的用户 ID 和宿主目录的属主 ID 对不上就会出现“明明挂载了却写不进去”的问题。热搜里docker 容器怎么赋予目录读写权限问的就是这个。解决办法有两个一是让容器内用户 ID 和宿主目录属主 ID 一致用--user指定二是把宿主目录的权限放宽到容器内用户可写。前者更安全后者更省事。我一般选前者因为放宽宿主目录权限等于把沙箱的边界往外推了。5.3 临时目录没设noexec被当成跳板有一次安全审计发现我们的沙箱虽然根文件系统只读但/tmp是可写的而且没设noexec。结果 Agent 生成的代码可以把一个可执行文件写到/tmp再执行绕过了“根目录只读”的限制。加上noexec之后就堵住了。这个坑很隐蔽因为大部分时候 Agent 不会这么干但一旦被诱导就会出问题。凡是可写目录都要考虑加noexec和nosuid。5.4 常见问题速查表现象可能原因排查方向代码报Operation not permittedseccomp 拦截了系统调用用SCMP_ACT_LOG看被拦的是哪个调用容器内写文件失败挂载目录属主不匹配或根目录只读检查--user和挂载参数Agent 跑一会儿就 OOM内存限制太紧或代码有内存泄漏调--memory加超时联网请求全部失败--networknone或出口白名单没配确认网络策略和代理配置进程数暴涨代码里有 fork 循环设--pids-limit沙箱内看不到工作目录mount namespace 没挂对检查--mount的 src/dst5.5 一个容易被忽略的点日志和审计沙箱不只是“挡住”还要“记录”。Agent 在沙箱里做了什么、尝试了什么被拦了这些日志对排查问题和发现异常都极其重要。我一般会把沙箱的 stdout/stderr、seccomp 的拦截日志、资源使用峰值都收集起来。特别是 seccomp 的拦截日志——如果某个 Agent 频繁尝试被禁用的系统调用那大概率是它在做不该做的事值得警惕。6. 权限收窄的工程实践从“能用”到“敢用”6.1 最小权限不是一次配好的是迭代出来的很多人以为最小权限就是“一开始就配到最紧”。实际做下来最小权限是一个迭代收敛的过程先给一个相对宽松但可用的配置观察 Agent 实际用了哪些能力然后逐步收紧直到刚好够用。这个过程中日志是核心依据。你需要知道 Agent 到底访问了哪些文件、调用了哪些系统调用、连了哪些地址。没有这些数据收紧就是瞎猜。我一般会先跑一周的“观察模式”把能力使用情况摸清楚再动手收。6.2 凭据管理别把长期密钥塞进沙箱Agent 需要调外部 API 时凭据怎么给是个大问题。最差的做法是把长期有效的密钥直接写进沙箱环境变量。一旦沙箱被突破密钥就泄露了。更好的做法是短期凭据 代理。Agent 不直接持有密钥而是通过一个代理服务去调外部 API代理服务持有密钥并做权限校验。这样即使沙箱被突破攻击者也拿不到密钥只能通过代理做有限的操作。热搜里行级权限的思路也是类似的——把权限控制下沉到数据行级别而不是靠一把万能钥匙。6.3 沙箱逃逸的现实风险有多大客观说容器逃逸不是随便就能做到的需要内核漏洞或者配置严重错误。但 Agent 场景的特殊性在于Agent 会执行模型生成的、可能被恶意输入影响的代码。这就把“逃逸”从一个需要主动攻击者的问题变成了一个“可能被无意触发”的问题。所以我的态度是不追求绝对安全那不存在但要把逃逸的成本和难度抬到足够高。多层隔离叠加、及时打内核补丁、不用--privileged、不挂载敏感目录做到这些绝大多数风险就挡住了。6.4 给不同规模团队的落地建议小团队、内部工具至少做到 Docker 隔离 非 root 用户 资源限制 工作目录隔离。这四样配齐能挡住 90% 的常见事故。中等团队、面向部分外部用户在上面基础上加 seccomp 白名单、网络出口白名单、凭据代理、完整审计日志。大团队、生产级多租户考虑用专门的沙箱运行时比如基于 gVisor 或 Firecracker 的方案做内核级隔离配合细粒度的权限系统和实时监控。不管哪个规模有一条是共通的永远不要用管理员或 root 身份直接跑 Agent。热搜里那个“你需要来自 administrators 的权限才能删除”的提示反过来就是最好的提醒——如果 Agent 真的有了 administrators 权限它删东西的时候可不会问你。7. 我个人的几点体会搭 Agent 沙箱这件事技术本身不算特别难难的是心态。大部分开发者包括我自己早期在搭 Agent 时潜意识里是“我要让它能干活”于是不断给它加权限、开通道。沙箱的思路恰恰相反是“我要让它干不了坏事”于是不断收权限、堵通道。这两种思路的拉扯贯穿整个开发过程。我现在的习惯是每给 Agent 加一个新能力都先问一句“如果这个能力被滥用最坏会怎样”。如果答案让我不舒服那就先加隔离再加能力。这个习惯帮我避开了好几次潜在事故。另外沙箱配置一定要写进代码、进版本控制、能一键复现。我见过太多团队沙箱是“手工配的”换台机器就忘了某个参数隔离直接失效。把沙箱配置当成代码来管理是让它真正可靠的前提。最后分享一个小技巧给沙箱加一个“干跑模式”让 Agent 在真正执行前先把要做的操作列出来人工或规则审核一遍再放行。这个模式对高风险操作特别有用虽然会牺牲一点自动化程度但换来的是对关键动作的可见性。等规则积累够了再逐步放开自动执行。这个渐进式的思路比一上来就全自动要稳得多。
返回列表