Post-quantum TLS key exchange on Cilium Gateway API Paperclip Inc. has deployed hybrid post-quantum TLS key exchange (X25519MLKEM768) on its Cilium Gateway API ingress to protect against the harvest-now-decrypt-later threat, using a mutating admission webhook because Cilium's Gateway API lacks native support for configuring Envoy TLS curves. The EU's coordinated roadmap requires member states to have national plans by end of 2026 and high-risk use cases migrated by end of 2030, with Germany's BSI mandating hybrid key exchange. Post-quantum TLS key exchange on Cilium Gateway API Cilium Gateway API has no knob for Envoy TLS curves. We added X25519MLKEM768 with a mutating admission webhook. The method, the measured cost, the caveats. Someone recording your encrypted traffic today cannot read it today. They can store it and wait. When a cryptographically relevant quantum computer arrives, everything they kept that was protected by classical key exchange becomes readable, retroactively, including sessions from years earlier. That is the harvest now, decrypt later problem, and it is why post-quantum key exchange is worth deploying before quantum computers exist. As of today, connections to paperclip.inc negotiate hybrid post-quantum key exchange X25519MLKEM768 . We host companies of AI agents /blog/what-is-paperclip-ai on infrastructure we operate ourselves in the EU, which means the front door is ours to change. Hybrid post-quantum key exchange means a TLS 1.3 handshake that derives its shared secret from both a classical algorithm X25519 and a post-quantum one ML-KEM-768, standardised by NIST as FIPS 203 https://csrc.nist.gov/pubs/fips/203/final , so the session stays secure unless both are broken. Getting it through our ingress took a mechanism nobody had published, because Cilium’s Gateway API has no way to configure it. What the quantum threat actually breaks The quantum threat is narrower than the marketing around it suggests, and the difference decides what actually needs changing. Symmetric encryption is fine. The usual line is that Grover’s algorithm https://en.wikipedia.org/wiki/Grover%27s algorithm halves your key length, so you should double it. That framing oversells the risk twice over: Grover halves the effective security level in bits rather than the key itself, and its quadratic speedup is essentially non-parallelisable, which is why NIST treats AES-128 as its Category 1 security benchmark for post-quantum algorithms. AES-256, ChaCha20-Poly1305 and HMAC-SHA256 need no change. Data at rest was never the problem. The problem is public-key cryptography, which Shor’s algorithm does break, and there the urgency splits in two. To exploit a broken signature, an attacker has to use it live, while the certificate is valid and someone is watching. To exploit a broken key exchange, an attacker only has to have been recording. The traffic is already captured, sitting on a disk. Breaking the key exchange later unlocks it. That asymmetry is why post-quantum TLS rollouts start with key exchange, and why ours did too. It is also why this has deadlines attached. The EU’s coordinated roadmap for the transition to post-quantum cryptography https://digital-strategy.ec.europa.eu/en/library/coordinated-implementation-roadmap-transition-post-quantum-cryptography asks member states for national plans by the end of 2026 and high-risk use cases migrated by the end of 2030, with public-facing TLS in the first wave. Germany’s BSI goes further in TR-02102-1 https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TG02102/BSI-TR-02102-1.html : it requires hybrid key exchange, post-quantum combined with a classical algorithm, rather than post-quantum alone. For a company hosting European data, hybrid is not a hedge on the way to something better. It is what the regulator asks for. Envoy could do it. Cilium had no way to ask. Key exchange is what protects a browser talking to us, so the work had to land on the public HTTPS ingress. We terminate TLS at Envoy, managed by Cilium’s Gateway API implementation on the Talos and Hetzner cluster we autoscale with our own Karpenter provider /blog/karpenter-hetzner . Envoy’s TLS stack is BoringSSL, which supports the hybrid group. The blocker was getting the setting to Envoy at all. Cilium’s Gateway API translation has no knob for Envoy TLS curves. It generates a CiliumEnvoyConfig from the Gateway resource, and that translation handles ALPN, proxy protocol, socket options and a handful of others. There is no field for tls params https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/transport sockets/tls/v3/common.proto , therefore no way to set ecdh curves , and no supported way to add Envoy configuration to a Gateway-managed resource at all. The upstream request for one, cilium 38089 https://github.com/cilium/cilium/issues/38089 , used ecdh curves and X25519MLKEM768 as its own motivating example. It drew no maintainer response and was auto-closed as stale.So the setting exists in Envoy and the path to reach it does not exist in Cilium. There are workarounds that take the Gateway away from the operator and hand-maintain the config, along with the load balancer and certificate wiring that come with it. Nobody had published a way across the gap that leaves the Cilium operator in charge, so we built one. Why the Cilium operator reverts your CiliumEnvoyConfig patch The obvious move is to edit the generated CiliumEnvoyConfig directly. We tried that. The patch was accepted, and the field was already gone when we read it back 0.6 seconds later. Our engineering notes recorded two things that contradicted each other: that the Cilium operator strips the field, and that the resource retains it. They cannot both be true, and the difference decided the whole approach, because three explanations fit the evidence: | Explanation | If true | Test | Result | |---|---|---|---| | The operator reverts drift on the resource it owns | Injecting during admission wins, because the operator’s own write carries the field | Put the same field on a config object no operator owns | Survives every re-read | | The cilium agent ignores the field for Gateway-managed configs | Nothing done at admission time matters | Serve a config we own carrying the field and offer a hybrid-only handshake | Ruled out: live Envoy answered ServerHello - NamedGroup: X25519MLKEM768 4588 | | The custom resource definition prunes the unknown field | | x-kubernetes-preserve-unknown-fields: true , so pruning is impossibleThe second and third would have killed the approach outright. They would also have killed the fallback we were holding in reserve, a Kubernetes 1.34 MutatingAdmissionPolicy https://kubernetes.io/docs/reference/access-authn-authz/mutating-admission-policy/ , since that injects at exactly the same point. Taking either on trust would have committed us to a one-way cluster upgrade and a full node reboot for a mechanism that could not have worked. The experiments were cheap and the answer was the first row: the operator reverts drift, within about a second, and nothing else touches the field. Which is good news, because a controller that reliably rewrites a resource is a controller whose writes pass through admission control. The fix: inject ecdh curves with an admission webhook An admission webhook watches CiliumEnvoyConfig writes in the gateway namespace and patches the one the Gateway generates, injecting the curve preference into every filter chain that carries a TLS context. The operator regenerates the config, that regeneration passes through admission on the way in, and it arrives already carrying the setting. The mechanism that used to undo the change now applies it. The injected fragment is small. This is what lands in the TLS context of the generated CiliumEnvoyConfig : And the registration, where two fields are doing the safety work: Three details are load-bearing. It fails open. failurePolicy: Ignore https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/ failure-policy , and every path through the code returns an allowing response, including on input it cannot parse. If the webhook is down or broken, config writes pass through unmodified. The already-stored setting stays put, and we only lose post-quantum key exchange the next time the operator rewrites that config, until the webhook is healthy again. It can degrade our crypto. It cannot stop our front door from being configured. The curve list keeps classical fallbacks. A hybrid-only list would hard-fail every client without ML-KEM support, which is the standard way to turn a security upgrade into an outage. Post-quantum for clients that have it, unchanged for everyone else. It finds the TLS chain by inspection, not by index. Indexing the filter chain by position works until a listener is added or reordered, at which point it stops matching and drops the front door back to classical with nothing to signal it. Controls that fail quietly into a weaker state are worse than ones that fail loudly, so the webhook patches every chain that actually carries a TLS context, and skips any that already has the setting so a repeated call cannot clobber it. Before pointing it at the live front door, we pointed it at a throwaway config object that routed nothing, confirmed the curves were injected there, and confirmed the real one was untouched. One failure mode was obvious enough to check up front: the operator wants the field absent, the webhook keeps putting it back, and that could have become a permanent write loop against etcd. Measured across 60 seconds, the resource version does not move. One gotcha is worth more than the rest of this section if you are reproducing it. Nudging the Gateway to trigger a reconcile does nothing on its own: if the config the operator derives is identical to the one already stored, it issues no write, so there is no admission request and nothing to mutate. The cutover needs a real write. We forced one by patching the config, which the operator then reverted, and that revert was itself the write that came back through the webhook carrying the curves. Verified on Cilium 1.18.10 with cilium-envoy v1.36.6, on Kubernetes 1.33. Envoy’s own documentation does not list X25519MLKEM768 among supported curves, so the version matters: check yours rather than assuming. What post-quantum key exchange costs in bytes and latency ML-KEM-768 keys are much larger than elliptic curve keys, so the client hello grows. | Measurement | Classical X25519 | Hybrid X25519MLKEM768 | |---|---|---| ClientHello key share extension | 38 bytes | 1222 bytes | | Handshake bytes out | 338 | 1522 | | Handshake bytes in | 3182 | 4269 | | Median handshake | 38.43 ms | 39.08 ms | | p90 / p99 | 60.09 / 103.99 ms | 73.12 / 124.92 ms | | Standard deviation | 15.18 ms | 20.89 ms | Method: 200 handshakes per arm, the two arms interleaved from a single EU client so network drift hits both equally. Measured against cloudflare.com, a reference endpoint that already supported the hybrid group, because at the time of measurement our own edge did not. These characterise the cost of hybrid key exchange in general, not our edge specifically. So it costs about 1.2 KB more from the client and 1.1 KB more back from the server, roughly 2.3 KB per handshake, every handshake. The median moved 0.65 ms, which is under one standard error of the mean at this sample size, so from a single vantage point it is not distinguishable from noise. Be careful about generalising that: the tail widens by around 20% at p90 and p99, and Cloudflare measured https://blog.cloudflare.com/pq-2024/ a real handshake slowdown of a few percent at internet scale. Small here does not mean free everywhere. The byte count has a second-order effect worth knowing about. A 1.2 KB key share pushes the ClientHello past a single packet, so it splits across segments. That is mostly uninteresting over TCP and genuinely matters for QUIC and for middleboxes that mishandle a fragmented hello. A couple of kilobytes per handshake buys the property that traffic recorded today does not become readable later. That is a good trade. What post-quantum key exchange does not protect Our certificates are not post-quantum. No publicly trusted certificate authority issues ML-DSA certificates as of July 2026. The key exchange is protected; the signature proving we are who we say we are is still classical. That is the right thing to fix first, for the timing reason above, but “post-quantum key exchange” and “post-quantum TLS” are different claims and we are only making the first one, here and in our security documentation /trust/security encryption . Node-to-node key exchange is still classical. Traffic between nodes in our network-isolated cluster /trust/security access is encrypted with WireGuard, whose ChaCha20-Poly1305 cipher is quantum-resistant, but whose key exchange is classical X25519. There is no way to add a post-quantum pre-shared key on top today: Cilium manages its WireGuard keys internally and exposes no hook for one. We claim nothing about traffic to model providers. Agents reach provider APIs directly with your own key /docs/paperclip/guides/org/agent-adapters , over whatever those providers negotiate. We do not control that far end. This one could not be rehearsed on staging. We have one Gateway and one generated config, shared by staging and production, so our usual rule of proving a change on staging first did not apply. We compensated structurally rather than procedurally: fail open, scope to a single named object, rehearse against a throwaway config that routes nothing, and keep rollback to deleting one object. That is a deliberate deviation and we would rather name it than have it found. Stated as a table, because “is Paperclip post-quantum” deserves a precise answer: | Path | Key exchange | Status | |---|---|---| | Browser to paperclip.inc | X25519MLKEM768 | Post-quantum | | Our TLS certificate | ECDSA signature | Classical, no public CA issues ML-DSA yet | | Node to node inside the cluster | WireGuard X25519 | Classical key exchange, quantum-resistant cipher | | Agent to model provider API | Whatever the provider negotiates | Not ours to control | | Backup transport to object storage | Classical | Encrypted at rest, classical key exchange in transit | We would rather publish a smaller claim we can prove than a larger one we cannot. How to verify post-quantum TLS with OpenSSL You do not have to take the headline claim on trust. With OpenSSL 3.5 https://openssl-library.org/post/2025-04-08-openssl-35-final-release/ or later: The first should establish a TLS 1.3 connection rather than fail. Two client-side traps: any OpenSSL before 3.5 has no ML-KEM support at all, and the openssl that ships on macOS is LibreSSL, which will reject the group name outright. Both produce a failure that tells you about your client rather than about our server. The