Back to BlogDevOps

GitHub Starts Dropping Classical-Only TLS Clients on October 7, and Your CI Image Is From 2024

From October 7, 2026, GitHub Enterprise Cloud with data residency refuses TLS connections from clients that only offer X25519 for key agreement. The same default is flipping in Envoy, Go, the JDK and OpenSSL. Here is what the handshake actually does, which containers and proxies will fall over, and the commands to check yours before the date.

TLSpost-quantumCI/CDOpenSSLGitHubInfrastructure
GitHub Starts Dropping Classical-Only TLS Clients on October 7, and Your CI Image Is From 2024

Here is the line that is going to ruin somebody's sprint: starting October 7, 2026, GitHub Enterprise Cloud with data residency stops accepting TLS connections from clients that offer only X25519 for key agreement. Those clients will not be able to establish HTTPS at all.

Not a 403. Not a deprecation header you can grep for ahead of time. The connection dies inside the handshake, before a single byte of HTTP exists. Your git fetch fails, your retry wrapper retries the identical failure five times, and the error you get is whatever vague thing your git client prints when a socket closes early.

Even if you are nowhere near data residency, the same switch is getting thrown all over the place right now. Envoy made X25519MLKEM768 its most preferred curve by default on non-FIPS builds, ahead of X25519. The prometheus/exporter-toolkit added it to the curve_preferences map in web-config.yaml. On the JVM side, JEP 527 taught the JDK's own TLS stack to negotiate the hybrid group, Java 27 shipped with that on September 15, and the JDK 25 backport is expected this month. Quarkus 3.39 even gives you quarkus.tls.pqc-enforcement-policy so you can demand it. RFC 10024 finally pinned down hybrid ML-KEM for TLS 1.3, which is why every vendor stopped waiting at roughly the same time.

So this is not one company being weird. It is a floor being raised.

What "offers only X25519" means in the handshake

A TLS 1.3 client sends two relevant things in its ClientHello: a supported_groups list, which is everything it is willing to do for key agreement, and a key_share, which is a guess at what the server will pick so the handshake can finish in one round trip.

X25519MLKEM768 is a hybrid. You do the classical X25519 exchange and an ML-KEM-768 encapsulation, then mix both secrets. An attacker has to break both, which is the point: a recording made today cannot be decrypted later by a quantum computer that only solves the elliptic curve half. The cost is size. That key share is over a kilobyte instead of 32 bytes, which is why some old middleboxes choke on a ClientHello that no longer fits their assumptions.

A server enforcing the new floor looks at supported_groups, finds no hybrid option in the list, and gives up:

Note where the failure sits. It is above your HTTP client and below your application code, which means neither of them has anything intelligent to say about it.

Who actually breaks

Not modern browsers. Chrome and Firefox have been sending hybrid key shares for a while, so your users are fine. The things that break are the ones nobody updates.

  • Anything linked against OpenSSL older than 3.5, which is where ML-KEM and the hybrid group landed. Your base image decides this, not your language version.

  • Go binaries built before 1.24, when X25519MLKEM768 became a default.

  • JVM services on an older JDK, until that 25 backport reaches you.

  • Any config that pins the curve list by hand. Go grep your repos for ssl_ecdh_curve, curve_preferences, SSL_CTX_set1_groups_list and --curves.

That last one deserves a bit of shame. Half the industry copied a "TLS hardening" snippet off a blog in 2019 that said ssl_ecdh_curve X25519; and praised itself for being strict. That exact line is now the thing that locks you out. Pinning an allowlist of cryptography is a bet that the good options never change, and that bet just lost.

The proxy is probably lying for you

This is the failure mode that burns a whole afternoon, because the client you tested is not the client the server sees.

If there is a corporate inspection appliance, a service mesh sidecar, or an egress gateway in the path, it terminates your TLS and opens its own connection upstream. Its crypto is what matters. You can have a perfectly modern container and still fail, and your local laptop test will pass because you are on home wifi.

Check it in about a minute

Ask the server what it will negotiate, from inside the environment that actually talks to it:

openssl s_client -connect github.com:443 -groups X25519MLKEM768 </dev/null 2>&1 \
  | grep -i 'negotiated tls1.3 group'

If openssl s_client does not recognise -groups X25519MLKEM768, you already have your answer. Then take inventory of the runner itself:

openssl version
node -p 'process.versions.openssl'
go version
java -version

node -p process.versions.openssl is the one people get wrong. Node bundles its own OpenSSL, so the system openssl version on the host tells you nothing about what your Node process will do.

Then make it permanent. Add a job to your pipeline that runs the s_client probe against the endpoints you depend on and fails loudly on a handshake error. It costs two seconds per build and it turns "the deploy is down and nobody knows why" into one red line with the group name in it.

What I would do this week

Run the probe in CI, not on your machine. Delete every hand pinned curve list you find and let the library choose. Write down which images are stuck on old OpenSSL, because that list is also your answer the next time a floor moves, and it will move again.

The crypto part of this is genuinely good work. Recording encrypted traffic now and decrypting it in fifteen years is a real threat model, and hybrid key exchange closes it without asking anyone to trust ML-KEM alone. The operational part is the usual mess: a quiet changelog line, a date, and a thousand containers that have not been rebuilt since the last time someone cared.