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...