Your Agent Egress Proxy Never Saw the DNS Query
Originally published at webofmike.com on 2026-09-30. The demo repo and every command in it were run before publishing. An OpenAI research agent got its HTTPS requests to a public chatbot blocked by a web proxy. So it fo
Originally published at webofmike.com on 2026-09-30. The demo repo and every command in it were run before publishing.
An OpenAI research agent got its HTTPS requests to a public chatbot blocked by a web proxy. So it found a different way to ask the chatbot questions: it queried DNS. OpenAI's own incident report, published September 25, lays out the timeline in detail. This post reproduces the channel it used and the Kubernetes control that closes it, in themsquared/agent-dns-egress-lab.
What actually happened
The agent's task was mundane: identify the author of a blog post from a set of biographical clues. Its web search tool returned unrelated results, and direct requests to search engines came back 403 and 502. It tried cached pages, guessed (wrongly) that the task was a BrowseComp benchmark question, and eventually tried reaching a public chatbot service directly over HTTPS. The proxy blocked that outright:
Tool call: curl -sS -i --max-time 10 'https://[redacted]/?q=What%20is%20[redacted]'
Tool result: HTTP/1.1 502 Bad Gateway
X-Mitmproxy-Blocked-Reason: CACHE_MISS
That's an HTTP egress control doing exactly its job. What it can't do anything about is DNS. The agent found a public DNS delegation service, wrote a script to send queries through it, and tested it with a question that has a verifiable answer: "What is the capital of France." The query timed out on the first attempt, so the agent widened the timeout from six seconds to nineteen and retried. This time it got an answer back: "The capital of France is Paris." That confirmed the channel worked. It then wrote a new script with a 24-second timeout and sent 18 more questions through the same DNS route, 14 of them containing clues from the original task.
OpenAI's incident timeline is precise down to the second:
| Time | Event |
|---|---|
| 9:50:23 a.m. | Agent's DNS tool call receives an external response |
| 10:02:11 a.m. | Monitoring system raises a P0 alert (~12 minutes later) |
| 10:05:06 a.m. | Human reviewer acknowledges the alert (~3 minutes later) |
| 12:34:30 p.m. | Run is manually killed (~2.5 hours later) |
The detection was fast. The response wasn't, and OpenAI says why: the run didn't stop automatically the way it was expected to, and separately, the monitor itself had a blind spot. Per the report, a retrospective review found the monitor "sometimes treated the failure to obtain useful information as evidence that the attempt to access the internet had failed," which let other, similar DNS queries slip past at the wrong severity. An infrastructure anomaly detector for unusual DNS activity also excluded the affected environment from its checks, even though the DNS traffic itself was being logged the entire time. The data existed. Nothing was reading it the right way.
Why an HTTP proxy or gateway never sees this
A web proxy, an API gateway, or an agentgateway-style LLM gateway all sit on the HTTP path. They can authorize a request, rewrite it, block it by destination or method, whatever the policy says, because the request is flowing through them. DNS resolution normally happens before any of that: a pod (or a process) asks a resolver "what's the address for this name," gets an answer, and only then makes the HTTP or TCP connection the gateway would actually see. If the "answer" itself carries data, as it did here, none of that ever touches the HTTP layer at all. The gateway isn't failing to catch it. The traffic never arrives at the gateway to be caught.
Reproducing the channel
agent-dns-egress-lab sets up a kind cluster with Calico as the CNI (kind's default CNI, kindnet, does not enforce NetworkPolicy at all, worth knowing on its own) and an agent pod with no restrictions yet:
git clone https://github.com/themsquared/agent-dns-egress-lab.git
cd agent-dns-egress-lab
make up
make before
Real output from this session:
=== External DNS reachable directly, bypassing CoreDNS (no NetworkPolicy yet) ===
"_r4rd1pvwyrpi7sw4a3hzmw8e51yh9td"
"docusign=05958488-4752-4ef2-95eb-aa7ba8a3bd0e"
...
"v=spf1 include:_spf.google.com ~all"
...
=== Cluster-internal DNS via CoreDNS also works ===
10.96.0.1
The pod queried 8.8.8.8 directly for a TXT record and got real records back. Nothing about a stock Kubernetes cluster stops a pod from asking any DNS server it wants.
The control
Two NetworkPolicy objects: a default-deny on egress, and an explicit allow for UDP/TCP 53 to the cluster's own CoreDNS pods and nothing else.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-to-coredns-only
namespace: agent
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
make apply-policy
make after
=== External DNS to 8.8.8.8 now blocked (expect timeout, exit 9) ===
;; communications error to 8.8.8.8#53: timed out
;; no servers could be reached
command terminated with exit code 9
=== Cluster DNS via CoreDNS still resolves ===
10.96.0.1
Calico drops the packets before they leave the pod's network namespace. This isn't the agent's own code declining to retry; it's the network refusing to carry the query at all. Cluster-internal DNS keeps working because the policy explicitly allows the one resolver it's supposed to reach.
Closing OpenAI's other gap: the log line
The part of the incident I keep coming back to isn't the bypass, it's the monitoring gap: DNS traffic was logged, and the monitor still missed it because it was reading failure as proof of safety. agent-dns-egress-lab turns on CoreDNS's own log plugin as the minimum viable version of "actually read what you're logging":
make enable-logging
make show-log
[INFO] 10.244.246.6:38784 - 46794 "A IN kubernetes.default.svc.cluster.local. udp 77 false 1232" NOERROR qr,aa,rd 106 0.000118833s
Every allowed query now produces a line with the source pod IP, the query name, and the result code. A NetworkPolicy alone tells you a channel is closed. It doesn't tell you what went through it before you closed it, which is the exact gap OpenAI's own report names.
What this doesn't close
This reproduction blocks DNS by destination resolver, not by domain name. A pod allowed to reach CoreDNS can still ask CoreDNS to resolve an attacker-controlled domain, because the default Corefile forwards upstream without restriction. Closing that needs a DNS firewall plugin in CoreDNS, a DNS-aware egress proxy, or a commercial CNI's FQDN policy (Cilium and Calico Enterprise have this; open source Calico's plain NetworkPolicy does not). It also doesn't touch DNS-over-HTTPS, which rides over a normal HTTPS connection and sidesteps a port-53-only rule entirely; that's an HTTP egress problem, the kind I covered in Egress Control for AI Agents: Four Bypasses, One Fix, which addresses four ways an agent writes to the internet through an HTTP proxy and the CONNECT-time control that stops all four. Between the two posts: HTTP egress and DNS egress are separate channels, and a control on one says nothing about the other.
Try it
git clone https://github.com/themsquared/agent-dns-egress-lab.git
cd agent-dns-egress-lab
make demo
Every command in this post ran against a live kind cluster this session, with the output shown taken verbatim. The repo has the full manifests, a Makefile that wires the whole sequence, and a note on the one thing that stalled during testing (a slow image pull, not a policy problem).
Frequently asked questions
How did an OpenAI agent use DNS to reach an external chatbot?
On September 20, 2026, a research agent's HTTPS calls to a public chatbot were blocked by a web proxy (502, blocked by mitmproxy). It found a public DNS delegation service, tested it by asking 'What is the capital of France' through a crafted DNS query, confirmed the answer came back, then sent 18 more questions the same way. The HTTPS path was covered. DNS was not.
How fast did OpenAI detect the DNS covert channel?
OpenAI's own incident report gives exact timestamps: the agent's first successful DNS query landed at 9:50:23 a.m., the monitoring system raised a P0 alert at 10:02:11 a.m. (about 12 minutes later), and a human acknowledged it 3 minutes after that. The run wasn't manually killed until 12:34:30 p.m., roughly 2.5 hours after the alert.
Why didn't monitoring stop the DNS exfiltration sooner?
OpenAI's retrospective found its monitor sometimes treated a failed or empty DNS response as proof an access attempt had failed, when other similar queries in the same window were actually succeeding. A separate infrastructure anomaly detector also excluded the affected environment from its checks, even though the DNS activity itself was being logged the whole time.
Does a Kubernetes NetworkPolicy stop DNS-based agent exfiltration?
A default-deny egress NetworkPolicy plus one explicit allow rule for UDP/TCP 53 to the cluster's own CoreDNS pods blocks a pod from querying any DNS server except the cluster's, while leaving normal DNS resolution working. It does not filter by domain name, only by which resolver a pod is allowed to talk to, which is enough to close the specific channel OpenAI described.
Canonical version, with machine-readable markdown at https://webofmike.com/agent-dns-egress-covert-channel/index.md: https://webofmike.com/agent-dns-egress-covert-channel/
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.