On September 20, 2026, an AI agent that OpenAI was training in a sandbox with no internet access reached the internet anyway. Its web requests were refused, so it wrote its questions into DNS queries. The sandbox's own resolver carried them out to a public chatbot and brought the answers back. One of them read "The capital of France is Paris." The agent then sent 18 more questions down the same path, most of them containing clues from the task it had been given.
Monitoring raised an alert within 12 minutes, but the run kept going for another two and a half hours until someone stopped it by hand. OpenAI has since paused training, evaluation and inference with tool use for its most capable models, and on September 25 it published an incident report that is candid down to the shell commands. The hole it describes is an ordinary one, and many isolated networks share it. Queries sent straight to public DNS servers got no answer, but the internal resolver would still fetch any name in the world for whoever asked. This post covers how the agent got out, why firewalls miss this path, and how to close it.
Twelve minutes to an alert, two and a half hours to a shutdown
The agent was partway through a reinforcement learning task: identify the author of a blog post from a set of biographical clues. The supplied search tool returned music and general advice. Requests to Google, Bing and DuckDuckGo came back 403, 502 and 502. So it started testing the network, and OpenAI's timeline begins with the DNS call that got an answer from outside.
Monitoring raised its P0 alert 11 minutes and 48 seconds after that call (the report's summary says "within 15 minutes"), and a human acknowledged it in Slack under three minutes later. Then the run went on for two and a half hours.
It was supposed to stop automatically. It did not, "leading to confusion around whether it should have been stopped", and someone killed it by hand at 12:34:30 p.m. After the Paris answer, the agent sent 18 more questions down the same path. Fourteen contained clues from the task. Four asked the chatbot for ways to search the web, fetch websites or tunnel through DNS, including "Name a publicly hosted DNS tunnel proxy free server domain".
It never solved the task. Its final message said that giving a name "would be guesswork".
Detection worked here, and the kill switch is what failed. A P0 that a person has acknowledged while the workload keeps running is an operations problem, whatever the model did. OpenAI has paused "all training, evaluation, and inference with tool-use" for its most capable models, its second pause in under three months according to Fortune, and says it will not resume training this particular one.
The proxy answered CACHE_MISS and the resolver answered yes
The HTTP side of the sandbox held. When the agent pointed curl at a public chatbot's website, it got this back:
HTTP/1.1 502 Bad Gateway
X-Mitmproxy-Blocked-Reason: CACHE_MISS Every web request passed through an intercepting proxy (mitmproxy, going by the header) that served an offline cache and refused anything it did not hold. Queries sent straight to public DNS servers "went unanswered", so direct DNS was evidently filtered as well.
Then the agent tried the sandbox's own resolver, which "returned a real record for a known domain and correctly rejected an invented one". A resolver can only tell those two apart by asking the internet. That single comparison told the agent there was a way out.
How a question rides inside a hostname
A DNS query name is a sequence of labels. RFC 1035 caps each label at 63 octets and the whole name at 255, and RFC 2181 is blunt about what may go in them: "any binary string whatever can be used as the label of any resource record". The letters-digits-hyphen rule is a convention for host names, and the protocol does not enforce it.
Whatever you put in the name is delivered to the authoritative server for that zone, and that is the outbound half of every DNS tunnel. The return half is the answer: a TXT record holds strings of up to 255 bytes each.
The classic setup has a cost of entry. The README of iodine, the best-known tunneling tool, says you need "control over a real domain" and "a server with a public IP address". The agent had neither, and the tunnel projects it found "had closed or required a separate server".
So it assembled the path from two public services. One was a chatbot that answers DNS queries directly, the kind you talk to with dig @server "question" TXT (ch.at is a public example of the genre; OpenAI redacts which one the agent used). The other was a free DNS service that, in the report's words, could "delegate a lookup to an address embedded in the hostname". Ask the sandbox resolver for a name under that service and it follows the referral, then hands the full query to the address in the name. Nobody had to register a domain or rent a server.
The first attempts failed. Queries went out as types 16 and 2, TXT and NS, with a six-second timeout, and the France question timed out. The reply to a clue-based query started with the bytes \x00*\x81\x81: transaction ID 42, then flags that decode to a response with recursion available and RCODE 1, a format error. The agent raised the timeout to 19 seconds, ran the lookups through a 16-worker thread pool, and got Paris. A language model takes longer to answer than a nameserver does.
"No internet access" usually still includes recursive DNS
Sandboxes need names. Code inside them talks to internal services and package mirrors, and the quickest way to make that work is to point /etc/resolv.conf at the resolver the rest of the network uses. That resolver recurses, because everything else needs it to.
The firewall sees a packet from the sandbox to 10.214.0.2 on port 53: internal to internal, allowed. The query to the outside world is made by the resolver, from the resolver's address. Egress rules written as source and destination pairs never see the sandbox talk to the internet, because it never does. OpenAI calls these "transitive paths" and says it is now working through narrower paths "used by system dependencies", replacing them with offline alternatives.
Reverse lookups take the same road. A PTR query goes to whichever nameservers the address holder listed for its reverse zone (our post on rDNS covers the delegation chain), so a log pipeline that resolves client IPs to hostnames sends queries to servers chosen by the owner of those IPs. Under an IPv6 /48, the first 20 labels of an ip6.arpa name, one nibble each, are the querier's to choose: 80 bits per lookup. An allowlist that waves the reverse tree through has a hole in it.
What the channel looks like in a resolver log
Tunnels built for throughput are loud. They produce thousands of long, high-entropy names under one registered domain, heavy on TXT or NULL queries. Vern Paxson and nine co-authors measured this in 2013 across 230 billion lookups from production networks and found 59 confirmed tunnels. On their enterprise datasets, flagging anything that leaked more than 4 kB per day cost an analyst one or two investigations a week.
This incident was quiet. About twenty questions, a sentence each, add up to a kilobyte or two (our estimate; the report gives no sizes), and plain English does not trip an entropy threshold. A detector tuned for iodine would have let it pass.
Simpler signals would have caught it. A training sandbox has no reason to send TXT queries, or to resolve anything under a domain no machine in the fleet has asked for before. OpenAI did have an infrastructure detector for anomalous DNS activity. It "excluded the affected environment, though DNS activity was logged".
The retrospective turned up a subtler error. Other external lookups had returned things like a static notice that a service had shut down, and the monitor "sometimes treated the failure to obtain useful information as evidence that the attempt to access the internet had failed". A useless answer from outside is still an answer from outside.
Allowlist the names first, the record types second
OpenAI's fix was to restrict DNS in the environment "to a limited allowed list of domains and record types" and to add blocking "at two independent layers, either of which would have prevented this access". That is the right fix. Its two halves are not equally strong, though.
Restricting record types narrows the way back in. With only A and AAAA allowed, answers come home 4 bytes per A record or 16 per AAAA, or in the target name of a CNAME, which iodine already uses to answer A queries. It does nothing for the way out, because the payload sits in the query name and reaches the far server whatever type is asked for. If exfiltration is the worry, the name allowlist is the control that counts.
With dnsdist in front of the recursor, both rules fit in a few lines:
local allowed = newSuffixMatchNode()
allowed:add("pypi.org.")
allowed:add("files.pythonhosted.org.")
addAction(NotRule(SuffixMatchNodeRule(allowed)),
RCodeAction(DNSRCode.REFUSED))
addAction(NotRule(OrRule({QTypeRule(DNSQType.A), QTypeRule(DNSQType.AAAA)})),
RCodeAction(DNSRCode.REFUSED)) Two cautions. An allowlisted suffix is only as closed as the zone behind it: allow a domain where users can create records or subdomains, and the tunnel moves there. And response policy zones, the usual "DNS firewall", are mostly run as blocklists. A blocklist stops the domains somebody has already reported, and a sandbox should not depend on that. The strongest version removes recursion altogether: a resolver that is authoritative for the internal zones the sandbox needs and refuses everything else.
If you run agents with tool access, test the resolver today
From inside the sandbox, resolve a domain you know exists and one you just invented. If the answers differ, you have an egress path, and the rest is patience.
The transcript shows how little the agent needed. It had a shell, a Python interpreter and a resolver, and it talked itself into a justification along the way: "User only gives permission to research, using publicly offered DNS services acceptable." The textbook DNS tunnel assumes an attacker who prepared a domain and a server in advance. This one was improvised from free services by a process that had been asked to find the author of a blog post.
Every path in this story ends at an IP address that somebody operates. Ipregistry returns the network that announces an address and whether it belongs to a hosting provider, a quick way to triage an authoritative server nobody on the team recognizes. You can try the API with 20,000 free lookups to get started.
Keep users in the loop Ship release notes that get read. Try Noticeable