HoneyLabs

Blog · · HoneyLabs

Analyzing 7 million Go TLS handshakes to see which Go versions scan the internet

Every handshake here came from a Go client that scanned one of our internet honeypots. Since Go 1.21 the toolchain takes its crypto/tls defaults from the go directive in go.mod, so each ClientHello carries the module's era rather than the compiler's. Measured with 36 lab builds against 6,954,270 such handshakes from 50,693 addresses: the share on a directive of 1.22 or older fell from 81 to 30 percent between March and May, only 4 percent of them send a Go user-agent, and a go 1.23 directive on a modern toolchain gets no post-quantum key exchange at all.

ShareLinkedIn
Analyzing 7 million Go TLS handshakes to see which Go versions scan the internet

HoneyLabs runs honeypots: sensors on the public internet that record what connects to them uninvited. Nothing is hosted on those addresses and nobody links to them, so everything arriving is a scan, a probe or a crawler: 53,160,834 connections from 233,689 addresses over the last seven months. Worth saying up front, because it bounds everything below: this is not a sample of Go programs in general, it is a sample of Go programs that go looking at strangers' IP addresses. What it is good for is watching that population change version over time, because a TLS handshake identifies the library that produced it even when nothing else does.

Go turned out to be the interesting case, and not for the reason we expected going in. A Go binary's handshake does not tell you which compiler built it. It tells you the go directive in the main module's go.mod, which is usually older and sometimes by years, and that has a consequence worth knowing before the rest of this makes sense.

Set go 1.23 in your go.mod, build with Go 1.24 or newer, and your program offers no post-quantum key exchange at all. You get neither Kyber, which Go 1.24 deleted, nor ML-KEM, which the directive tells the toolchain you are too old for. Both Go 1.23 and Go 1.26 would give you one if you asked either of them alone.

That follows from a rule most Go programs are subject to without anyone noticing. Since Go 1.21 the toolchain reads the go directive of the main module and sets its GODEBUG defaults to match that release. Go's documentation is explicit: when a module "declares an older Go version, the Go toolchain amends its defaults to match that older Go version as closely as possible." Several of those defaults live in crypto/tls, so the directive decides what your ClientHello looks like, and the compiler mostly does not.

Three step plots over Go release dates from February 2023 to August 2026, sharing an x axis. Cipher suites offered fall from 19 to 14 at Go 1.22 and to 13 at 1.23. Key exchange groups rise from 4 to 5 at 1.23 when Kyber is offered, stay at 5 when ML-KEM replaces it at 1.24, and rise to 7 at 1.26. Signature algorithms fall from 12 to 10 at 1.25 and rise to 13 at 1.27. Each step is a filled marker if an older go.mod directive still reverts it on Go 1.27 and hollow if not: the ECDHE-only cipher list, Kyber and ML-DSA are hollow, ML-KEM, the SHA-1 removal and the SecP hybrids are filled.

Read from the wire side, the cipher count alone separates three eras, because a directive of 1.21 or older offers 19 suites, 1.22 offers 14 and 1.23 or newer offers 13. The supported groups separate the rest, since ML-KEM appears at directive 1.24 and the two SecP hybrids at 1.26, and the signature algorithm list drops SHA-1 at 1.25.

Establishing that mapping took 36 builds of a four-line tls.Dial client, one for every Go toolchain from 1.20 to 1.27 against every directive from 1.20 up to its own minor. The handshake was read off a listening socket and parsed from raw bytes, so the measurement does not depend on the thing being measured. Two of the toolchains were built under Docker on Linux rather than on macOS, which also rules out the host, since both platforms agree on every toolchain where both ran.

Bubble plot with Go toolchains 1.20 to 1.27 as rows and go.mod directives 1.20 to 1.27 as columns, lower triangle only. Circle area is the address-weeks seen carrying that build's fingerprint since 7 June, and cells that produce the same fingerprint share one outline. The outlines run down columns: toolchains 1.21 and 1.22 on a directive of 1.20 or 1.21 share one, toolchains 1.23 and 1.24 on the same directive another, toolchains 1.25 and 1.26 a third. The largest circle is 51,820 for toolchains 1.25 and 1.26 on directive 1.25, then 14,595 for 1.23 and 1.24 on a directive of 1.20 or 1.21, 14,275 for 1.24 on its own directive and 13,068 for 1.20 on its own. 28 percent of address-weeks fall in an outline whose directive is 1.22 or older.

A three-build control shows the directive is acting through GODEBUG and not merely correlating with it. Go 1.27.1 with a go 1.27 directive produces JA3 725543c7; changing only the directive to go 1.21 produces ad8e2dde; and setting GODEBUG=tlsmlkem=0 by hand on the go 1.27 build produces ad8e2dde as well. Two different routes arrive at one handshake, which is what makes it a mechanism.

