Tailscale ships a non-reproducible crypto downgrade

nimih1 pts0 comments

security: Tailscale ships a non-reproducible crypto downgrade (DefaultGODEBUG=tlsmlkem=0) · Issue #20067 · tailscale/tailscale · GitHub

//voltron/issues_fragments/issue_layout" data-turbo-transient="true" />

Skip to content

Search or jump to...

Search code, repositories, users, issues, pull requests...

-->

Search

Clear

Search syntax tips

Provide feedback

--><br>We read every piece of feedback, and take your input very seriously.

Include my email address so I can be contacted

Cancel

Submit feedback

Saved searches

Use saved searches to filter your results more quickly

-->

Name

Query

To see all available qualifiers, see our documentation.

Cancel

Create saved search

Sign in

//voltron/issues_fragments/issue_layout;ref_cta:Sign up;ref_loc:header logged out"}"<br>Sign up

Appearance settings

Resetting focus

You signed in with another tab or window. Reload to refresh your session.<br>You signed out in another tab or window. Reload to refresh your session.<br>You switched accounts on another tab or window. Reload to refresh your session.

Dismiss alert

{{ message }}

tailscale

tailscale

Public

Notifications<br>You must be signed in to change notification settings

Fork<br>2.6k

Star<br>32.4k

security: Tailscale ships a non-reproducible crypto downgrade (DefaultGODEBUG=tlsmlkem=0) #20067

New issue<br>Copy link

New issue<br>Copy link

Open

Open<br>security: Tailscale ships a non-reproducible crypto downgrade (DefaultGODEBUG=tlsmlkem=0)#20067

Copy link

Labels<br>bugBugBugneeds-triage

Description

chad-loder<br>opened on Jun 9, 2026

Issue body actions

What is the issue?

Tailscale's released binaries (Linux/amd64 and the macOS app, probably others) from current versions back to at least 1.96.4 - carry "build DefaultGODEBUG=tlsmlkem=0", which disables X25519MLKEM768 in crypto/tls. This is the component that negotiates TLS for the control plane over HTTPS, essentially this undocumented setting disables the only post-quantum resistant key exchange algorithm in the control plane. The Noise algo does not help with PQ here, because it's based on the same curves as X25519, also vulnerable to Shor's.

As far as I can tell, this directive exists in none of Tailscale's public source - not the app, not the build scripts, not the Go fork. I can only conclude it's applied as a pre-build or post-build step in your private release pipeline. So the shipped client is not reproducible from public source for a security-relevant TLS default, and the public tree (where the Go 1.24+ default would offer the hybrid) misrepresents the crypto the binary actually negotiates.

So anyone auditing the source code for PQ resistance would conclude (incorrectly) that MLKEM is available for key exchange. In my opinion, assuming I'm correct, this is a transparency and reproducible builds issue between the public open source repo and the private build setup. I can understand why this might be shipped as a default (ClientHello packet bloat on MTU-restricted paths), but this really should be documented in the public repo.

Steps to reproduce

How I hit it: I run a self-hosted Headscale control plane behind Caddy, and I'd pinned the control endpoint to hybrid-only (curves x25519mlkem768, TLS 1.3 only). Every Tailscale client failed at the GET /key bootstrap with "remote error: tls: handshake failure". openssl confirmed Caddy was correct - it negotiates X25519MLKEM768 and refuses plain x25519, exactly as configured. So the client wasn't offering the group.

go version -m on the deployed tailscaled:

build DefaultGODEBUG=tlsmlkem=0

i.e. crypto/tls falls back to the pre-1.24 curve list (X25519, P-256/384/521), no ML-KEM. Same string in 1.96.4 (go1.26.1) and 1.98.2/1.98.3/1.98.4 (go1.26.3), so it's standing policy across two toolchains, not a one-off.

It's not Linux-specific either. The current macOS build (Tailscale-1.98.5-macos.zip, sha256 3a860bf5cb9275ae448ec4b85194dbc4a9bef07a567cd3abfddcfdd70617ae32) ships the same setting in its system network extension - the Go core of the macOS client, a different build target than the Linux daemon (module tailscale.io/xcode/ipn-go-bridge):

io.tailscale.ipn.macsys.network-extension: go1.26.3

build DefaultGODEBUG=tlsmlkem=0

Same go1.26.3, same baked tlsmlkem=0, different artifact. So the directive is applied pipeline-wide - across platforms and across distinct Go builds, not in any single packaging path - which is what you'd expect from a shared build-env step that touches every Go compilation.

I went looking for where it comes from, and it is nowhere public:

App source v1.98.3 (commit 8f2c8d6): no //go:debug, no go.mod godebug, the go directive is 1.26.3.

build_dist.sh, release/dist (cmd/dist), and tool/gocross: nothing sets a godebug.

Their Go fork at the exact build commit (the binary's tailscaleGoGitHash, e877d973840c91ec9d4bc1921b0845789de359ae) diffed against upstream go1.26.3 touches zero lines of crypto/tls, internal/godebug, internal/godebugs, or cmd/go/internal/load/godebug.go. Their fork's diff is...

tailscale build crypto tlsmlkem public reproducible

Related Articles