Rows of server cabinets in a data center running containerized Java services

JDK Certificate Flaw: Patch Java Now

November 1, 2026 · 9 min read · By Priya Sharma

The version tag on your FROM line is a label. The cacerts file inside the image layer is the verdict. On 17 October 2026 the private keys behind eight eMudhra intermediate CA certificates were recoverable, and Chrome and Firefox had blocked the affected keys within hours. Mozilla removed emSign ECC Root CA – G3 from its store on 20 October; Chrome followed on 27 October. Many container images did not move at all. A scan of 30 popular Docker images on 24 October found 10 still shipping that root in at least one trust store.

Key Takeaways:

  • cacerts inside the image layer decides whether the forged chains validate; the version string does not.
  • Fixed artifacts add a fourth component, so 25.0.5 and 25.0.5.0.2 are different binaries with the same family name.
  • Deleting one root alias is not the fix: OpenJDK asks for four, and alias names vary by vendor.
  • A keytool delete in a running container does not reach the image the next pod starts from.
  • On Debian and Ubuntu, updating ca-certificates and ca-certificates-java still leaves a stale Java keystore.

What CVE-2026-91403 changed and which artifacts carry the fix

Rebuild without cache and verify the rebuilt image

The fixed artifacts do not carry the version you are grepping for. Safeguard’s vendor-build roundup records them as 27.0.1.0.2, 25.0.5.0.2, 21.0.13.0.2, 17.0.21.0.2, 11.0.33.0.2 and 8u511 build b32, all dated 21 October 2026. Oracle names 25.0.5 as vulnerable and 25.0.5.0.2 as fixed. Both strings contain 25.0.5. A tag scan returns the patched build and the exposed one with identical confidence, which is why the check has to happen on the file.

The version lists published by different projects disagree at the edges too. OpenJDK’s advisory names 27.0.1, 25.0.5, 21.0.13, 17.0.21, 11.0.32.1 and 8u504, while the Scala advisory names 11.0.33 and 8u512. Neither list describes the image you actually run.

The remediation is a deletion. OpenJDK’s advisory page asks for four root aliases to go: emsignrootcag1 [jdk], emsignrootcag2 [jdk], emsigneccrootcag3 [jdk] and comodoaaaca [jdk]. Removing one is not enough, and removing the wrong one has a cost. Guillaume Laforge’s technical write-up notes that deleting only emSign ECC Root CA – G3 leaves the JVM exposed to six *-aaa-emudhra TLS chains and two cross-signed code-signing chains, while deleting AAA Certificate Services distrusts the entire Sectigo hierarchy and can break legacy endpoints that never touched eMudhra. The same write-up points at blocked.certs, the JDK’s hash-based blocklist, as the mechanism for blocking all 16 compromised certificates without removing a root at all.

Which root carries the risk depends on the chain in front of you. Reporting on the incident places the six TLS certificates under emSign Root TLS CA – G3, chaining to emSign ECC Root CA – G3, a root carried by the Chrome and Apple stores. eMudhra generated the eight intermediate CA certificates at a key ceremony in April 2025, so the window opened then, not on the disclosure date.

Two cacerts paths, and the images that hide them

OpenJDK’s instructions split by major version, and the split breaks naive scripts. JDK 11 and later read $JAVA_HOME/lib/security/cacerts. JDK 8 reads $JAVA_HOME/jre/lib/security/cacerts, and its keytool has no -cacerts option at all, so one command line cannot serve both.

, architecture diagram

Vendor images move the path again. Official Temurin images install the runtime under /opt/java/openjdk, which is why the extraction recipe below uses that prefix. Distroless Java images read /etc/ssl/certs/java/cacerts, the store a Debian build stage generates, so a script that only inspects the JDK directory reports a clean bill of health on an image that is still exposed.

Two overrides decide whether the bundled file is even consulted. If $JAVA_HOME/lib/security/jssecacerts exists, the JVM prefers it over cacerts, and an application that sets javax.net.ssl.trustStore ignores the bundled file entirely. Check the launch configuration before you trust a clean grep.

Alias names are not portable either. LOGIQ’s write-up records Liberica shipping emsigneccrootcag3 [jdk] while Temurin ships cn_emsign_ecc_root_ca__g3 for the same certificate. A cleanup script that hardcodes one vendor’s aliases can exit zero having deleted nothing, which is the worst of both outcomes: a green build and an exposed runtime.

Store location Who reads it Fix Source
$JAVA_HOME/lib/security/cacerts Standalone JDK 11+ and vendor JDK images keytool -delete -cacerts, or a vendor build carrying the fix OpenJDK
$JAVA_HOME/jre/lib/security/cacerts JDK 8 images keytool -delete with the explicit -keystore path LOGIQ
/opt/java/openjdk/lib/security/cacerts Official Temurin images Extract, inspect, then delete in the Dockerfile Safeguard
/etc/ssl/certs/java/cacerts Debian and Ubuntu packages, distroless stages Regenerate with update-ca-certificates –fresh OpenJDK
OS CA bundle Arch, Fedora, Gentoo, Oracle Linux, RHEL packages Update the OS CA certificate package OpenJDK

