Agent沙箱的DNS盲区:从OpenAI事件看工具调用安全加固 1. 一次工具调用被暂停为什么整个 Agent 圈都在讨论 DNSOpenAI 暂停了某个最强模型的工具调用能力官方给出的理由指向一个 DNS 缺口。这件事在 Agent 开发者圈子里炸开锅不是因为模型能力被砍而是因为暴露了一个很多人从来没认真想过的盲区AI 沙箱的隔离边界远比我们以为的要脆弱。先把这件事的核心逻辑讲清楚。所谓工具调用Tool Calling / Function Calling是 Agent 与外部世界交互的通道。模型本身只会生成文本它要查天气、读文件、发请求、跑代码全靠工具调用把意图翻译成真实动作。而沙箱就是执行这些动作的隔离环境——理论上沙箱里的代码只能访问被允许的资源碰不到宿主系统也碰不到内网。问题就出在 DNS 上。DNS 是互联网的电话簿把域名翻译成 IP 地址。绝大多数沙箱方案会把网络出口锁死只允许白名单域名通过。但 DNS 查询本身往往是一个被忽略的旁路通道即使 HTTP 请求被拦截DNS 解析请求可能仍然能发出去。攻击者可以构造一个特殊域名把数据编码进域名里通过 DNS 查询把信息带出去——这就是经典的DNS 隧道DNS Tunneling和DNS 外带DNS Exfiltration。这次事件的价值不在于OpenAI 出事了而在于它给所有做 Agent 的人提了个醒你精心设计的沙箱可能在一个你根本没注意的协议层上漏了底。这篇文章我会从 DNS 解析原理讲起拆解沙箱隔离的常见误区给出可落地的排查方法和加固方案最后聊聊 Agent 安全这件事到底该怎么系统性地想。适合正在做 Agent 开发、平台工程、或者对 AI 基础设施安全感兴趣的读者。2. DNS 解析原理那个被所有人当成基础设施的协议2.1 一次域名解析到底发生了什么很多人对 DNS 的理解停留在输入网址浏览器帮你找到服务器。但真正做安全加固时你必须知道每一次解析背后经过了几跳。当沙箱里的一段代码执行curl https://example.com时实际发生的是应用层调用系统解析器glibc 的getaddrinfo或 musl 的实现解析器检查本地 hosts 文件和 DNS 缓存缓存未命中向配置的 DNS 服务器通常是/etc/resolv.conf里的地址发起查询DNS 服务器递归查询根域名服务器、顶级域服务器、权威服务器拿到 A 记录或 AAAA 记录返回 IP应用层用这个 IP 建立 TCP 连接关键点在于第 3 步到第 5 步走的是 UDP 53 端口或 TCP 53、DoH 的 443这条路径和后面的 HTTP/HTTPS 请求是完全独立的。你的沙箱如果只做了 HTTP 层的域名白名单DNS 层可能是敞开的。2.2 为什么 DNS 是天然的数据外带通道DNS 查询的报文结构决定了它天生适合夹带私货。一个查询报文里查询名QNAME字段可以放任意符合域名格式的字符串。攻击者只要控制一个权威 DNS 服务器就能这样操作把要外带的数据比如 API Key、环境变量、文件片段做 Base32 或十六进制编码拼成一个子域名比如aGVsbG8td29ybGQ.exfil.attacker.com让沙箱里的代码发起对这个域名的解析攻击者的权威服务器收到查询解码子域名数据就到手了整个过程不需要建立任何 TCP 连接不需要 HTTP 请求甚至不需要联网这个动作在应用层可见。从沙箱的视角看它只是解析了一个域名而域名解析在很多安全策略里是被默认放行的。注意DNS 外带的带宽很低单次查询能带的数据有限但用于窃取密钥、环境变量、少量配置信息绰绰有余。而且它可以配合分片慢慢把大文件搬出去。2.3 沙箱隔离的三种常见做法及其 DNS 盲区我见过和用过的沙箱方案大致分三类每一类在 DNS 上都有不同的暴露面隔离方案网络控制方式DNS 盲区容器 iptables限制出站 IP 和端口若放行 53 端口DNS 隧道畅通用户态网络代理强制走 HTTP 代理代理不接管 DNS解析仍走系统微虚拟机如 Firecracker独立网络命名空间若命名空间内有 DNS 转发仍可外带seccomp 无网络完全禁止 socket相对安全但影响工具可用性最常见的坑是第二种很多团队用 HTTP 代理做出口管控以为万事大吉结果 DNS 解析根本没走代理。代码里一个socket.getaddrinfo()就把数据送出去了。3. 从这次事件反推Agent 沙箱的隔离边界该怎么画3.1 工具调用为什么必须跑在沙箱里先回答一个基础问题为什么 Agent 的工具调用不能直接在宿主上跑因为工具调用的输入本质上是模型生成的、不可信的内容。模型可能被提示注入Prompt Injection攻击生成恶意代码也可能因为幻觉生成破坏性的命令。如果这些代码直接在宿主执行一次rm -rf /或者一次读取~/.ssh/id_rsa就是灾难。沙箱的作用就是给这些不可信代码划一个圈你能做什么、不能做什么由圈决定不由代码决定。这个圈包括文件系统、进程、网络、系统调用四个维度。DNS 缺口属于网络维度的漏洞。3.2 网络隔离的最小权限到底该多小做网络隔离时很多人的思路是先全开出问题再关。这在 Agent 场景下是反的应该先全关按需开。具体到 DNS我的建议是分三层控制第一层默认拒绝所有出站。沙箱启动时网络命名空间里没有任何路由连 DNS 服务器都不可达。第二层按工具需求开白名单。如果某个工具确实需要访问特定 API只放行那个 API 的 IP 和端口DNS 解析在沙箱外完成把 IP 直接注入。第三层DNS 查询强制走受控解析器。如果必须让沙箱内解析域名用一个只允许白名单域名的内部解析器并且记录所有查询日志。这里有个实操细节很多工具依赖域名而不是 IP比如 TLS 证书校验需要 SNI 匹配域名。这时候不能简单地把域名换成 IP而要在沙箱内配置一个 hosts 映射或者用一个能透传 SNI 的代理。我一般用后者因为 hosts 映射在域名多的时候维护成本高。3.3 一个容易被忽略的点DNS 缓存投毒与解析劫持除了外带DNS 还有另一个风险方向投毒。如果沙箱内的 DNS 解析被劫持攻击者可以把一个白名单域名解析到自己的服务器从而绕过 IP 白名单。防御方法有两个一是用 DNSSEC 验证解析结果二是在沙箱内固定解析结果——把白名单域名的 IP 在启动时解析好写进 hosts之后不再动态解析。第二种更简单可靠我一般优先用。4. 实操如何检测和封堵沙箱里的 DNS 外带通道4.1 先检测你的沙箱现在能不能外带数据在动手封堵之前先确认问题是否存在。方法很简单在沙箱里跑一段代码尝试解析一个你能监控的域名import socket # 尝试解析一个带唯一标识的子域名 payload test123456 domain f{payload}.your-monitor-domain.com try: socket.getaddrinfo(domain, None) print(DNS 查询已发出检查你的权威服务器日志) except Exception as e: print(f解析失败{e})如果你在自己的权威 DNS 服务器上看到了test123456这条查询记录说明沙箱的 DNS 出口是敞开的。这个测试不需要任何特殊工具纯标准库就能跑。提示测试时用一个你自己控制的域名不要用公共域名避免误伤和隐私问题。4.2 封堵方案一网络命名空间层面切断 DNS如果你用的是容器或微虚拟机最彻底的做法是在网络命名空间里直接丢掉 53 端口的出站包# 在沙箱的网络命名空间内执行 # 默认策略丢弃所有出站 iptables -P OUTPUT DROP # 只放行必要的目标 IP 和端口示例放行某个 API 的 443 iptables -A OUTPUT -d 203.0.113.10 -p tcp --dport 443 -j ACCEPT # 显式拒绝 DNS其实默认 DROP 已经覆盖这里是为了日志可见 iptables -A OUTPUT -p udp --dport 53 -j LOG --log-prefix DNS-BLOCK: iptables -A OUTPUT -p udp --dport 53 -j DROP iptables -A OUTPUT -p tcp --dport 53 -j DROP这套规则的关键是默认 DROP。很多人习惯默认 ACCEPT 再加拒绝规则那样一旦漏了某条路径就前功尽弃。默认拒绝 白名单才是正确姿势。4.3 封堵方案二用受控解析器替代系统解析如果工具确实需要域名解析就在沙箱内跑一个本地 DNS 转发器只允许白名单域名通过# 以 CoreDNS 为例的配置片段 .:53 { # 只允许白名单域名 template IN A { match ^([a-z0-9-]\.)?api\.trusted-service\.com\.$ answer {{ .Name }} 60 IN A 203.0.113.10 } # 其他一律拒绝 template IN A { match .* rcode NXDOMAIN } log }这个配置的效果是白名单域名返回固定 IP其他域名一律 NXDOMAIN。配合日志你还能看到沙箱里到底在尝试解析什么域名——这本身就是很有价值的威胁情报。4.4 封堵方案三应用层禁用直接解析如果沙箱运行的是你自己可控的代码可以在运行时层面拦截 DNS 调用。比如在 Python 里 monkey-patchsocket.getaddrinfoimport socket _original_getaddrinfo socket.getaddrinfo ALLOWED_DOMAINS {api.trusted-service.com} def guarded_getaddrinfo(host, *args, **kwargs): if host not in ALLOWED_DOMAINS: raise PermissionError(fDNS 解析被拒绝{host}) return _original_getaddrinfo(host, *args, **kwargs) socket.getaddrinfo guarded_getaddrinfo这种方法适合作为纵深防御的一层但不能作为唯一防线——因为子进程、原生扩展、其他语言运行时都可能绕过它。它的价值在于快速发现异常行为。5. 排查链路实录我是怎么定位到一次 DNS 外带尝试的5.1 异常信号一个不该出现的解析延迟事情是这样的。我在给一个 Agent 平台做性能调优时发现某些工具调用的耗时分布很奇怪大部分请求 200ms 内返回但偶尔会冒出 3 到 5 秒的尖刺。一开始以为是模型推理的抖动后来抓包才发现尖刺都发生在 DNS 解析阶段。正常的白名单域名解析应该是毫秒级的为什么会有几秒的延迟答案通常是解析请求发到了一个不可达的服务器触发了超时重试。而不可达的原因可能是沙箱的网络策略把 DNS 出口封了但应用层还在傻等。5.2 抓包定位tcpdump 看到的东西在沙箱的网络命名空间里抓包# 抓 53 端口的流量 tcpdump -i any -n port 53 -w dns.pcap # 分析抓到的包 tcpdump -r dns.pcap -n结果看到大量发往8.8.8.8:53的查询查询名是一串看起来像随机字符串的子域名。这就很可疑了——正常的业务代码不会去解析这种域名。进一步看这些查询来自一个第三方库的初始化过程而这个库的某个依赖被替换成了带外带逻辑的版本。5.3 根因依赖链里的一个投毒包顺着调用栈往上查发现问题出在一个间接依赖上。这个包在安装时会执行一个 postinstall 脚本往某个配置文件里写入一段代码在特定条件下发起 DNS 查询。这就是典型的供应链攻击你信任的是直接依赖但风险藏在间接依赖里。这次经历让我彻底改变了对沙箱网络策略的态度默认拒绝不是可选项是必选项。如果当时沙箱默认放行了 53 端口这个外带通道可能一直不会被发现。5.4 修复与验证封堵后如何确认有效修复动作分三步在网络命名空间层面默认 DROP 所有出站只放行必要 IP在沙箱内配置受控解析器白名单域名返回固定 IP重新跑一遍 4.1 节的检测代码确认解析失败验证时我特意用了几种不同的解析方式getaddrinfo、gethostbyname、直接构造 DNS 报文确保没有遗漏的路径。只测一种方式是不够的因为不同语言、不同库走的系统调用可能不一样。6. Agent 安全不能只靠沙箱分层防御的完整思路6.1 沙箱只是最后一层不是唯一一层这次事件最大的启示是不要把所有安全责任都压给沙箱。沙箱是执行层的隔离但 Agent 的安全链条要长得多输入层用户输入和外部数据要过滤防止提示注入模型层模型输出要校验防止生成危险指令工具层工具的参数要验证防止越权访问执行层沙箱隔离防止逃逸审计层全链路日志便于事后溯源任何一层单独做都不够。沙箱再严如果模型被注入后生成了合法的工具调用参数照样能造成损害。6.2 工具调用的参数校验该怎么做工具调用的参数是模型生成的必须当成不可信输入处理。校验要点类型校验参数类型必须符合预期字符串不能当路径用范围校验数值参数要有上下界防止资源耗尽路径校验文件路径必须规范化后检查是否在允许目录内防止../穿越命令校验如果工具会执行命令参数必须做白名单或转义防止命令注入我一般会在工具定义里加一个validate函数模型生成的参数先过这个函数不通过就直接拒绝不进入执行阶段。6.3 审计日志出事后你能查到什么DNS 外带这类攻击事后溯源全靠日志。至少要记录每次工具调用的完整参数和返回值沙箱内的网络连接尝试包括被拒绝的DNS 查询记录域名、时间、来源进程沙箱的生命周期事件启动、销毁、异常退出日志要集中存储不能放在沙箱内——沙箱销毁后日志就没了。我一般用 sidecar 模式把日志实时推送到外部系统。6.4 一个反直觉的结论可用性和安全性要一起设计很多团队做安全加固时习惯先做功能再补安全结果补的时候发现处处掣肘。正确的做法是安全策略和功能设计同步进行。比如工具需要访问外部 API设计时就要问这个 API 能不能用固定 IP能不能走内部代理能不能把调用挪到沙箱外这些问题的答案会直接影响架构。如果等到上线后再封 DNS可能会发现某个核心工具直接不可用了。7. 给 Agent 开发者的几条实操建议7.1 沙箱网络策略的默认值必须是拒绝这条没有例外。任何新起的沙箱网络默认全关然后按工具需求逐条开白名单。开的时候记录清楚为什么开、开给谁、什么时候能关。定期 review 白名单把不再需要的关掉。7.2 DNS 解析尽量在沙箱外完成如果工具需要访问的域名是固定的在沙箱启动前就解析好把 IP 通过环境变量或配置文件注入。这样沙箱内根本不需要 DNS 能力从源头消除外带通道。7.3 定期做外带测试把 4.1 节的检测代码做成自动化测试定期在沙箱里跑。一旦发现能外带立即告警。这个测试成本极低但能发现很多配置漂移导致的问题。7.4 依赖链要审计供应链攻击是 DNS 外带的常见载体。用锁文件固定依赖版本定期扫描已知漏洞对 postinstall 脚本保持警惕。如果条件允许用私有镜像源避免直接拉公共包。7.5 别忽视 IPv6很多人封 DNS 只封了 IPv4 的 53 端口忘了 IPv6。如果沙箱环境启用了 IPv6攻击者可能通过 IPv6 的 DNS 通道外带数据。封堵时要 IPv4 和 IPv6 一起处理。8. 写在最后这次事件真正值得记住的是什么我在实际做 Agent 平台的过程中踩过不少类似的坑。最开始总觉得沙箱嘛隔离了就安全了后来才发现隔离的边界远比想象中复杂。DNS 只是其中一个例子还有 ICMP、NTP、甚至时间同步协议都可能成为外带通道。这次 OpenAI 暂停工具调用表面看是一次产品调整实质是一次安全边界的重新校准。对做 Agent 的人来说真正该记住的不是OpenAI 出事了而是任何一个你以为只是基础设施的协议都可能是攻击面。DNS 之所以危险恰恰因为它太基础、太被信任、太容易被忽略。最后分享一个我自己的习惯每次设计一个新的沙箱环境我都会先问自己一个问题——如果我是攻击者在这个环境里我能通过哪些通道把数据送出去把这个问题回答清楚比堆砌一堆安全工具都管用。DNS 只是答案之一但它是很多人第一个漏掉的答案。