Back to Blog
Architecture2026-07-21

Proxying SPDY Upgrades for Kubectl Exec

Why standard HTTP/2 breaks kubectl exec over a transparent TLS MITM proxy, and how we avoid it by staying on HTTP/1.1 and splicing the upgraded stream.

When an AI agent runs kubectl exec or kubectl attach, it expects a seamless interactive session. Under the hood, this requires a persistent, bidirectional stream. The Kubernetes API implements this using SPDY protocol upgrades over HTTPS.

This presents a unique challenge for Iddio. As a transparent TLS MITM proxy, we sit directly in the data path. We intercept the connection, classify the intent, evaluate policy, and then forward the traffic. For standard HTTP requests, this is straightforward. For SPDY upgrades, standard proxy implementations fail catastrophically.

Here is how we solve the protocol upgrade problem by deliberately avoiding HTTP/2 and splicing the upgraded connection.

The HTTP/2 Problem

Go’s net/http will happily negotiate HTTP/2 over TLS whenever the peer offers it. For modern web traffic, HTTP/2 multiplexing is highly desirable.

For kubectl exec, it is a trap.

The protocol upgrade relies on hop-by-hop HTTP/1.1 headers, most notably Upgrade and Connection. The HTTP/2 specification forbids connection-specific header fields. If a proxy ends up speaking HTTP/2 on either leg, those headers cannot be carried through, the API server receives a request with no upgrade headers, and the kubectl exec session fails.

The Solution: Stay on HTTP/1.1 and Splice

Iddio sidesteps the problem by never offering HTTP/2 on either leg, then handling the upgrade by hand once policy has allowed it.

1. HTTP/1.1 on both legs

On the client side, the daemon terminates the intercepted TLS connection itself with tls.Server and a per-host leaf certificate. It advertises no ALPN protocols, so the client falls back to HTTP/1.1, and it reads requests with http.ReadRequest. There is no http.ResponseWriter involved, so there is nothing to hijack.

On the upstream side, the upgrade path dials the Kubernetes API server with tls.Dial and a plain tls.Config. Because no NextProtos are set, the server cannot select h2, and the connection stays HTTP/1.1.

2. Classify and decide first

The Kubernetes classifier works from the parsed request path and method. A request whose subresource is exec, attach, portforward or proxy is raised to the break-glass tier, regardless of headers. The upgrade headers play no part in classification.

Only after policy allows the request does the proxy check whether it is an upgrade request: an Upgrade header plus a Connection header containing the upgrade token. A denied request gets a 403 and is never spliced.

3. Forwarding the upgrade manually

For an allowed upgrade, the proxy opens the second TLS connection to the API server and writes the client’s request to it with req.Write, which keeps the Upgrade and Connection headers intact (they are only stripped when a request goes through an http.Transport). It then relays the server’s 101 Switching Protocols response back to the client.

4. Relaying the stream

From that point the HTTP request/response cycle is over. The proxy holds two TLS connections, one to the client and one to the API server, and starts two goroutines: one copying bytes client to server and the other server to client, until either side closes. It does not parse or modify the SPDY frames.

Conclusion

By understanding the underlying protocols, we maintain absolute control over the connection initiation. We correctly classify and govern the break-glass operation before any data flows. Once authorized, we step out of the way, dropping to a plain byte relay to preserve the SPDY protocol semantics.

The AI agent sees a completely normal, uninterrupted kubectl exec session. We achieve the security and visibility of a transparent MITM proxy without breaking the complex protocol upgrades that critical infrastructure tools rely on.

Try It Yourself

Iddio is open source. Deploy a zero-trust command proxy for your AI agents in minutes.