The step plots also show where the rule stops, because a directive can only restore behaviour the toolchain still implements, and GODEBUG settings are guaranteed for "a minimum of two years (four Go releases)" before they may be deleted. Go 1.27 removed tlsrsakex, so a 1.27 toolchain offers 13 cipher suites whatever the directive says, and tlskyber went in 1.24, which is why the go 1.23 case at the top of this piece ends up with nothing post-quantum instead of Kyber.

The result is visible from the other end of the connection. Of those 53 million events, 6,954,270 TLS handshakes from 50,693 addresses carry a ClientHello that exactly matches a fingerprint produced by one of the 36 builds, and the directive era is legible in every one of them.

Area chart over 29 weeks. The share of Go clients on a go.mod directive of 1.22 or older, which offers no post-quantum key exchange, falls from 81 percent of the population in the week of 22 February to 30 percent by 17 May, with the steepest movement between 15 March and 17 May, and then holds between 28 and 35 percent through to 12 September.

Watching that population week by week answers a question the Go team can otherwise only estimate, which is how quickly a crypto/tls default reaches deployed binaries. In late February, 81 percent of the Go clients we saw were on a directive of 1.22 or older and therefore had no post-quantum key exchange. By mid-May that was 30 percent, and it has stayed near 30 since. The move took about ten weeks, and the 1.22 band went in a single week in March, a step that reads as one large fleet rebuilding rather than many operators drifting. What remains is a floor: the directive 1.20 and 1.21 band has held between 25 and 35 percent since June and is not drifting.

Individual builds are legible in the same way, which makes the composition shift visible. In the week of 22 February the largest single group was go1.20 with a matching directive, at 48 percent of the Go addresses that week. By 12 September that group was down to 21 percent and the largest had become go1.25 or 1.26 on a directive of 1.25 or 1.26, at 44 percent. Go 1.27 was released on 19 August, its fingerprint first reached a sensor at 08:00 UTC on 27 August, and three weeks later it held 15 percent of the population.

Ridgeline of nine Go build classes over 29 weeks, one row each, ordered by the week that build peaked so the wave runs down the page. Each row is indexed to its own peak, printed on the right. The go1.22 directive row peaks in the first week and is gone by April. The go1.27 row is flat until the end of August and then rises to 1,185 addresses a week.

A few extremes fall out of the same measurement. The newest build in the set is go1.27, first handshake at 08:00 UTC on 27 August, eight days after the release. The oldest is a floor and not a value: a go directive below 1.20 is clamped to 1.20 by the toolchain, so a module still declaring go 1.13 is indistinguishable on the wire from one declaring go 1.20. Binaries from the go1.20 compiler itself are the largest single group here, 25,435 of the 50,693 addresses, from a release that shipped in February 2023. The oddest is an address that turned up on 5 September, joined by two more in the following fortnight, running a go1.27 compiler against a directive of 1.24 or older, a toolchain honouring defaults from a release up to three and a half years its senior.

One more thing falls out of selecting a population by its handshake instead of by what it says about itself. Of those 50,693 addresses only 2,221 send Go-http-client/1.1, while 28,581 of them send a browser string: Chrome on Windows, Safari on an iPhone, Konqueror on OpenBSD. The TLS layer is unmoved by any of it, because net/http sets a header while crypto/tls sets cipher order, key shares and signature algorithms, and only the first of those is a string a developer thinks to change. The population is not all adversarial either. Palo Alto Cortex Xpanse, zgrab and CensysInspect all appear in it, which is a useful check on the fingerprint set, since all of them are Go programs and all land where the lab says a Go program should. For the ones wearing a Chrome string the check is stronger than a shared JA4: of the 18,965 browser-labelled addresses seen since 5 June, when JA3 recording began, 17,014 have a JA3 that matches one of the lab fingerprints exactly, and JA3 keeps the supported-groups order that JA4 discards.

None of this is a vulnerability and nothing here needs patching, but it is a fingerprinting surface with two practical consequences. If you care what your binary looks like on the wire, the go directive is part of that surface, and raising it changes your TLS behaviour by design. And if you have a module sitting on go 1.23 while your CI uses a current toolchain, you are shipping the one combination that gets no post-quantum key exchange at all, which is worth ten seconds in go.mod to fix.

Dataset: 53,160,834 connections to HoneyLabs honeypots from 16 February to 19 September 2026, of which 14,918,300 carry a TLS client fingerprint. The Go population is the 50,693 addresses whose JA4 matches one of 10 fingerprints produced in the lab, counted over the 29 complete weeks from 22 February to 12 September 2026; the matrix weights and the JA3 cross-checks use 7 June onward, when JA3 recording began. Release notes, the GODEBUG history and release dates from go.dev.

Found this useful?

Pass it to whoever should see it next.

LinkedIn

New research, by email

Get new HoneyLabs research by email as it publishes: write-ups like this one, plus the threat reports on which ports moved, which KEV and recent CVEs are being probed in the wild, and the attack paths worth grepping your own logs for. Here is a recent threat report.

Double opt-in: we send one confirmation email and nothing else until you click it. Unsubscribe in one click, any time.