Go CVE-2026-111943: Why Protection Against the eMudhra Keys Lives in the Binary, Not the Trust Store
When a certificate authority loses control of its keys, browsers and distributions remove its certificates from their trust stores, and for most programs that settles the matter. With the eight compromised eMudhra intermediate certificates this is not enough, because the same keys lead to trusted roots along several paths and most programs outside the browser check neither revocation nor Certificate Transparency. Go therefore took a different route with CVE-2026-111943 and blocked the keys directly in crypto/x509, regardless of which root a program trusts. The catch lies in how Go programs are built. The standard library is compiled into every binary, so every tool, operator and controller in your cluster stays unprotected until it has been rebuilt with Go 1.27.3 or 1.26.10.
TL;DR — 90 seconds
- What: Go 1.27.3 and 1.26.10 add a denylist to
crypto/x509containing the keys of the eight compromised eMudhra intermediate certificates.Certificate.Verifydiscards every chain that contains one of these keys. - Severity: The Go team gives no CVSS score; Amazon rates the flaw at CVSS 3.1 6.8. I rate the case as critical because the private keys can be computed from the public certificates, and an attacker in a suitable network position can use them to take over TLS connections.
- Who is affected: Every Go program built before the fix that validates TLS endpoints against the system roots, including kubectl, Helm, OpenTofu, operators and controllers as soon as they talk to public endpoints. Go 1.25 and older get no fix.
- What to do: Rebuild and roll out your own programs with Go 1.27.3 or 1.26.10, update third-party tools to releases built with that toolchain, and refresh the CA packages in all your images as well.
- What would have helped beforehand: Your own trust anchors for internal endpoints instead of the entire Web PKI, an inventory of the Go versions in your binaries, and a pipeline that rebuilds automatically when a new Go patch appears.
One certificate chain, three paths to trust
What happened at eMudhra is covered in detail in the post on the eMudhra CA compromise. For Go, one property of the case matters most. The eight intermediate certificates were created with ECDSA keys whose private part is smaller than 256, so the key can be recovered from the public key in milliseconds. The certificates are public, and that makes the keys effectively public too. On top of that, each of these certificates leads to a trusted root along more than one path, once through the emSign hierarchy under emSign ECC Root CA - G3 and additionally through cross-certificates that Sectigo issued under AAA Certificate Services.
Why this is particularly unpleasant for Go programs becomes clear from the code that loads the system roots on Linux. crypto/x509 searches a fixed list of paths such as /etc/ssl/certs/ca-certificates.crt for Debian or /etc/ssl/cert.pem for Alpine, takes the first file it finds and also reads directories such as /etc/ssl/certs. The variables SSL_CERT_FILE and SSL_CERT_DIR override these paths. All of this is loaded exactly once per process, on first use. I read that from the sync.Once in root.go, and it has a practical consequence: a controller that has been running for weeks only sees an updated CA package in its container after a restart.
The third point concerns what Go does not do during a connection. crypto/x509 checks neither CRLs nor OCSP nor Certificate Transparency. A Go client therefore never notices that eMudhra revoked the intermediates on October 18 and Sectigo revoked the cross-certificates on October 19. On macOS and Windows, Go does leave verification to the platform as long as the program uses the system pool. On Linux, and thus in practically every container, the list of roots in the image is all that counts. If the emSign root is missing there but AAA Certificate Services is not, the path via the Sectigo cross-certificates stays open, at least as I read the combination of loading code and chain building.
What the fix commit changes
Fix commit 7f86f95 by Roland Shoemaker is titled “crypto/x509: distrust a handful of intermediates” and touches three files. In verify.go it introduces a map distrustedSPKIHashes with eight SHA-256 hashes, each computed over a certificate’s SubjectPublicKeyInfo field, and a function containsDistrustedSPKIHash that computes this hash for every link in a chain and looks it up. Verify calls it in the same filter that already sorts out chains with invalid policies, unsuitable key usage or violated name constraints. If no chain survives, verification fails.
Because this filter runs after chain building, it applies no matter where the roots come from, whether from the system, from a custom CertPool or from x509roots/fallback. For macOS and Windows the commit adds the same check to systemVerify in root_darwin.go and root_windows.go, because the platform verifier would otherwise bypass Go’s own filter. I see no GODEBUG switch to turn it off in the commit.
The choice of identifier is the most revealing part. What gets blocked is keys rather than certificates, which according to Google is also how Chrome set up its CRLSet block. A hash over the public key matches the eMudhra issuance and the Sectigo cross-certificate alike, because both carry the same key. So the Go team blocks what is actually compromised rather than one of the paths leading to it. The price is that the list lives inside the program itself and has to ship with it.
Attack path and who is really affected
An attack needs two things: a certificate signed with one of the eight keys, and a position in the network from which a TLS connection can be redirected. Anyone who knows the public intermediate certificates can issue such a certificate, because according to Scott Helme’s analysis the keys can be computed in milliseconds. The network position is the real hurdle. Typical routes are a manipulated DNS resolver, a compromised proxy, an open Wi-Fi network or a compromised node in the same network. If both succeed, the attacker sees and alters everything the client sends and receives, without the client reporting an error.
In a Kubernetes environment it is worth looking at which Go programs validate against the system roots at all. Traffic to the API server from inside the cluster is hardly affected, because client-go’s in-cluster configuration uses the CA certificate from the service account mount as its only trust anchor. It is different for everything that talks to the outside. cert-manager obtains certificates from ACME servers, Argo CD and Flux clone from Git hosts, OpenTofu providers and external-dns talk to cloud APIs, Helm pulls charts from registries, and the GitLab Runner registers with GitLab. By default, all these connections trust the roots in the image. I derive that from how crypto/x509 works. Whether an individual tool ships its own roots is something you need to check case by case.
You are really affected if such a program was built before the fix and runs in an image whose CA package still contains the affected roots. Two groups, on the other hand, easily believe they are affected when they are not. Programs that talk exclusively to their own private CA do not trust the eMudhra roots at all. And for verification with the key usage ServerAuth, the default for Verify, the two code-signing intermediates play no role. According to the list on the disclosure site they only allow code signing, and the filter discards chains with unsuitable key usage before it ever consults the denylist.
| CVE | Affected | Fix | Precondition | CVSS |
|---|---|---|---|---|
| CVE-2026-111943 (GO-2026-6809) | Go before 1.26.10 and 1.27.0 to 1.27.2, symbol crypto/x509.Certificate.Verify | 1.26.10 / 1.27.3 | Program trusts a root through which the eight keys are reachable; attacker in network position | 6.8 per Amazon (own rating: critical) |
Misuse of the keys against real targets has not been documented so far. According to Google, no TLS certificate under the six TLS intermediates had been logged to Certificate Transparency before the block, and eMudhra states in the Mozilla bug that none of the eight intermediates issued end-entity certificates. For clients without CT checks this proves little, though, because an attacker does not have to log a forged certificate anywhere. The CVE is not in the CISA KEV catalog, and its EPSS score stood at 0.145 percent on October 21.
Go 1.27.3, 1.26.10 and the way into your binaries
The fixed versions are Go 1.27.3 and Go 1.26.10, both dated October 19, 2026 in the release history, followed by advisory GO-2026-6809 on October 21. The Go team recommends upgrading immediately. Go 1.25 gets no fix, because according to the release policy each version is only supported until two newer ones have appeared, and Go 1.27 came out on August 19. If you still build with 1.25, the way forward is a move to a supported version.
For operations, what matters most is how Go differs here from languages with a shared runtime. With Python, Node.js or Java you swap a runtime or a CA package, and every program benefits. A Go binary, by contrast, carries its own standard library, and the new denylist is part of it. For your own programs that means raising the toolchain, either by setting the build container to golang:1.27.3 or golang:1.26.10 or by adjusting the toolchain line in go.mod, then rebuilding and rolling out. For third-party tools such as kubectl, Helm, OpenTofu, Traefik or your operators you have to wait for releases built with the new toolchain, and for many projects that is likely to take days to weeks.
That is why the second leg always belongs with it: current CA packages in every image. On Linux, Go reads the roots from the image at runtime, so a refreshed package also helps older binaries as soon as the process restarts. It only covers the path via the removed roots, though. Whether your CA package also removes AAA Certificate Services differs between distributions, and according to Red Hat bulletin RHSB-2026-012 there is only a blocklist workaround for RHEL so far. At the time of writing, Amazon Linux 2023 still had no updated package according to issue #1147.
For images without a CA package, such as scratch, Go offers the module golang.org/x/crypto/x509roots/fallback, which compiles a root bundle into the program. In the current module version v0.0.0-20261020153306-9f2b28d4331a the only remaining eMudhra root is emSign Root CA - G1, and AAA Certificate Services is absent altogether, as I checked in the bundle itself. If you use the module, bump it as well. The fix only changes behaviour for connections that run through the blocked keys. They will fail with a certificate error from now on, which should not happen for legitimate services, because eMudhra states that it issued no end-entity certificates from these intermediates.
Hardening beyond the patch
To put things in context, it helps to ask what is actually being attacked. It is not a bug in the program but a trust decision: the client believes a certificate because a chain leads to a root it considers trustworthy. Anything that shrinks the set of trusted issuers, gets the denylist into every binary quickly or makes the network position harder to obtain is therefore effective.
Your own trust anchors for internal endpoints
The most effective measure is not to hand Go programs the entire Web PKI for internal connections in the first place. A service that only talks to your database, your Vault or an internal API gateway needs nothing but your private CA as a trust anchor. In Go you set RootCAs in the tls.Config to a custom CertPool, and many tools offer an option such as --cacert or a CA path in their configuration. For connections inside the cluster, a service mesh with mTLS or certificates from a cert-manager issuer with your own CA achieves the same. The effort lies in distributing and rotating the CA; the gain is that no public certificate authority anywhere can influence your internal traffic any more.
Maintained trust bundles via SSL_CERT_FILE
For connections to the internet you still need the Web PKI, but you can decide which bundle applies. Because Go honours SSL_CERT_FILE on Linux, you can give every pod a centrally maintained bundle instead of relying on the CA package of each image. In the cluster, cert-manager’s trust-manager produces such bundles as ConfigMaps. According to its documentation the default package must be at least trust-pkg-debian-trixie:20250419-deb13u1.0 or trust-pkg-debian-bookworm:20250419-deb12u2.0; older tags keep trusting the compromised CA. Mounted as a file and set via the environment variable, this bundle applies to every Go program in the pod. There are two limits to keep in mind. Go programs read the bundle only at startup, so an updated bundle takes effect only after a rollout. And a bundle does not replace the denylist as long as it contains AAA Certificate Services.
An inventory of Go versions
Before you can rebuild, you need to know what to rebuild. Go writes the toolchain it used into every binary, and go version -m reads it out together with the module versions. govulncheck -mode binary goes one step further and matches a binary’s symbols against the Go vulnerability database, reporting standard library findings as well. Both can run in the pipeline against every image you build and regularly against the images in the cluster. Many container image scanners also detect the Go version in binaries, but check whether yours already knows GO-2026-6809. Without such an inventory, rebuilding remains guesswork, especially for tools that were copied into a base image at some point.
Automatic rebuilds on new Go patches
Since every Go patch only takes effect through a rebuild, a new patch should trigger a build automatically. Renovate or Dependabot can keep the tag of the golang build image and the base images of your runtime containers current, and a pinned toolchain in go.mod makes visible which version you build with. With minimal images without a shell, for example based on Wolfi or distroless, the CA package comes with the base image, so a new digest also brings new roots. The operating cost is low once the pipeline is in place, and it pays off again with every future fix in the standard library.
Egress along known routes
The attacker needs a position between client and target. A NetworkPolicy that only lets pods out to known destinations, or only through a controlled egress proxy, does not prevent that position, but it makes it rarer because fewer routes exist that could be redirected. Together with a trustworthy DNS resolver in the cluster this is a sensible addition, but it replaces none of the other measures. How to build egress rules for a TYPO3 platform is covered in the series TYPO3 on Kubernetes under IT-Grundschutz.
What does not help here
Updating the CA packages on the host changes nothing for containers, because every image brings its own package. Restarting without a new image just loads the same old roots again. CRLs and OCSP do not help, because Go does not check them, and even clients that do often fail because the revocation information cannot be reached. DNS CAA records do not protect you either. According to RFC 8659 the certificate authority checks them before issuance, clients are explicitly not allowed to use them during validation, and an attacker with their own key does not ask anyway.
Certificate Transparency does not help Go clients directly either, because crypto/x509 does not require SCTs. CT monitoring for your own domains still makes sense, but it only reports certificates that someone chose to submit to a log. Container hardening with seccomp, dropped capabilities, Pod Security or user namespaces is good practice but does not touch this mechanism, because the attacked process does nothing unusual.
Detection and verification
You can establish whether you are affected in two steps. First the binaries: with go version -m or govulncheck -mode binary you check which toolchain a program was built with, and anything before 1.26.10 or between 1.27.0 and 1.27.2 lacks the denylist. Then the roots: check in each image whether the CA bundle still contains emSign ECC Root CA - G3 or AAA Certificate Services. Searching for the word “emSign” is not enough, because emSign Root CA - G1 remains trusted, so compare names or fingerprints specifically, as published by the disclosure site in roots.json.
The test hosts listed on the disclosure site under we-have-a-problem.com allow a practical cross-check, for example dv-g3b-emudhra.we-have-a-problem.com for the chain via emSign and dv-g3b-aaa-emudhra.we-have-a-problem.com for the chain via AAA. What matters is which program you test with. A test with curl only tests curl; a request from the affected Go program, or a Go tool in the same image, is what tells you something. If the connection fails with a certificate error, that program is protected.
Whether someone has used the keys against you can only be shown if you log the certificate chains of your outgoing connections. Where a proxy or mesh does that, you can search for chains containing one of the eight SPKI hashes from the fix commit. No indicators of real attacks have been published. If you suspect something, treat all credentials that travelled over the affected connections as compromised, meaning API tokens, Git credentials and cloud credentials, and rotate them after the rebuild.
Recommendation for operators
Act now for Go programs that send credentials to public endpoints, such as Git access from Argo CD or Flux, cloud APIs from OpenTofu or external-dns, ACME requests from cert-manager and runners registering with your GitLab instance. Rebuild your own programs today with Go 1.27.3 or 1.26.10, refresh the CA packages in all images, restart the pods and check whether third-party tools already have releases with the new toolchain. Where that is still pending, use SSL_CERT_FILE to provide a maintained bundle without the affected roots.
Monitoring is enough if your Go programs talk exclusively to internal endpoints through your own CA. They do not trust the eMudhra roots anyway, and the rebuild can come with the next regular update.
This case shows particularly clearly why I want patches and updates automated. With Go it is not enough to update a package. Every program has to be rebuilt, and that only works reliably when Renovate or Dependabot propose the new golang image and new base images as merge requests on their own, and the pipeline builds, tests and deploys to staging. Then Go 1.27.3 is in all your images within hours. A cooldown for routine updates protects against compromised releases, and security updates like this one are exempt from it. And if a service does turn out to run through one of the blocked intermediates, the certificate error shows up in testing rather than in production. When builds happen by hand, the one tool that was copied into a base image two years ago tends to get left behind, and that is exactly the one that keeps talking with the old standard library for months.
Frequently asked questions about CVE-2026-111943
Is updating ca-certificates enough?+
It helps, but it does not close every path. The keys are also reachable through Sectigo cross-certificates under AAA Certificate Services, and whether your package removes that root depends on the distribution. Only the denylist in the rebuilt binary protects you completely.
Is restarting my pods enough?+
Only if the image received an updated CA package beforehand. The denylist itself lives in the binary and only arrives with a rebuild on Go 1.27.3 or 1.26.10.
Are my connections to the Kubernetes API server affected?+
Hardly. In-cluster clients based on client-go only trust the cluster CA from the service account mount. Go programs that validate public endpoints against the system roots are the ones affected.
I only use Go 1.25. What now?+
Go 1.25 has been unsupported since Go 1.27 was released and gets no fix. Move to Go 1.26.10 or 1.27.3, and keep the CA packages in your images current until then.
Conclusion
Go’s fix for the eMudhra incident is technically small, 42 lines in three files, yet big for operations, because it only takes effect through a rebuild. It targets the keys, and with them the one point that every path to the compromised intermediates has in common. The transferable lesson has two sides. Trusting the Web PKI is almost always more than a service needs for internal connections, and your own CA takes the sting out of incidents like this. And with a language that compiles its standard library into every program, an inventory of toolchain versions matters as much as an inventory of packages. With both in place, a case like this takes one build rather than a crisis meeting.
Sources
- Go Vulnerability Database: GO-2026-6809 (CVE-2026-111943)
- Go fix commit 7f86f95: crypto/x509: distrust a handful of intermediates
- Go issue #82345
- golang-announce: Go 1.27.3 and Go 1.26.10
- Go release history and release policy
- Go source: crypto/x509/root.go (SSL_CERT_FILE, loading roots)
- Go source: crypto/x509/root_linux.go (CA bundle paths)
- golang.org/x/crypto/x509roots/fallback
- govulncheck: binary mode
- oss-security: Go 1.27.3 and Go 1.26.10 are released for CA certificate removal
- Mozilla Bugzilla 2083449: eMudhra incident
- Scott Helme: Eight bits of trust
- Disclosure site with test hosts, roots.json and impacted-intermediates.json
- Google: Protecting Chrome and Android users from compromised eMudhra intermediate CA certificates
- Red Hat RHSB-2026-012
- Amazon Linux 2023 issue #1147
- cert-manager docs: trust-manager
- RFC 8659: DNS Certification Authority Authorization (CAA)
- FIRST EPSS: CVE-2026-111943
I find the Go binaries in your cluster, rebuild them with the current toolchain and set up your own trust anchors and trust bundles so the next CA incident does not touch your operations.
Inventory and rebuilds of Go tools and operators, trust-manager bundles and private CAs for internal connections, Renovate pipelines for build and base images.
Platform operations instead of advice on paper: I continuously check, patch and harden your infrastructure.