Finding the aliases inside an image

Start with the image you ship, not the host. Create a stopped container from it, copy the store out, then remove the container. The sequence is docker create for an id without a running process, docker cp -L to resolve symlinks, then keytool -list -keystore against the copied file with the store password. Pulling the file onto your workstation also means you can archive it next to the ticket as evidence.

Pipe the listing through a case-insensitive match on emsign|comodoaaa. OpenJDK’s page gives the alias forms to expect, and the helper snippets published after the incident match on aaa_certificate_services as well, because some builds carry the subject name rather than the vendor alias. Match on both the alias and the certificate subject string before you conclude anything.

A hit means the image accepts every certificate those intermediates can mint. No hit means something narrower: that this particular store, in this particular image, with no jssecacerts override and no application-supplied trust store, does not carry the roots. Say that in the ticket instead of writing “not vulnerable”.

Deleting the roots in the build, not the container

The edit belongs in the build. A docker exec deletion writes to that container’s writable layer, so the container that replaces it starts from the untouched image, and the next deploy silently undoes the fix. Put the deletion in a layer and every container built after it inherits the change.

Mechanically it is four keytool -delete -cacerts -storepass changeit calls, one per alias, chained with && so the build fails if any alias is missing. Leave the ticket reference in a comment above the RUN line, and delete the whole block once your base image ships a fixed build. A permanent workaround with no owner becomes a mystery layer, and the next person to bump the base image will not know whether the deletions still matter.

Three details trip up the first attempt. The default store password is changeit, so a store built with a different password fails loudly rather than silently, which is the behaviour you want. Missing aliases return an error that images treat as fatal, a reasonable build gate but a poor outcome when the alias is simply spelled differently on this vendor’s JDK. And on JDK 8, replace -cacerts with -keystore "$JAVA_HOME/jre/lib/security/cacerts". Distroless and non-root images have no shell and no write access to the runtime stage, so run the deletions against a copy in a JDK build stage and copy the result over /etc/ssl/certs/java/cacerts.

Rebuild without cache and verify the rebuilt image

A new RUN line invalidates that layer, and that is all it does. If the JDK arrives through a cached parent stage or a tarball fetched from an unchanged URL, the vulnerable store survives the rebuild. Build with --no-cache, or add a build argument that changes when the certificate situation changes, then check the digest of the base image that actually resolved.

Verify on the rebuilt image, not on the Dockerfile. Re-run the extraction and the grep; an empty result is the only outcome that closes the loop. Pulling a newer base tag is not verification, because the Java store can stay stale even when the OS bundle has moved on.

Restart the workloads next. A JVM that started before the rebuild keeps the old keystore in memory, so a rolling restart is part of the fix rather than cleanup afterwards. Expect to repeat the check after later JDK updates as well, since reinstalling or upgrading a runtime can restore its original cacerts file. GraalVM native images need their own pass: the trust configuration is embedded at build time, so patch the host JDK first and then rebuild the binary.

ca-certificates-java and the Debian rebuild trap

Debian and Ubuntu are where the obvious fix fails. Their OpenJDK packages are meant to read the system bundle through the ca-certificates-java package, and a long-standing bug means upgrading ca-certificates and ca-certificates-java does not refresh the Java store. OpenJDK’s page links Debian bug 1069251 for the bug itself and Debian bug 1142266 for the workaround.

Other distributions work differently. Arch, Fedora, Gentoo, Oracle Linux and RHEL packages read the OS bundle, so updating the certificate package is the fix there. FreeBSD and Homebrew JDKs do not read the system store at all, which means the JDK package itself has to be updated.

Checklist and what to watch

  • Extract cacerts from every base image you ship, grep for emsign and comodoaaa, and attach the output to the ticket.
  • Confirm each image runs a build string with the fix, or carries an owned Dockerfile deletion with a ticket reference and an expiry.
  • Rebuild with –no-cache, then verify the rebuilt image rather than the Dockerfile.
  • Roll the workloads and restart build agents so no process keeps an old store in memory.
  • On Debian and Ubuntu, regenerate /etc/ssl/certs/java/cacerts instead of trusting a package upgrade.
  • Re-run dependency resolution and inspect TLS inspection logs for chains issued by the eight intermediates.

Track the vendor release notes for the next respin rather than the version number, the OpenJDK advisory page and its timestamp, and any image tag you pin by digest. The browser side finished in ten days: Mozilla dropped the root on 20 October, Chrome on 27 October. Ten of 30 popular images were still carrying it on 24 October. That gap is the whole risk, and it closes only when someone rebuilds.

Sources and References

Sources cited while researching and writing this article:

Priya Sharma

Thinks deeply about AI ethics, which some might call ironic. Has benchmarked every model, read every white-paper, and formed opinions about all of them in the time it took you to read this sentence. Passionate about responsible AI, and quietly aware that "responsible" is doing a lot of heavy lifting.