ARTICLE DETAIL

资讯详情

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

AI Agent安全边界:沙箱、凭据代理与L7策略实战解析

AI Agent安全边界:沙箱、凭据代理与L7策略实战解析 最近一直在折腾AI编码Agent在GPU机上的落地用Claude Code这类工具跑代码生成和自动修复时心里始终悬着一件事模型拿到shell之后权限边界到底在哪里。放任Agent直接在宿主机上跑等于把一把能读SSH私钥、能curl内网元数据接口的万能钥匙交给了一个会自己编命令的进程。后来接触了NVIDIA生态里围绕Agent沙箱的一套做法社区里一般叫OpenShell核心就是三件事把进程装进沙箱、用凭据代理替Agent管理钥匙、在L7层卡住它的网络访问。这篇文章就把我对这套方案的理解、拆解和实际落地经验完整梳理一遍。写这篇解析的初衷很简单不是去复述某个项目的README而是把你真正会遇到的问题讲清楚沙箱和GPU穿透怎么共存、凭据代理怎么做才不会变成新的单点故障、L7策略怎么设才能既管得住又不误伤正常开发流程。如果你正好在搭建类似的环境或者只是好奇AI Agent的安全边界该怎么做这篇应该能给你一个相对完整的参考框架。1. 为什么AI Agent需要一套独立的Shell边界1.1 自然语言到系统命令的权限放大问题传统的命令行安全模型很简单一个用户、一个进程、一组权限。管理员给开发人员一个普通用户账号开发人员在终端里敲什么命令系统就执行什么命令后果由这个人自己承担。但AI编码 Agent把这一层逻辑彻底打穿了——你给模型一个目标它自己生成bash命令、自己执行、自己根据输出来决定下一步。这意味着人的意图和命令的实际权限之间出现了一段无人看守的路段。举一个很具体的例子。我在项目里跑一个自动重构任务Agent的第一步往往就是读取项目里的配置文件来找依赖关系。如果它运行在宿主机上它就有能力读~/.ssh/id_rsa、/etc/passwd、甚至云厂商的元数据接口169.254.169.254。它不会故意做坏事但自然语言模型在上下文里看到敏感文件内容之后下一步的决策就可能受影响万一被提示词注入后果比手动执行危险得多。常规的做法是多用户隔离或者容器隔离。但容器解决的是这个进程不能动别的进程的问题解决不了这个进程可以访问网络上的什么资源的问题。一个Docker容器默认拥有完整的出站网络能力它可以访问内网所有主机、所有端口。对传统应用来说这还能接受因为应用的行为是可预测的对Agent来说行为是模型实时生成的攻击面变成了模型工具网络整个链条。1.2 OpenShell的三个核心组件如何互补OpenShell的思路是把Agent的运行时环境拆成三个正交的控制面沙箱约束进程本身的能力。限制文件系统可见范围、系统调用、资源配额让Agent睁眼只能看到它该看到的工作区。凭据代理让Agent不再持有真实的长期密钥。需要访问Git仓库、调用云API的时候通过代理去换取短期凭证用完即失效全程可审计。L7策略执行在网络层之上做访问控制。不是只允许访问某台主机的443端口而是精确到允许GET /api/repos禁止POST /api/deployments。三者缺一不可。只有沙箱没有凭据代理Agent还是会从某个配置文件里读到密码只有凭据代理没有L7策略Agent拿到令牌后依然可以在内网横冲直撞只有L7策略没有沙箱Agent还是能把别人的文件删掉。这套设计最打动我的一点是它没有试图限制模型的能力而是限制了越权的路径——模型该有多强就有多强但所有强能力都被收口在可控管道里。2. 沙箱层Agent进程的活动边界该怎么划2.1 不同沙箱方案的取舍沙箱选型是个反复权衡的过程。我最初直接用Docker跑Agent遇到的问题是镜像太重、每次启动都要等而且Docker daemon本身成为Agent和宿主机之间的一个巨大攻击面——Agent如果能逃逸容器下一个目标就是docker socket。后来对比了几个方案整理如下方案隔离强度启动速度GPU穿透适用场景Docker容器中中配合nvidia-container-toolkit团队统一环境可接受较重启动gVisor较高中早期较困难强隔离需求不依赖GPUbubblewrap中上极快直接绑定设备单机轻量隔离最常用虚拟机/微虚机极高慢复杂多租户高安全场景考虑到OpenShell主要跑在单台NVIDIA工作站的场景我最后选的是bubblewrap配合nvidia-container-runtime来做GPU穿透。bwrap的好处是不需要daemon用uid_map和pid_namespace直接创建隔离环境启动一个Agent实例只要几百毫秒。但它的坏处也很明显配置完全靠命令行参数堆出来可读性差必须封装成配置文件来管理。2.2 文件系统与系统调用的收紧细节在实际配置里文件系统的处理是第一优先级。我的做法是给每个Agent会话创建一个临时工作目录目录里软链上项目代码的只读副本然后整个根文件系统以只读方式挂载。这样Agent能写的地方只有它自己的工作目录想动/home、/usr、/etc都是痴心妄想。真正需要抠的是系统调用层面。bwrap本身不管seccomp需要额外套一个seccomp profile。我参考Docker的默认profile做了一些裁剪保留了进程创建、文件读写、网络socket等常规调用屏蔽了mount、ptrace、keyctl这类高风险调用。这里有个很实际的经验不要直接禁用clone3新版glibc和某些runtime会用到它禁用后沙箱里跑Python都会被卡死我第一次配的时候就被这个坑绊了一跤。CPU和内存配额同样不能省。Agent的token生成需要CPU但推理一般在宿主机上做所以我在cgroup里限制Agent进程最多用4核、8G内存防止某个失控的自动化任务把整台机器拖垮。磁盘配额用XFS project quota做给每个工作目录设一个5G上限实测很稳定。2.3 GPU穿透与沙箱隔离的矛盾点NVIDIA环境下的特殊问题在于GPU。Agent本身可能要做模型推理的调用但如果Agent是跑在GPU驱动的宿主机上经常需要访问/dev/nvidia*设备。bwrap默认把这些设备都藏起来Agent里一旦执行nvidia-smi或者调用CUDA就会报No such device。解决办法是显式把设备节点绑进沙箱同时把NVIDIA的用户态库和驱动目录也挂载进去。大致是这样bwrap \ --dev-bind /dev/nvidia0 /dev/nvidia0 \ --dev-bind /dev/nvidiactl /dev/nvidiactl \ --dev-bind /dev/nvidia-uvm /dev/nvidia-uvm \ --ro-bind /usr/lib/x86_64-linux-gnu/nvidia /usr/lib/x86_64-linux-gnu/nvidia \ --ro-bind /usr/lib/x86_64-linux-gnu/libcuda.so /usr/lib/x86_64-linux-gnu/libcuda.so \ --ro-bind /etc/nvidia /etc/nvidia \ --ro-bind /tmp/task_workspace /workspace \ --proc /proc \ -- bash注意这里有个细节--dev-bind是允许沙箱内进程直接访问GPU设备节点这等同于把设备的控制权完全交给了沙箱内的进程。为了降低风险我会再叠加一层cgroup的device controller限制沙箱内进程只能访问c 195:*NVIDIA字符设备而不允许访问其他块设备。这是NVIDIA Container Toolkit一贯采用的做法只不过bwrap需要手工维护。如果你不想维护这么多细节直接用Docker加--gpus all会省事很多代价是启动速度和镜像体积。3. 凭据代理让Agent借钥匙而不是拿钥匙3.1 环境变量里放密钥为什么不行很多团队的自动化任务习惯把GitHub Token、云厂商AK/SK放到环境变量里Agent镜像里预置一把。这个做法在传统CI里勉强能接受在Agent场景里就是灾难。环境变量对沙箱内所有进程可见Agent执行任何命令时shell都会把环境变量继承给子进程。一旦子进程里某个依赖库把环境变量打出来或者模型输出把env的内容带进上下文密钥就等于进了模型的思考上下文。更麻烦的是审计角度。密钥如果一直被Agent持有你根本分不清一个API调用是哪个任务产生的。凭据代理要解决的就是这个问题Agent手里只有一张开票凭证每次真正要用key的时候去代理那里换一张短期有效的票代理记录谁换的、换了多久、用来做了什么。3.2 签发-注入-回收的完整模型我搭建的凭据代理核心逻辑很简单作为一个常驻服务跑在宿主机上通过unix socket与沙箱内Agent通信。整个流程分四步签发Agent启动时代理给这个会话签发一个短期访问令牌有效期默认15分钟到期前Agent需要刷新。注入令牌通过unix socket写入沙箱内的/run/agent_token文件文件权限设为0600只允许Agent进程所有者读取。换票Agent调用Git或云API时通过这个令牌向代理请求具体服务的临时凭证。临时凭证有效期为5分钟而且Scope被限定在一次操作需要的最小权限范围内。回收与审计任务结束或会话关闭代理主动吊销所有已颁发的临时凭证并把整个签发链条写入审计日志。这个机制像是酒店前台给房卡而不是把万能钥匙直接交给客人。Agent每次进房间都要重新刷卡房卡过期了前台能立刻收回去而且每一张房卡开了哪扇门都有记录。3.3 与K8s和本地环境的对接差异在单机环境里unix socket方案干净利落。但在K8s集群里沙箱跑在Pod内凭据代理跑在集群里通信链路就得换成gRPC而且要用ServiceAccount做双方身份认证。这一层我建议直接用SPIFFE/SPIRE来管理身份给每个Agent Pod一个独立的SPIFFE ID代理只给持有合法SPIFFE ID的调用方签发凭证。本地开发环境的思路则相反反向代理模式更顺手。我让Agent进程直接通过http://localhost:9211访问凭据代理代理在收到请求后附加真实的认证头再转发给目标服务。这样Agent代码里完全不需要感知密钥的存在网络请求看起来就像是在访问一个本地的HTTP服务心理负担小很多。这里有一个很多人忽略的细节凭据代理本身也必须有网络隔离保护。如果代理监听在0.0.0.0上局域网内任何一个进程都能来换票那沙箱就等于白做了。我一般让代理只监听127.0.0.1或者通过TLS双向认证绑定SPIFFE ID绝对不能裸奔在公网或内网大网段上。4. L7策略执行在第七层管住Agent的手4.1 为什么传统的L3/L4网络策略不够用如果只是禁止Agent访问外网那直接防火墙就够了问题在于Agent必须访问外网或内网服务来完成任务而服务往往以域名和API路径的形式存在不是固定的IP和端口。举例来说Agent要拉取Git仓库它需要访问github.com的443端口它要调用大模型API需要访问某个推理服务的443端口。如果只按IP和端口开白名单那就只能开放一堆443端口和全放没有区别。因为443端口上既能跑合法的Git请求也能跑非法的数据回传。真正能区分这两者的只有HTTP层的方法、路径、请求头。L7策略的价值就在这允许GET https://git.internal.example.com/repos/myapp但不允许DELETE https://git.internal.example.com/repos/myapp允许POST https://llm.internal.example.com/v1/chat但不允许POST https://llm.internal.example.com/v1/admin/users。策略的颗粒度从能不能访问这台主机变成了能不能执行这个操作。4.2 基于sidecar代理的规则设计我在OpenShell的架构里把L7策略下沉到一个sidecar进程部署在Agent沙箱和外部网络之间。沙箱内Agent的HTTP流量被强制转发到这个sidecar由sidecar做一层透明的L7中间人检查然后由sidecar代为访问目标服务。规则引擎我用的OPAOpen Policy Agent来写策略这是因为它能动态加载规则Agent每次会话都能根据任务类型加载不同的策略集。一个简化版的策略定义大概是这样的package agent.policy import rego.v1 default allow : false # 允许访问代码仓库的读取接口 allow if { input.method GET startswith(input.url, https://git.internal.example.com/repos/) } # 允许调用推理服务的对话接口 allow if { input.method POST startswith(input.url, https://llm.internal.example.com/v1/chat) not has_admin_header(input) } has_admin_header(input) if input.headers[X-Admin] true这段策略的逻辑是默认拒绝一切请求只放行代码仓库的GET请求和推理服务的聊天接口同时显式拦截带X-Admin: true的管理请求。之所以加最后一条是因为很多内部服务用一个自定义头来标识管理员操作Agent在上下文中被注入管理员权限后可能会尝试借这个接口绕过限制必须在L7层把这个口子堵死。为了避免Agent用不同的HTTP方法绕过我在sidecar上做统一的大小写归一化不管请求写的是get还是GET策略检查都以小写为准。同时强制要求所有请求必须带Content-TypeJSON响应不允许被压缩成gzip后再转发否则中间人解析会失效。4.3 动态策略让任务意图和网络放行联动L7策略最有意思的点是动态化。同样是Agent访问内部API跑读取文档任务时只需要GET权限跑发布版本任务时才需要POST权限。如果一份静态策略覆盖所有场景要么过于宽松全放要么过于严格什么都做不了。我的做法是在会话创建阶段根据任务的类型向策略服务注册一个临时策略集。具体来说OpenShell在接收到一个新任务时先把任务的意图解析成一组允许的操作生成一条带有效期的策略记录然后下发到sidecar。策略记录里包含任务ID、会话ID、允许的URL前缀、允许的方法、有效期。任务一结束这条记录立即失效。这套机制在工程上有个很关键的考量策略服务必须知道任务什么时候结束。Agent经常会提前退出或者崩溃如果策略只按时间失效那一次崩溃后的5分钟里Agent仍然有机会用旧的权限做操作。我增加了心跳机制Agent沙箱每隔30秒向sidecar上报存活状态连续三次心跳超时sidecar就主动吊销该会话的全部策略。这在实践里非常管用那些任务失败后Agent残留进程还在后台跑的情况都被及时兜住了。5. 从零搭一套OpenShell参考环境5.1 组件选型与整体拓扑整个参考环境我拆成了五个独立进程全部用systemd单元管理sandbox-launcher负责创建bwrap沙箱启动Agent主进程。credential-agent负责签发、回收临时凭证监听unix socket。l7-proxy负责L7策略执行基于OPA做请求审批。task-manager负责任务生命周期管理、策略注册、心跳检测。agent主进程跑在沙箱内通过localhost代理与外部交互。沙箱内的出站流量走向是这样Agent发出的HTTP请求先打到本机的127.0.0.1:9212l7-proxyl7-proxy检查策略后转发到目标服务如果目标服务需要认证l7-proxy会先调用credential-agent换票再把票附加到请求头。整个过程对Agent透明Agent甚至不知道自己用的是短期凭证。5.2 沙箱配置示例我封装了一个启动脚本start_sandbox.sh核心参数如下#!/bin/bash WORKDIR$(mktemp -d /tmp/agent_work_XXXXXX) cp -r --reflink /data/projects/myapp $WORKDIR/src chown -R 1000:1000 $WORKDIR bwrap \ --unshare-all \ --share-net \ --new-session \ --die-with-parent \ --hostname agent-$(date %s) \ --ro-bind /usr/bin /usr/bin \ --ro-bind /usr/lib /usr/lib \ --ro-bind /usr/lib64 /usr/lib64 \ --ro-bind /lib /lib \ --ro-bind /lib64 /lib64 \ --ro-bind /etc/ssl /etc/ssl \ --ro-bind /etc/resolv.conf /etc/resolv.conf \ --dev-bind /dev/null /dev/null \ --dev-bind /dev/urandom /dev/urandom \ --dev-bind /dev/nvidia0 /dev/nvidia0 \ --dev-bind /dev/nvidiactl /dev/nvidiactl \ --dev-bind /dev/nvidia-uvm /dev/nvidia-uvm \ --bind $WORKDIR /workspace \ --chdir /workspace \ --setenv PATH /usr/bin:/bin \ --setenv HOME /workspace \ --uid 1000 --gid 1000 \ -- bash /usr/local/bin/agent-entrypoint.sh几个容易被忽略的点--share-net这里不是放开了网络隔离而是因为l7-proxy需要走127.0.0.1如果完全unshare网络沙箱内的Agent反而连不上代理。为了安全我在cgroup层面又叠加了net_cls标签和iptables规则确保来自沙箱的流量只能发往127.0.0.1:9212任何直接出站包都被DROP。还有--die-with-parent这个参数特别重要它保证bwrap这个父进程一死沙箱里的所有进程都会被立即杀掉不会出现孤儿Agent进程。我见过不少忘了加这个参数的案例Agent进程在被kill后继续跑还一直发心跳非常头疼。5.3 凭据代理配置示例credential-agent我用了一个轻量Go服务配置按服务类型分区listen: unix: /run/credential-agent.sock services: github: type: oauth token_endpoint: https://github.com/login/oauth/access_token scopes: [repo] ttl: 5m llm: type: apikey api_key_env: LLM_API_KEY ttl: 10m internal-api: type: mtls cert_file: /etc/credential-agent/client.pem key_file: /etc/credential-agent/client-key.pem ttl: 5m使用方在沙箱内通过socket请求换票curl --unix-socket /run/credential-agent.sock \ -H Authorization: Bearer $AGENT_TOKEN \ -d {service: github} \ http://localhost/issue返回的临时token只对github.com这一个host有效而且代理在配置里写明scopes只能是最小权限。如果Agent请求的scope大于注册时声明的最大scope代理直接拒绝。这是防止最小权限原则被模型上下文里的恶意指令绕过的最直接手段。5.4 L7策略服务配置示例l7-proxy的配置我分成两个部分base.rego里放全局严格策略session.rego由task-manager动态生成# 全局基础策略: 默认拒绝内网其他主机 # 只允许访问经凭据代理登记的域名 opa eval -f raw \ -d policy/base.rego \ -d policy/session.rego \ -i input.json \ data.agent.policy.allowsidecar本身是一个基于envoy的自定义filterHTTP请求进来后先把method、url、headers解析成input JSON然后丢给OPA实例做决策决策通过才真正发起出站连接。所有被拒绝的请求会统一记录成结构化日志包括Agent会话ID、任务ID、请求URL、命中哪条拒绝规则这对事后排查Agent到底想干什么非常关键。这一步踩过最典型的坑是HTTPS流量的明文改写。要让envoy做L7解析必然要终结TLS再重新发起Agent端如果不信任sidecar的证书就会报证书错误。解决办法是在沙箱内预置一套私有CA证书把SSL_CERT_FILE指向它。这个证书是隔离环境下专用的不会影响宿主机其他服务。6. 实际落地中的坑与排查路径6.1 沙箱内CUDA不可用驱动版本与库不一致我一开始在沙箱里跑Agent的GPU检测脚本在宿主机上一切正常进了bwrap之后nvidia-smi直接报找不到libnvidia-ml.so。排查了一整天才定位到原因宿主机上NVIDIA驱动是用runfile方式安装的用户态库散落在/usr/lib/x86_64-linux-gnu/nvidia下但沙箱里只挂载了常用的/usr/lib/x86_64-linux-gnu没把nvidia这个子目录一并绑进去。解决方式是把整个NVIDIA用户态库目录完整挂载同时把驱动配置文件/etc/nvidia也带上。更稳妥的做法是直接用包管理器安装驱动让库落在标准搜索路径里。另外CUDA版本和驱动版本必须匹配如果沙箱内装的是CUDA 12.2宿主机驱动却是550系列通常会遇到CUDA driver version is insufficient这类错误。这里建议先查驱动支持的CUDA版本表再决定沙箱内安装哪个版本的CUDA runtime。6.2 凭据代理的socket回环死锁凭据代理和l7-proxy在单机部署时都是走127.0.0.1这引入了一个隐蔽的回环问题。当Agent调用一个需要访问凭据代理的目标服务时l7-proxy需要先从credential-agent换票换票过程本身也是一次HTTP请求如果代理服务在实现时不小心把换票请求也当成普通出站流量送到l7-proxy就会形成A等B、B等A的死锁整个请求链卡死。这个坑的解决办法是明确划分通信层级。我把credential-agent的socket访问排除在策略执行之外l7-proxy在初始化时建立一条到credential-agent的专用HTTP连接这个连接不走OPA策略判断直接从网络栈层面打上SO_MARK标记配合路由规则直通到代理进程。简单说管理通道和数据通道必须物理分开一个socket用来换票另一个socket用来访问外部服务绝不能混在一起。6.3 L7策略误伤内部API的排除机制L7策略跑稳之后会遇到一个日常开发频率最高的抱怨Agent要调用某个内部服务的非标准接口被默认拒绝规则挡住了。比如内网某个老系统提供了一个GET /api/status/detailed?formatjson接口本身没有操作作用只是返回信息但我的策略只开了标准的/api/status带detailed后缀的路径直接就403了。这类问题不能靠松动全局规则解决正确做法是为特定任务注册一条临时例外规则并把例外规则和行为审计绑定在一起。我先查一下Agent当前任务的描述确认它确实需要这个接口然后在session策略里加一条allow if input.method GET and startswith(input.url, http://legacy.internal/api/status/)同时给该规则设置过期时间。任务结束后规则自动注销不会污染后续会话。这里最关键的经验是例外规则一定记录到审批日志里并且要有owner字段写明是谁批准的。没有这个字段三个月后你根本想不起来为什么会有这么一条奇怪的放行规则审计上也说不清楚。6.4 沙箱内ssh-agent转发陷阱还有一个必须是鲜肉级踩坑涉及Git认证。一开始我把凭据代理的换票机制用起来了但为了省事在命令里直接把SSH私钥也挂进了沙箱想着Agent要推代码时用ssh协议就好。结果发现Git仓库的远程地址如果是gitgithub.com:org/repo.gitAgent会直接尝试走ssh而ssh的认证流程完全绕过了L7代理等于打开了一个不受监管的后门。最终的修正方案是统一强制使用HTTPS协议访问Git把远程地址全部改写成https://github.com/org/repo.git同时沙箱里不挂任何SSH私钥所有认证都走凭据代理换来的临时token。这样做的另一个好处是所有Git操作都经过L7代理能留下完整的请求审计。别看这个细节小很多人Agent环境中密钥泄露的源头就是漏了一条SSH通道。7. 这套方案还能往哪些方向延伸OpenShell这种沙箱凭据代理L7策略的架构放到今天来看已经不是某个具体工具的专利而是AI Agent安全基础设施的基本骨架。用熟了之后我越来越觉得下一步值得做的是把策略执行结果和模型行为关联起来形成意图-操作-结果三段式审计。现在策略日志只能告诉我Agent做了什么请求我还想知道Agent是出于什么意图发起的这个请求这样当策略拦截发生时我能直接给Agent反馈原因而不是单纯地403。另一个方向是凭据代理的联邦化。现在单机部署的credential-agent只管理本机的密钥如果团队有多台GPU工作站每台机器各自管一套密钥库密钥更新就成了噩梦。理想状态下应该是有一个集群级的凭据中心各机器的credential-agent向中心注册并拉取策略本机只维护短期凭证。这个改造思路有点像把本地的密钥管理升级成集中式密钥系统但好处是权限回收能真正做到全局即时生效。最后一点个人体会隔离方案做得再严密也只是把风险边界划清楚了真正决定安全上限的还是Agent本身的工具调用习惯。OpenShell这套架构最大的价值不是绝对安全而是让每一次越权尝试都留下可解释的痕迹让你能在出事之后快速回答Agent当时做了什么、为什么被拦、是谁批的例外。在Agent能力越来越强的阶段这个可解释性比任何银弹都重要。
返回列表