September 25, 2026·12 min read
Post-Quantum Cryptography: Harvest Now, Decrypt Later, and What It Means for Apache Flink
By Gabor Somogyi, Apache Flink PMC member
I started digging into post-quantum cryptography (PQC) for my own understanding, and the more I read, the more it looked like something teams running Flink in production should have on their radar now rather than later, not because a quantum computer is about to break TLS next week, but because the attack that matters here doesn’t wait for one to exist. It’s not purely theoretical either: the NSA already requires national security systems to exclusively use post-quantum algorithms for software and firmware signing by 2030. This post covers the problem with a concrete example, how it’s actually exploited, where the NIST-standardized solutions stand today, a small from-scratch Java demo you can run yourself to see the mechanics rather than take my word for it, and what it means for Flink’s own TLS configuration today.
The problem: two ways “quantum-safe, eventually” isn’t good enough
Almost every Flink deployment leans on classical public-key cryptography in two places: key exchange (TLS handshakes for RPC between JobManager and TaskManagers, the REST endpoint, and Kafka connections) and signatures (the same TLS handshakes’ certificate chains, plus any artifact or config signing in your CI/CD pipeline). Both rely on problems (the elliptic-curve discrete log, RSA factoring) that a large enough quantum computer solves efficiently using Shor’s algorithm. No such computer exists yet. Two separate failure modes still exist today because of that eventual possibility.
1. Harvest now, decrypt later
Imagine a Flink pipeline processing something with long-lived sensitivity (claims data, health records, transaction streams), checkpointing to S3 and exchanging data between TaskManagers over TLS. An adversary who can see the encrypted traffic (a network tap upstream of you, a compromised router, a nosy cloud neighbor with access to the right segment) doesn’t need to break your encryption today. They record the ciphertext and the ephemeral public keys exchanged during the TLS handshake, and wait. The moment a cryptographically relevant quantum computer exists, they run Shor’s algorithm against the recorded ECDHE exchange, recover the session key, and decrypt a transcript that’s been sitting on their disk for years.
This is harvest-now-decrypt-later, and the risk calculation isn’t about whether quantum computers exist now, it’s about whether your data still matters by the time one does. NIST frames this as a race condition (often attributed to Michele Mosca): if the time your data needs to stay confidential, plus the time it takes you to migrate, exceeds the time until a quantum computer capable of breaking it exists, you’ve already lost. The exposure happened at capture time, not at decryption time.
2. Signature forgery breaks trust retroactively and prospectively
The second failure mode is about integrity, not confidentiality, and it’s arguably worse because it isn’t limited to “old” data. An ECDSA public key is a point on an elliptic curve; recovering the matching private key from it is exactly the discrete-log problem Shor’s algorithm solves. The same TLS certificate chains that authenticate your Kafka brokers or JobManager REST endpoint, and any RSA/ECDSA signatures your build pipeline uses to sign release artifacts, become forgeable the day that computer exists, not just for messages signed in the past, but for anything an attacker chooses to sign under that identity going forward. A harvested private key doesn’t decrypt a stored transcript; it lets an attacker mint new, indistinguishable-from-real signatures indefinitely, including a forged certificate for a hostname your cluster trusts.
Neither failure requires today’s quantum hardware to be a today problem. They require you to still care about the data or still trust the identity by the time the hardware exists.
The solutions: what NIST actually standardized
In August 2024, NIST finalized the first two PQC algorithms relevant here:
| Standard | Algorithm | Purpose | Based on |
|---|---|---|---|
| FIPS 203: Module-Lattice-Based Key-Encapsulation Mechanism Standard | ML-KEM | Key exchange (replaces ECDHE/RSA key transport) | CRYSTALS-Kyber |
| FIPS 204: Module-Lattice-Based Digital Signature Standard | ML-DSA | Signatures (replaces ECDSA/RSA signatures) | CRYSTALS-Dilithium |
A third signature standard, FN-DSA (based on Falcon), hasn’t been published even in draft form yet. NIST’s own project tracker still lists its FIPS document (expected to be FIPS 206) as pending, so it’s not something to build on yet. Both finalized algorithms come in three parameter sets each (ML-KEM-512/768/1024 and ML-DSA-44/65/87), trading key/signature size against security margin. ML-KEM-768 and ML-DSA-65 are the commonly recommended defaults, roughly targeting AES-192-equivalent security.
JDK support landed in JDK 24
If you’re on the JVM, this is the part that matters most: JEP 496 and JEP 497 shipped ML-KEM and ML-DSA as final, non-preview java.security algorithms in JDK 24 (March 2025): no incubator module, no --enable-preview flag. You get them through the ordinary JCA surface:
KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-KEM-768");
KeyPairGenerator sig = KeyPairGenerator.getInstance("ML-DSA-65");
If your Flink cluster (or the client JVMs producing to it) is still on an older LTS release (11, 17, and 21 are all still common), Bouncy Castle’s bcprov-jdk18on (current stable: 1.86) implements the same algorithm names and works back to JDK 8, so you can start experimenting immediately without an upgrade.
Hybrid key exchange, not a straight swap
Nobody serious is proposing you replace ECDHE with pure ML-KEM in TLS. RFC 10024 (August 2026, IETF TLS working group) standardizes hybrid key exchange groups (X25519MLKEM768 is the practical default) that run classical X25519 ECDH and ML-KEM-768 side by side and combine both shared secrets into the one TLS actually uses. The logic is defense-in-depth. ML-KEM is built on a newer, less battle-tested hardness assumption than elliptic curves: it was only finalized as a formal standard in August 2024, though the underlying Kyber design was a public NIST competition candidate for years before that. X25519, by contrast, is decades-proven but quantum-vulnerable. A hybrid session stays confidential as long as either half holds. You lose nothing by combining them and gain a hedge against either scheme individually turning out to be weaker than believed.
Technical constraints: what you actually pay for this
PQC key exchange and signatures aren’t free: the constraint is size and, to a lesser extent, CPU. Here’s what I measured directly (not from a spec sheet, but from the demo below, run on JDK 17 + Bouncy Castle 1.86):
| Scheme | Public key | Signature / ciphertext | Quantum-vulnerable? |
|---|---|---|---|
| X25519 (ECDH) | 44 bytes (X.509) | None (32-byte shared secret) | Yes, via Shor’s algorithm |
| ML-KEM-768 | 1206 bytes (X.509) | 1088 bytes (encapsulation) | No (current cryptanalysis) |
| ECDSA P-256 | 91 bytes (X.509) | 70 bytes (DER, this run) | Yes, via Shor’s algorithm |
| ML-DSA-65 | 1974 bytes (X.509) | 3309 bytes | No (current cryptanalysis) |
That’s roughly a 27x growth in public key size (44 → 1206 bytes) and a 47x growth in signature size (70 → 3309 bytes), plus a new 1088-byte ciphertext blob the KEM exchange has to carry that X25519 never needed. For a single TLS handshake, that’s noise. For a Flink cluster doing frequent reconnects between TaskManagers, or between a TaskManager and the JobManager, at scale, or a Kafka cluster with thousands of client connections churning through handshakes, it’s worth actually load-testing before rollout rather than assuming it’s free. Extra handshake bytes and somewhat higher CPU cost per handshake are the real, measurable trade-off for the confidentiality/integrity guarantee.
The other constraint is maturity: ML-KEM and ML-DSA have had far less public cryptanalysis than RSA or elliptic curves, simply because they’re new. That’s exactly why the hybrid construction in RFC 10024 exists, and why it’s the right default rather than a pure PQC swap for anything you’d deploy soon.
Seeing it work: a from-scratch demo
Reading about ML-KEM and ML-DSA is one thing; running them is more convincing. I wrote a bare-minimal, dependency-light Java demo (no network calls, no Flink, just java.security plus Bouncy Castle for the algorithms your JDK might not ship yet) and pushed it to github.com/gaborgsomogyi/pqc-demo.
It does two things:
KemComparisonDemo establishes a shared secret three ways: classical X25519, pure ML-KEM-768, and a simplified hybrid combiner in the shape of RFC 10024’s X25519MLKEM768 (concatenate-then-hash rather than a byte-for-byte RFC implementation), so you can see the harvest-now-decrypt-later exposure and the hybrid mitigation side by side:
KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-KEM-768", "BC");
KeyPair bob = kpg.generateKeyPair();
KeyGenerator senderKem = KeyGenerator.getInstance("ML-KEM-768", "BC");
senderKem.init(new KEMGenerateSpec(bob.getPublic(), "AES"), new SecureRandom());
SecretKeyWithEncapsulation aliceSide = (SecretKeyWithEncapsulation) senderKem.generateKey();
SignatureComparisonDemo signs the same payload with ECDSA P-256 and ML-DSA-65 and prints both signatures’ sizes. It doesn’t (can’t) actually break ECDSA, since no cryptographically relevant quantum computer exists to run Shor’s algorithm against it, but it makes the mechanics and the size trade-off concrete rather than theoretical.
Clone it and run it yourself:
$ git clone https://github.com/gaborgsomogyi/pqc-demo.git
$ cd pqc-demo
$ mvn -q package
$ java -jar target/pqc-demo-jar-with-dependencies.jar
Every number in the constraints table above came from that actual output, not from a spec PDF.
Availability and caveats
FIPS 203 and FIPS 204 are final NIST standards, not drafts. JEP 496 and JEP 497 are final JDK 24 features. RFC 10024 is a published Proposed Standard. None of that is speculative. What is still moving: JSSE’s default negotiation of hybrid groups varies by JDK build and should be checked per release rather than assumed; NIST’s own transition-year milestones for organizations outside that national-security scope, in IR 8547, are still in draft; and the demo’s hybrid combiner is illustrative, not a certified RFC 10024 implementation. Don’t lift it directly into anything that needs to interoperate with another TLS stack.
The path forward for Flink
Flink doesn’t implement TLS itself. Its security.ssl.* settings configure the JDK’s own TLS engine underneath, or an OpenSSL-based one instead via security.ssl.provider=OPENSSL. The JDK (or OpenSSL) is what actually does the cryptography; Flink only decides how that engine is configured.
Those settings cover two places in a Flink deployment: internal traffic between JobManager, TaskManagers, and the blob server (security.ssl.internal.*), and the external REST endpoint (security.ssl.rest.*). Two more places aren’t covered by them at all: Kafka connector TLS is configured through the Kafka client, not Flink, and artifact or checkpoint signing is a separate integrity problem that no TLS setting touches.
Three settings matter here:
| Setting | Value | Why |
|---|---|---|
security.ssl.protocol |
TLSv1.2,TLSv1.3 |
Flink 2.4’s default. TLSv1.2 alone cannot negotiate a post-quantum hybrid group under any circumstances; listing both means both sides negotiate TLS 1.3 automatically whenever they support it and fall back to 1.2 otherwise, with nothing to configure. |
security.ssl.algorithms |
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384 |
Also Flink 2.4’s default. The first two suites are for a TLS 1.2 handshake, the last two for TLS 1.3; whichever protocol actually gets negotiated needs a matching suite in this list. |
security.ssl.named-groups (proposed, not in Flink yet) |
Depends on why you’d set it, there’s no single value to recommend here, see below | Doesn’t exist in Flink today; this is the option I’m adding. Unlike the two rows above, this isn’t something every deployment should set. |
The third row deserves more than a table cell, because a simple recommendation doesn’t actually hold up here, and I only found that out by testing it rather than assuming it. Once TLS 1.3 is negotiated (the two rows above), which key-exchange group gets used is decided almost entirely by whichever side is acting as TLS client for that connection: it proactively offers one candidate group per key-exchange category, and the server just accepts the first one it can use. The server’s own configured preference barely factors into that choice. That means a Flink-side option can’t reliably express “prefer the hybrid group”, the mechanics don’t cooperate with that framing, no matter how the option is designed.
What it can do is restrict: refuse to accept anything outside an explicit allowlist. That’s genuinely useful for two specific situations, not as a general recommendation for every deployment. Restricting to the traditional NIST curves only: SecP256r1MLKEM768,SecP384r1MLKEM1024,secp256r1,secp384r1,secp521r1, deliberately excluding X25519 and X25519MLKEM768. That’s for a deployment whose compliance framework specifically names P-256/P-384/P-521 and hasn’t caught up to Curve25519, a real and common gap even after NIST’s own SP 800-186 added Curve25519 in a 2023 revision. Or enforcing PQC-only key exchange with no classical fallback at all: X25519MLKEM768,SecP256r1MLKEM768,SecP384r1MLKEM1024, deliberately with no safety net, since accepting a classical fallback is exactly what this is meant to prevent. Setting either narrows what Flink will accept and will reject a peer that doesn’t meet it, that’s the point, not a bug to route around. If neither situation applies to you, leaving this unset is the right call: JDK 25 and 27 already prefer the hybrid group in their own default, with nothing configured anywhere, and a hand-picked list can only narrow that, never improve on it, for a deployment that has no specific compliance reason to restrict anything.
SecP256r1MLKEM768 and SecP384r1MLKEM1024 themselves aren’t in any released JDK yet, only X25519MLKEM768 is, a separate, later JEP (JEP 527, targeting JDK 27) from the JCA algorithm JEP 496 adds in JDK 24. They’re already in OpenJDK’s own upstream source, including its jdk27u maintenance branch, so this is a timing gap rather than a missing feature, but it means the NIST-curve example above only reaches the hybrid groups once a build picks that up; until then it falls through to the plain classical curves listed alongside them.
Separately from key exchange: internal traffic already authenticates both directions the moment security.ssl.internal.enabled=true, with no extra configuration needed by default. JobManager and TaskManager each present a certificate. Three narrower overrides exist underneath it (taskmanager.data.ssl.enabled, blob.service.ssl.enabled, pekko.ssl.enabled), each defaulting to true, so mutual TLS only drops out for one of those specifically if someone has separately turned it off. The REST endpoint doesn’t do this by default; set security.ssl.rest.authentication-enabled=true if you want the same guarantee there. That’s an identity question, separate from the confidentiality question above.
The first two rows are already Flink 2.4’s defaults, nothing to configure. The third is the one I’m building, and it’s narrower than it might sound: an enforcement lever for a specific compliance need, not a general recommendation for everyone.