Safeguard
Supply Chain Security•

How Java TLS Trust Works: JSSE, TrustManager, cacerts, Truststores

Java trusts a server when its TrustManager builds a PKIX path to a root in the JVM truststore, usually cacerts. How it works and how to change it safely.

Safeguard Security Team
48 min read

Java decides whether to trust a TLS server inside JSSE, its TLS implementation: the X509TrustManager in the SSLContext builds a certificate path from the server's certificate to a root in a truststore, then runs the JDK's own checks. Unless you configure something else, that truststore is the cacerts file inside the JDK, not your operating system's store.

Everything else follows from that one sentence. A browser fix does not reach a JVM, two JVMs with the same version number can trust different roots, and a typo in a system property can silently put you back on the defaults. This guide goes one level below our two eMudhra incident posts, how to check and fix Java cacerts after the eMudhra compromise and the JDK vendor builds and workarounds that shipped in October 2026. Those posts told you what to do that week. This one explains the machinery, so the next CA distrust takes an hour instead of a week. For the general idea of a truststore, start with what a trust store is.

We checked the behaviour described here against the Oracle JSSE Reference Guide, the keytool and jlink manuals, the OpenJDK source on GitHub as of 23 October 2026, and a local Eclipse Temurin 21.0.12 build on macOS. Where the documentation and the running JDK disagree, we say so.

Key takeaways

  • JSSE is the pipeline. An SSLContext holds a KeyManager (your own certificate, for client authentication) and a TrustManager (who you trust). The default TrustManager algorithm is PKIX, set by ssl.TrustManagerFactory.algorithm in java.security.
  • The default truststore lookup is short. The JDK uses the file named by javax.net.ssl.trustStore, else JAVA_HOME/lib/security/jssecacerts, else JAVA_HOME/lib/security/cacerts. In our test on Temurin 21.0.12, a trustStore path that did not exist fell back to cacerts with only a debug line, even though the guide describes an empty truststore for that case.
  • cacerts has changed format twice. JEP 229 made PKCS12 the default keystore type for new keystores in JDK 9. The cacerts file itself became a password-less PKCS12 file in JDK 18 (JDK-8275252). The documented initial password is still changeit.
  • Revocation is off by default. With the usual TrustManagerFactory.init(KeyStore) path, revocation checking is disabled unless you set com.sun.net.ssl.checkRevocation=true. That is why a CA revoking an intermediate changes nothing for a default JVM.
  • The JDK has three ways to stop trusting a CA. Remove the root from cacerts, add certificates to lib/security/blocked.certs, or add a policy to jdk.security.caDistrustPolicies. The last one can be date based: it compares the server certificate's notBefore date with a cutoff.
  • Vendors ship different root lists. OpenJDK's source tree held 111 root certificates on 23 October 2026. Eclipse Temurin builds its cacerts from Mozilla's list instead; our Temurin 21.0.12 cacerts held 118 entries.
  • jlink and container images freeze trust at build time. cacerts lives in java.base, so even the smallest jlink image carries a copy. Updating trust means rebuilding the image with a patched JDK.
  • Frameworks mostly sit on top of the same SSLContext. The JDK HttpClient, Netty and Apache HttpClient fall back to JDK defaults unless you hand them trust material; Spring Boot's SSL bundles are the clean way to do that.

What are JSSE, the default SSLContext, KeyManager and TrustManager?

JSSE, the Java Secure Socket Extension, is the part of the JDK that implements TLS and DTLS. The Oracle JSSE Reference Guide describes it as a set of engine classes you obtain from a provider (normally SunJSSE) rather than construct directly. Four of them matter for trust.

ClassWhat it doesWhere its configuration comes from by default
SSLContextThe factory for TLS sockets and engines. It holds one set of key managers, one set of trust managers and a source of randomness.SSLContext.getDefault(), created on first use from the system properties below
KeyManager / X509KeyManagerChooses which of your certificates and private keys to present. Servers need one; clients only need one for mutual TLS.javax.net.ssl.keyStore, otherwise an empty keystore
TrustManager / X509TrustManagerDecides whether the peer's certificate chain is trustworthy.javax.net.ssl.trustStore, then jssecacerts, then cacerts
TrustManagerFactoryBuilds trust managers from a KeyStore or from PKIX parameters.Algorithm from ssl.TrustManagerFactory.algorithm, which is PKIX in the shipped java.security

What happens when Java code opens an HTTPS connection?

When code calls HttpsURLConnection, the JDK HttpClient, or SSLSocketFactory.getDefault(), the JDK uses the default SSLContext. The SSLContext javadoc says the first call to getDefault() triggers SSLContext.getInstance("Default"), and that object becomes the default context unless someone has already called SSLContext.setDefault(). The JDK HttpClient.Builder documentation says the same thing from the client side: if you do not call sslContext(...), new clients use the default context.

The default context is initialised with a default KeyManager and a default TrustManager. The JSSE guide spells out where they come from: if javax.net.ssl.trustStore is set, the default TrustManager manages that file; if not, the JDK looks for jssecacerts and then cacerts under JAVA_HOME/lib/security.

What does the default PKIX TrustManager actually check?

The guide recommends X509ExtendedTrustManager over the plain interface because it adds connection-aware checks, algorithm constraints and host name verification at the TLS layer. For a server certificate, the SunJSSE PKIX trust manager does several things in order:

  1. Builds a path from the server's leaf certificate, through any intermediates the server sent, to a certificate in the truststore. If it cannot, you get the familiar "PKIX path building failed ... unable to find valid certification path to requested target".
  2. Validates the path with the JDK's CertPath PKIX implementation: signatures, validity dates, basic constraints, key usage and name constraints.
  3. Applies algorithm constraints from jdk.certpath.disabledAlgorithms and jdk.tls.disabledAlgorithms. The shipped java.security on the OpenJDK master branch, for example, disables SHA1 jdkCA & usage TLSServer, which means SHA-1 signed TLS server certificates that chain to a root shipped in cacerts are rejected.
  4. Checks the block list in lib/security/blocked.certs (named blacklisted.certs on JDK 8), which lists certificates by fingerprint that must never be accepted.
  5. Applies CA distrust policies listed in jdk.security.caDistrustPolicies, which are covered in their own section below.
  6. Checks revocation only if asked. The JSSE guide states that when the factory is initialised with TrustManagerFactory.init(KeyStore), default PKIX parameters are used "with the exception that revocation checking is disabled". You enable it with com.sun.net.ssl.checkRevocation=true, and CRL distribution point fetching needs com.sun.security.enableCRLDP=true as well. If you initialise the factory with CertPathTrustManagerParameters instead, revocation checking is on by default.

Host name verification sits next to this. HttpsURLConnection and the JDK HttpClient verify that the certificate matches the host name; a raw SSLSocket only does so if you set an endpoint identification algorithm on its SSLParameters. Trust in the CA and a correct host name are two separate checks, and both must pass.

What does explicit trust configuration look like in code?

When you want a specific truststore for one client and not for the whole JVM, you build your own SSLContext. This is the pattern the JSSE guide uses, trimmed to the trust side:

import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

KeyStore ts = KeyStore.getInstance("PKCS12");
try (InputStream in = Files.newInputStream(Path.of("/etc/myapp/truststore.p12"))) {
    ts.load(in, System.getenv("TRUSTSTORE_PASSWORD").toCharArray());
}

TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
tmf.init(ts);                       // revocation off, as with the default

SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);   // no client certificate

var client = java.net.http.HttpClient.newBuilder()
        .sslContext(ctx)
        .build();

Two details matter. First, passing null as the KeyStore to tmf.init does not mean "trust nothing": the guide says a null keystore makes the SunJSSE factory run the same javax.net.ssl.trustStore, jssecacerts, cacerts lookup as the default context. Second, this context only affects the client you hand it to. Everything else in the JVM still uses the default.

Where is cacerts in each JDK layout?

The keytool manual gives the canonical location: JAVA_HOME/lib/security/cacerts on Linux and macOS, and JAVA_HOME\lib\security\cacerts on Windows. The details vary by how the JDK was installed.

Install typePath to the file the JVM readsWho updates it
JDK 9 and later, any archive or installerJAVA_HOME/lib/security/cacertsThe JDK vendor, with each JDK update
JDK 8 JDK (with embedded JRE)JAVA_HOME/jre/lib/security/cacertsThe JDK vendor
JDK 8 standalone JREJRE_HOME/lib/security/cacertsThe JDK vendor
macOS JDK bundle/Library/Java/JavaVirtualMachines/NAME.jdk/Contents/Home/lib/security/cacertsThe JDK vendor
Debian or Ubuntu packaged OpenJDKUsually a symbolic link to /etc/ssl/certs/java/cacerts; confirm with readlink -fThe ca-certificates-java package, through ca-certificates hooks
RHEL, Fedora and related packaged OpenJDK/etc/pki/java/cacerts, a symbolic link to /etc/pki/ca-trust/extracted/java/cacertsupdate-ca-trust extract
jlink runtime imageIMAGE/lib/security/cacerts, copied from the JDK you linked withNobody, until you relink
Container imageWhatever the base image's JDK or distribution providesWhoever rebuilds the image

On our macOS test machine, the Temurin 21 lib/security directory held four files: blocked.certs, cacerts, default.policy and public_suffix_list.dat. The java.security configuration file itself lives one level up, in JAVA_HOME/conf/security/java.security on JDK 9 and later (it was jre/lib/security/java.security on JDK 8).

To find the file a given JVM really uses, ask the JVM. The javax.net.debug output names it:

java -Djavax.net.debug=ssl:trustmanager -jar app.jar 2>&1 | grep -i "trustStore is"
# javax.net.ssl|DEBUG|...|TrustStoreManager.java:113|trustStore is: /Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home/lib/security/cacerts

That line comes from our own run against Temurin 21.0.12. The exact format of debug output is not a stable interface, so do not parse it in production scripts, but it is the fastest way to end an argument about which file is in play.

What format is cacerts, and what is the default password?

There are two separate changes here, and they are often merged into one.

JDK 9 changed the default keystore type. JEP 229, "Create PKCS12 Keystores by Default", delivered in JDK 9, moved the default type for new keystores from JKS to PKCS12. The shipped java.security now contains keystore.type=pkcs12 and keystore.type.compat=true, the second of which keeps old JKS files loadable through the same API.

JDK 18 changed the cacerts file itself. The Oracle JDK 18 release notes record JDK-8275252: "The cacerts keystore file is now a password-less PKCS #12 file." The certificates inside are not encrypted and there is no MAC for password integrity, so existing code that loads cacerts as JKS with any password, including null, still works. Before JDK 18, cacerts stayed a JKS file even though JDK 9 had changed the default type.

The password. The keytool manual says: "The initial password of the cacerts keystore file is changeit", and advises administrators to change it and tighten file permissions after installing. On JDK 18 and later, reading the file does not need the password at all; on Temurin 21.0.12, keytool -list -cacerts printed all 118 entries without prompting. Writing to it still requires the store password, which is changeit unless someone changed it. Distribution-managed files can differ: Debian's ca-certificates-java defaults file has its own storepass setting.

# Read the type and entry count; no password needed on JDK 18 and later
keytool -list -cacerts | head -4
# Keystore type: PKCS12
# Keystore provider: SUN
#
# Your keystore contains 118 entries

Do all JDK vendors ship the same roots?

No, and this surprises people more than anything else in this post.

The OpenJDK source keeps its roots under src/java.base/share/data/cacerts, one PEM file per root plus a README. The README says each change must be approved by the OpenJDK Security group and that VerifyCACerts.java, a test that pins the expected list, must be updated alongside. On 23 October 2026 that directory held 111 certificates on the master branch. Oracle JDK builds draw their roots from the Oracle Java Root Certificate program, which the keytool manual names as the source of the shipped set.

Eclipse Temurin chose differently. The Adoptium temurin-build repository explains that OpenJDK's cacerts "lacks a number of CA certificates that are present in common browsers", that a 2020 conversation with OpenJDK reached no consensus, and that the project therefore builds its cacerts from Mozilla's certdata.txt during every JDK build, unless the builder passes --custom-cacerts=false. Our Temurin 21.0.12 cacerts had 118 entries, with aliases derived from each certificate's subject name, where OpenJDK uses short aliases such as letsencryptisrgx1.

So "we run 21.0.12" does not tell you which roots a service trusts. The vendor, the build and the packaging all matter. Our Java LTS comparison covers which versions are supported; the root list is a separate question you have to answer per vendor.

How do javax.net.ssl.trustStore system properties override cacerts?

Four system properties control the default truststore. The JSSE guide lists them with their defaults:

PropertyWhat it setsDefault
javax.net.ssl.trustStorePath to the truststore file, or NONE for a non-file store such as a hardware token or an OS storejssecacerts if it exists, otherwise cacerts
javax.net.ssl.trustStorePasswordPassword used to check the store's integrityNone (treated as an empty string)
javax.net.ssl.trustStoreTypeKeystore type, for example PKCS12, JKS, Windows-ROOT, KeychainStore-ROOTKeyStore.getDefaultType(), which is pkcs12
javax.net.ssl.trustStoreProviderSecurity provider to load the store withNone

What is the precedence order, exactly?

From the guide and the OpenJDK TrustStoreManager source, the default TrustManager resolves its store like this:

  1. Code wins over everything. If any code has called SSLContext.setDefault(...), that context is used. If a library builds its own SSLContext with its own trust managers, that library ignores the system properties entirely.
  2. javax.net.ssl.trustStore, if set. Set on the command line with -D, through JAVA_TOOL_OPTIONS, or with System.setProperty before anything touches TLS.
  3. JAVA_HOME/lib/security/jssecacerts, if it exists. The guide describes it as a way to give JSSE "a JSSE-specific set of trusted root certificates separate from ones that might be present in cacerts for code-signing purposes". Its existence alone changes behaviour, which makes it a common hiding place for old changes.
  4. JAVA_HOME/lib/security/cacerts.
  5. An empty truststore, if none of the above can be read. Every handshake then fails path building.

What happens if the trustStore path is wrong?

The JSSE guide says that if javax.net.ssl.trustStore names a file that does not exist, "a default TrustManager using an empty keystore is created". That is not what we observed. The current TrustStoreManager source builds a candidate list of the property value followed by the default cacerts path and uses the first one it can open. On Temurin 21.0.12 we ran a small HTTPS client with -Djavax.net.ssl.trustStore=/nonexistent. The handshake succeeded, and the debug log showed why:

TrustStoreManager.java:162|Inaccessible trust store: /nonexistent
TrustStoreManager.java:113|trustStore is: .../temurin-21.jdk/Contents/Home/lib/security/cacerts

The practical consequence is serious. A typo in a hardened deployment that is meant to trust only your private CA quietly gives the service the full public root list instead, and nothing fails. Check the path exists before the JVM starts, and check the trustStore is: line in a test run.

When must the properties be set?

Before the default context is created. SSLContext.getDefault() creates it once, on first call. The safest places are the command line, JAVA_TOOL_OPTIONS in a container, or your application server's JVM options file. Setting the property from inside the application after a library has already opened a TLS connection may have no effect on that library.

A short reference for the common cases:

# One service, one custom truststore (password from a protected options file is better than argv)
java -Djavax.net.ssl.trustStore=/etc/myapp/truststore.p12 \
     -Djavax.net.ssl.trustStoreType=PKCS12 \
     -Djavax.net.ssl.trustStorePassword=changeit \
     -jar app.jar

# Same, without touching the start script (containers, CI)
export JAVA_TOOL_OPTIONS="-Djavax.net.ssl.trustStore=/etc/myapp/truststore.p12 -Djavax.net.ssl.trustStoreType=PKCS12"

# Show the security properties the JVM actually loaded
java -XshowSettings:security:properties -version 2>&1 | grep -E "keystore.type|caDistrust|TrustManagerFactory"

The JSSE guide warns against putting the password on the command line where other users can read it. For a truststore that only contains public certificates the password protects integrity, not secrecy, but process listings end up in logs and tickets, so keep the habit.

Can java.security itself be overridden?

Yes, which matters for distrust policies. The shipped java.security says an alternate properties file can be supplied with -Djava.security.properties=URL (one equals sign appends and overrides, two equals signs replace the whole master file), provided security.overridePropertiesFile=true, which is the shipped value. That is how operators change jdk.security.caDistrustPolicies or jdk.certpath.disabledAlgorithms without editing the JDK. It is also how someone can switch your distrust policies off, so treat a java.security.properties flag in a start script as something to review.

How does a JDK vendor update its root list, and how do distrust dates work?

A JDK has three separate levers for changing trust, and vendors use all three.

LeverWhere it livesWhat it doesTypical use
Add or remove a rootlib/security/cacertsChanges the set of trust anchorsA new CA is accepted into the root program; an expired or withdrawn root is removed
Block a certificatelib/security/blocked.certsRejects specific certificates by fingerprint wherever they appear in a chainA compromised intermediate or key, such as the eMudhra intermediates in October 2026
Distrust policyjdk.security.caDistrustPolicies in java.security, code in sun.security.validatorRejects TLS server certificates anchored by named roots, optionally only those issued after a dateA CA loses public trust but its roots must stay for code signing, timestamps or older certificates

How does a root change reach your JVM?

For OpenJDK, a root change is a source change: a PEM file is added to or deleted from src/java.base/share/data/cacerts, the pinned list in VerifyCACerts.java is updated, and the Security group approves it. The build turns the directory into the binary cacerts file. Backports then flow into the update repositories (jdk21u, jdk17u and so on) and from there into each vendor's quarterly release. Temurin's own list follows Mozilla's certdata.txt instead. Distribution-packaged JDKs on Debian, Ubuntu and RHEL take their Java store from the distribution's CA package, so they change when that package changes.

None of this reaches a running JVM. The truststore is read when the default context is built, so a JDK update only takes effect after the process restarts on the new JDK, and a container only gets it when the image is rebuilt.

How does jdk.security.caDistrustPolicies work?

The policy is a comma-separated list in java.security. On the OpenJDK master branch on 23 October 2026 it read:

jdk.security.caDistrustPolicies=SYMANTEC_TLS,ENTRUST_TLS,CAMERFIRMA_TLS,\
    CHUNGHWA_TLS

Our Temurin 21.0.12 build printed the same four values with -XshowSettings:security:properties. Each value maps to a small class in sun.security.validator that holds the SHA-256 fingerprints of the affected roots. When a TLS server certificate's path is anchored by one of those roots, the class decides whether to reject it. The comment in java.security explains the scope and the limits: unknown values are ignored, an empty property enforces nothing, and the property "does not override other security properties which can restrict certificates", so jdk.certpath.disabledAlgorithms still applies when a policy is off.

Distrust policies only cover TLS server certificates. That is the point of them. The roots stay in cacerts, so code that was signed or timestamped under those roots keeps verifying, while new TLS server certificates are refused.

How do the dates work?

A date-based policy compares the server certificate's notBefore date, converted to a UTC calendar date, with a cutoff. Certificates issued on or before the cutoff keep working until they expire; certificates issued after it fail. This avoids breaking every site at once on the day of the announcement, while stopping the CA from issuing new trusted certificates. The Chunghwa policy in the current source adds a second date after which every certificate under those roots is rejected, regardless of issue date.

The history, taken from the OpenJDK commit log and the source of each policy class:

Date (commit)OpenJDK changeWhat it does
Before 2024SYMANTEC_TLS policyDistrusted TLS server certificates anchored by the legacy roots that policy names and issued after 16 April 2019, with two named subordinate CAs allowed until 31 December 2019 (as described in java.security before October 2026)
3 Sep 2024JDK-8337664Adds ENTRUST_TLS: distrust TLS server certificates anchored by Entrust roots and issued after October 2024
27 Sep 2024JDK-8341059Moves the Entrust cutoff to 12 November 2024; the code rejects certificates whose notBefore date is after 11 November 2024
24 Jan 2025JDK-8346587Adds CAMERFIRMA_TLS: certificates anchored by the Camerfirma root and issued after 15 April 2025
30 Apr 2025JDK-8350498Removes two Camerfirma root certificates from cacerts
30 Jul 2025JDK-8361212Removes the AffirmTrust roots from cacerts
8 Jan 2026JDK-8369282Adds CHUNGHWA_TLS: certificates anchored by the Chunghwa ePKI root and issued after 17 March 2026
30 Sep 2026JDK-8386371Removes an Entrust G4 root from cacerts
15 Oct 2026JDK-8382178, "Enhance TLS certificate handling"Drops the issue-date check for the SYMANTEC_TLS, ENTRUST_TLS and CAMERFIRMA_TLS policies, so any TLS server certificate anchored by those roots is now rejected; adds the 19 April 2027 hard cutoff for Chunghwa
20 to 21 Oct 2026JDK-8395142, "CA Enhancements"Adds the eight compromised eMudhra intermediates and their eight cross-signs to blocked.certs, covered in our vendor builds post

The dates in the first column are commit dates in the OpenJDK mainline repository, not release dates. A change reaches your JDK only when your vendor ships a build that contains it, and backports to older update lines can arrive in a later quarter or not at all. We confirmed the November 2024 Entrust cutoff in the jdk21u source at tag jdk-21.0.5+11 and the April 2025 Camerfirma cutoff at jdk-21.0.7+6. To see what your own JDK enforces, read the comment above jdk.security.caDistrustPolicies in its conf/security/java.security; the comment changes with the code.

Why did the eMudhra fix use blocked.certs instead?

Because the problem was eight specific intermediates whose private keys were exposed, not a CA's general conduct. A distrust policy acts on everything under a root, and removing the root would only cut one of the two paths those intermediates had. The block list rejects the exact certificates wherever they appear. Our eMudhra cacerts post explains the two trust paths, and the CA key compromise explainer covers the incident itself. For the browser side of the same mechanics, see what happens if a browser distrusts your certificate authority.

The CA/Browser Forum's Baseline Requirements set the rules that publicly trusted TLS CAs must follow. Root programs, including the browser, operating system and JDK programs, decide what happens when a CA does not follow them. The JDK lags browsers by design, because its changes ship in JDK releases rather than over-the-air lists.

How do keytool commands list, import and delete roots, safely?

The rule that prevents most outages: do not edit the JDK's own cacerts in place. It is replaced on every JDK update, so your change disappears, and while it exists it affects every application on that JDK. Copy it into an application truststore, change the copy, and point the application at the copy. Edit the shared file only when that is genuinely the intent, and then through your configuration management so the change is reapplied after every update.

Listing and inspecting

# Every entry, short form (JDK 9 and later; -cacerts finds the file for you)
keytool -list -cacerts

# One entry in full: owner, issuer, validity and SHA-256 fingerprint
keytool -list -cacerts -alias letsencryptisrgx1 -v

# PEM output for one entry, useful for diffing or for openssl
keytool -list -cacerts -alias letsencryptisrgx1 -rfc

# A file you have not imported yet: print it before you trust it
keytool -printcert -file corp-root-2026.pem
openssl x509 -in corp-root-2026.pem -noout -subject -issuer -dates -fingerprint -sha256

The keytool manual says -cacerts is equivalent to -keystore with the path and type of the JDK's cacerts, and that combining -cacerts with -keystore or -storetype is an error. Aliases differ by vendor and distribution, so search by subject or fingerprint rather than assuming an alias:

keytool -list -cacerts -v | grep -E "^Alias name:|^Owner:|SHA256:" | paste - - -

Building an application truststore

# 1. Copy the JDK's roots as a starting point (or start empty for a private-CA-only service)
cp "$JAVA_HOME/lib/security/cacerts" ./app-truststore.p12

# 2. Check the fingerprint of the certificate you were given against one you got out of band
openssl x509 -in corp-root-2026.pem -noout -fingerprint -sha256

# 3. Import it as a trusted certificate under a clear alias
keytool -importcert -noprompt \
  -keystore ./app-truststore.p12 -storetype PKCS12 \
  -storepass:env TRUSTSTORE_PASS \
  -alias corp-root-2026 -file corp-root-2026.pem

# 4. Confirm it is there and that the count went up by one
keytool -list -keystore ./app-truststore.p12 -storepass:env TRUSTSTORE_PASS | head -5
keytool -list -keystore ./app-truststore.p12 -storepass:env TRUSTSTORE_PASS -alias corp-root-2026 -v | grep SHA256

Two notes on step 3. -noprompt skips the "Trust this certificate?" question, which is right in a script only because step 2 already checked the fingerprint. And -storepass:env reads the password from an environment variable, which the keytool manual supports alongside :file, so the password does not appear in shell history or process listings. On Temurin 21.0.12, importing into a copy of the password-less cacerts with -storepass changeit worked, and the copy could still be listed without a password afterwards, so do not rely on the password to protect a truststore's integrity; rely on file ownership and permissions.

Do not add -trustcacerts when importing a root into a truststore. That option tells keytool to consider the cacerts roots when building a chain for a certificate reply, which matters for -importcert of a signed certificate into a keystore with a private key, not for adding a trust anchor.

Deleting a root

Deleting a trust anchor is destructive for every service that reads the file: any server whose chain ends there will fail to connect. Back up and check the path first.

Warning: the next block removes a trust anchor. Run it against a copy, confirm the file path and alias, and keep the backup until your tests pass.

# Check which file you are about to change, and that it is the copy you meant
TS=./app-truststore.p12
readlink -f "$TS"; ls -l "$TS"

# Back it up
cp -p "$TS" "$TS.bak.$(date +%Y%m%d%H%M%S)"

# Confirm the alias points at the certificate you intend to remove
keytool -list -keystore "$TS" -storepass:env TRUSTSTORE_PASS -alias old-root -v | grep -E "Owner|SHA256"

# Remove it
keytool -delete -keystore "$TS" -storepass:env TRUSTSTORE_PASS -alias old-root

To delete from the JDK's own file, the same command takes -cacerts in place of -keystore. The JSSE guide recommends exactly that route for removing an untrusted CA from cacerts, and says to contact your system administrator if you lack permission to edit it. On a Debian or RHEL packaged JDK the file is generated by the distribution, so a manual delete is undone on the next CA package update; use the distribution's mechanism, described below.

How do OS trust stores interact with Java?

By default, they do not. A JDK installed from a vendor archive or installer reads its own cacerts on Windows, macOS and Linux, and ignores the certificates your operating system trusts. Corporate roots pushed by group policy or a device management profile are invisible to Java until you do something about it. There are two ways to connect them.

Windows: the SunMSCAPI keystores

The Oracle providers documentation lists four keystore types in the SunMSCAPI provider. Windows-ROOT (also known as Windows-ROOT-CURRENTUSER) reads the current user's Trusted Root store, and Windows-ROOT-LOCALMACHINE reads the machine-wide one; Windows-MY and Windows-MY-LOCALMACHINE hold personal certificates with private keys. Because these are not files, the trustStore property is set to NONE:

java -Djavax.net.ssl.trustStoreType=Windows-ROOT -Djavax.net.ssl.trustStore=NONE -jar app.jar

macOS: the Apple provider keystores

The Apple provider offers KeychainStore, which the JDK 25 documentation describes as private keys and certificates "for the user's current keychain", and KeychainStore-ROOT, which contains "certificates from the system root certificates keychain". KeychainStore-ROOT is documented for JDK 25 but does not appear in the JDK 21 providers guide, so check your version. We tried KeychainStore with NONE on Temurin 21.0.12 and a public HTTPS site failed path building, which is consistent with the documentation: the user keychain is not the system root list.

Linux: distribution-managed Java stores

On Debian and Ubuntu, the ca-certificates-java package hooks into update-ca-certificates. Its README says it maintains the keystore /etc/ssl/certs/java/cacerts "used for many java runtimes", stores each certificate under an alias prefixed with debian:, imports every *.pem in /etc/ssl/certs on first install, does not automatically handle local certificates (though it does not overwrite them), and can be fully re-imported with update-ca-certificates -f.

# Debian and Ubuntu: add a corporate root to the system and Java stores
sudo cp corp-root-2026.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
keytool -list -keystore /etc/ssl/certs/java/cacerts | grep -i -c corp

On RHEL, Fedora and related distributions, the update-ca-trust manual describes a consolidated system. You place sources under /etc/pki/ca-trust/source/anchors/ and run update-ca-trust extract, which writes /etc/pki/ca-trust/extracted/java/cacerts; the classic /etc/pki/java/cacerts path is a symbolic link to it. The manual adds an important limit: "Distrust information cannot be represented in this file format, and distrusted certificates are missing from these files". A certificate the system distrusts is simply absent from the Java file, which is fine for roots and does nothing for a JDK blocked.certs style block.

# RHEL and Fedora
sudo cp corp-root-2026.pem /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extract
readlink -f /etc/pki/java/cacerts

The catch on Linux is mixing install types. A distribution-packaged OpenJDK reads the distribution store; a vendor tarball unpacked into /opt on the same machine reads its own cacerts and ignores all of the above. Our safe JDK install guide covers choosing between them.

PlatformDefault for a vendor JDKHow to use the OS storeNotes
WindowsJDK cacertstrustStoreType=Windows-ROOT or Windows-ROOT-LOCALMACHINE, trustStore=NONEGroup policy roots become visible to Java only this way
macOSJDK cacertstrustStoreType=KeychainStore-ROOT on JDKs that document it, trustStore=NONEKeychainStore alone is the user keychain
Debian, UbuntuPackaged JDK: /etc/ssl/certs/java/cacertsupdate-ca-certificates, with ca-certificates-java installedVendor tarballs ignore it
RHEL, FedoraPackaged JDK: /etc/pki/java/cacertsupdate-ca-trust extractDistrusted certificates are omitted, not marked

How do containers and jlink images get their trust store?

jlink images carry a frozen copy

cacerts belongs to the java.base module, and jlink always includes java.base. We confirmed it directly: jlink --add-modules java.base --output jlimg on Temurin 21.0.12 produced an image whose lib/security held blocked.certs, cacerts, default.policy and public_suffix_list.dat, and keytool -list on that cacerts reported the same 118 entries as the source JDK.

That copy never changes. A jlink image built in July carries July's roots, July's blocked.certs and July's java.security distrust policies until you relink it with a newer JDK. A minimal runtime is a good idea for size and attack surface, but it turns every trust change into a rebuild.

# Build stage: link a small runtime from a patched JDK
FROM eclipse-temurin:21-jdk AS jre
RUN jlink --add-modules java.base,java.net.http,java.logging \
          --strip-debug --no-header-files --no-man-pages \
          --output /opt/jre
# Optional: add a corporate root to the linked runtime's truststore copy
COPY corp-root-2026.pem /tmp/
RUN /opt/jre/bin/keytool -importcert -noprompt -cacerts \
        -storepass changeit -alias corp-root-2026 -file /tmp/corp-root-2026.pem

FROM debian:stable-slim
COPY --from=jre /opt/jre /opt/jre
ENV PATH=/opt/jre/bin:$PATH

The module list in that example is illustrative; jdeps --print-module-deps on your application gives the real one. The point is the second RUN: trust changes belong in the build, so they are reviewed, repeatable and visible in the image history.

Official Temurin images: the USE_SYSTEM_CA_CERTS switch

The Docker official image documentation for eclipse-temurin describes an opt-in way to add certificates at run time on Linux images. You mount PEM files, ideally with a .crt extension, into /certificates and set USE_SYSTEM_CA_CERTS to any value. The entrypoint then adds them to both the JVM truststore and the system CA store. If you override the entrypoint, you must call /__cacert_entrypoint.sh yourself.

docker run -v "$(pwd)/certs:/certificates/" -e USE_SYSTEM_CA_CERTS=1 eclipse-temurin:25

The same documentation describes two restricted cases. When the container runs as a non-root user, the system store cannot be written, so the entrypoint creates a separate truststore, exports its path as JRE_CACERTS_PATH and extends JAVA_TOOL_OPTIONS to point the JVM at it. With a read-only filesystem, it also needs a writable volume at /tmp. Windows-based images do not support the feature.

Distribution base images

If your image installs Java from the distribution (apt-get install openjdk-21-jre-headless, dnf install java-21-openjdk-headless), the Linux section above applies inside the image: copy the root into the distribution's anchor directory and run update-ca-certificates or update-ca-trust extract in a RUN step.

What updating actually means for a container

A running container keeps the truststore it started with. To pick up a JDK vendor's root or block list change you need a new base image, a rebuild of every image that uses it, and a redeploy. The only shortcut is the run-time mount described above, which adds trust; it cannot add a blocked.certs entry. The eMudhra vendor builds post has a checklist for checking container images rather than running containers.

How do Spring, Netty and Apache HttpClient pick up custom trust?

Most libraries follow one of two patterns. Either they use the JDK default SSLContext, in which case the system properties and cacerts apply, or they build their own from trust material you give them, in which case the system properties do not apply to that client at all. Knowing which pattern a library uses tells you where a fix has to go.

LibraryDefault trustHow to give it custom trustWatch for
JDK HttpClient (java.net.http)Default SSLContextHttpClient.newBuilder().sslContext(ctx)Context is fixed per client instance
HttpsURLConnectionDefault SSLSocketFactorysetSSLSocketFactory(ctx.getSocketFactory()) per connection, or setDefaultSSLSocketFactory globallyThe global setter affects every caller
Spring BootThe underlying client's defaultspring.ssl.bundle.* properties, referenced by nameBundles can reload on file change
Netty"null uses the system default" per SslContextBuilder docsSslContextBuilder.forClient().trustManager(...)Trust-all managers meant for tests
Apache HttpClient 5Its own default SSLContextBuild an SSLContext and pass it to the TLS strategySystem properties only with useSystemProperties()

Spring Boot SSL bundles

Spring Boot's SSL documentation describes SSL bundles: named sets of trust material configured under spring.ssl.bundle.jks (Java keystore files in JKS or PKCS12 format) or spring.ssl.bundle.pem (PEM text). An auto-configured SslBundles bean hands each bundle to client and server components by name.

spring:
  ssl:
    bundle:
      pem:
        partner-api:
          truststore:
            certificate: "file:/etc/myapp/partner-ca.crt"
          reload-on-update: true
      jks:
        internal:
          truststore:
            location: "file:/etc/myapp/truststore.p12"
            password: "${TRUSTSTORE_PASSWORD}"
            type: "PKCS12"
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.stereotype.Component;

@Component
public class PartnerClientFactory {
    private final SslBundle bundle;

    public PartnerClientFactory(SslBundles sslBundles) {
        // The same lookup the Spring Boot reference shows; how the bundle is
        // wired into a RestClient, WebClient or server connector depends on
        // your Spring Boot version, so follow that version's reference docs.
        this.bundle = sslBundles.getBundle("partner-api");
    }
}

The documentation says reload-on-update: true starts a file watcher, and that when the files change the bundle reloads and triggers a reload in components that support it, such as Tomcat connectors. It also notes that when bundles are configured through environment variables, the bundle name is converted to lowercase. The useful property of bundles is scope: the partner CA is trusted by the partner client only, not by the whole JVM.

Netty

Netty's SslContextBuilder takes trust material through trustManager(...) overloads for a PEM file, an input stream, a list of X509Certificate objects, a TrustManagerFactory or a single TrustManager. Its javadoc says that passing null "uses the system default".

SslContext ctx = SslContextBuilder.forClient()
        .trustManager(new File("/etc/myapp/partner-ca.crt"))
        .build();

Netty also ships a trust-all trust manager factory intended for tests. Search your codebase for it before an audit does.

Apache HttpClient 5

The HttpClientBuilder javadoc says system properties are taken into account only when useSystemProperties() is called before build(), and the properties it lists are proxy, keep-alive and agent settings. Trust comes from the SSLContext behind the client's TLS strategy. The SSLContextBuilder in HttpCore 5 builds one from a truststore file:

SSLContext ctx = org.apache.hc.core5.ssl.SSLContextBuilder.create()
        .loadTrustMaterial(new File("/etc/myapp/truststore.p12"),
                           System.getenv("TRUSTSTORE_PASSWORD").toCharArray())
        .build();
// Pass ctx to the connection manager's TLS strategy. The builder method
// names changed between HttpClient 5.x minor releases; check your version's docs.

Trusting your private CA and the public roots together

A common need is "public roots plus our internal CA". There are two clean answers. Build one truststore that contains a copy of cacerts plus your root (the keytool walkthrough above), which is simple but freezes the public roots at copy time. Or keep two trust managers, one from the JDK default and one from your private store, and write a small composite X509ExtendedTrustManager that accepts a chain if either accepts it; the JSSE guide shows the delegation pattern for custom trust managers. The composite keeps public roots current with the JDK, at the cost of code you have to test.

Whatever you do, never answer a PKIX error with a trust manager that accepts everything. It turns off the one check TLS exists to perform.

How do you inventory trust stores across a fleet and in SBOMs?

You cannot reason about the next distrust if you do not know where trust lives. The inventory has four layers.

1. Find every JDK and truststore file

# Linux and macOS: JDKs, JDK truststores and likely application truststores
sudo find / -xdev \( -path /proc -o -path /sys \) -prune -o \
  \( -name cacerts -o -name jssecacerts -o -name blocked.certs -o -name blacklisted.certs \
     -o -iname '*truststore*' -o -name '*.jks' -o -name '*.p12' -o -name '*.pfx' \) -print 2>/dev/null

# Which of those cacerts files are links into the distribution store?
for f in $(sudo find / -xdev -name cacerts -path '*/lib/security/*' 2>/dev/null); do
  printf '%s -> %s\n' "$f" "$(readlink -f "$f")"; done
# Windows
Get-ChildItem -Path C:\ -Recurse -ErrorAction SilentlyContinue -Include cacerts,jssecacerts,*.jks,*.p12 |
  Select-Object FullName, LastWriteTime

2. Find what each running JVM was told

# Truststore flags on running JVMs, plus JAVA_TOOL_OPTIONS in their environment
ps -eo pid,args | grep -E "[j]ava .*javax.net.ssl.trustStore|[j]ava .*java.security.properties"
for p in $(pgrep java); do sudo tr '\0' '\n' < /proc/$p/environ 2>/dev/null | grep -E "JAVA_TOOL_OPTIONS|JDK_JAVA_OPTIONS" | sed "s/^/$p: /"; done

# Application configuration that names trust material
grep -rnE "trust-?store|ssl\.bundle|trustManager\(|loadTrustMaterial|TrustManagerFactory" --include=*.{yml,yaml,properties,java,kt} .

3. Fingerprint the contents

Comparing aliases across vendors is useless because aliases differ. Compare SHA-256 fingerprints:

# One line per entry: fingerprint and subject, sorted for diffing
keytool -list -v -keystore "$TS" -storepass:env TRUSTSTORE_PASS 2>/dev/null |
  awk -F': ' '/^Owner:/{o=$2} /SHA256:/{print $2"  "o}' | sort > trust-$(hostname).txt

# Then diff against a reviewed baseline
diff trust-baseline.txt trust-$(hostname).txt

When the next block or distrust arrives, you search these files for the published fingerprints instead of guessing which aliases a vendor used. Do the same for blocked.certs, whose entries are already fingerprints, and record the jdk.security.caDistrustPolicies line from each JDK's java.security.

4. Put it next to your SBOM

A software bill of materials records the JDK as a component, with vendor and version, and that is the field you search first when an advisory names fixed builds. It usually does not record the contents of cacerts, an application truststore or the active distrust policies. CycloneDX's CBOM capability is designed for that gap: it describes cryptographic assets, including algorithms, keys and certificates, and their relationships to software components. Whether you use CBOM or a simpler internal list, keep three facts per service together: the runtime (vendor, version, build date), the truststore actually in use (path and fingerprint list), and any java.security overrides.

Step by step: give one service its own truststore and prove it works

This is the end-to-end procedure we would use for a service that must trust a partner's private CA without changing anything else on the host.

StepActionProof it worked
1Record the JDK in use: java -XshowSettings:properties -version 2>&1 | grep -E "java.home|java.vendor|java.runtime.version"Vendor, version and home path written down
2Confirm the current truststore with -Djavax.net.debug=ssl:trustmanager in a test runThe trustStore is: line names the file you expected
3Get the partner root over a second channel and compare its SHA-256 fingerprintFingerprints match exactly
4Build app-truststore.p12 from a copy of cacerts or from empty, import the root with -storepass:envkeytool -list -alias shows the right fingerprint
5Store it outside the JDK, owned by the service account, mode 0644 or tighterls -l shows the owner and mode you intended
6Point the service at it, preferably per client (Spring bundle, Netty, HttpClient.sslContext), otherwise with javax.net.ssl.trustStoreConfig reviewed in a pull request
7Test with debug on: the partner endpoint succeeds, and a host signed by a CA you did not add fails if the store is private-onlyBoth results as expected; no "Inaccessible trust store" line
8Record the store's fingerprint list in your inventory and add a rebuild step to the image or deploymentBaseline file committed

Step 7 is the one people skip. A successful handshake proves only that some store trusted the server. The negative test, and the absence of the fallback message, prove it was your store.

Which approach fits which situation?

SituationRecommended approachAvoid
One client must trust a partner or internal CAPer-client SSLContext, Spring SSL bundle or Netty trustManagerEditing the JDK cacerts
Every Java app on a managed Linux host needs the corporate rootDistribution store plus packaged JDK, or USE_SYSTEM_CA_CERTS in Temurin imagesHand edits that the next package update reverts
Windows desktop apps must honour group policy rootsWindows-ROOT or Windows-ROOT-LOCALMACHINE with trustStore=NONECopying roots into each JDK by hand
A CA is distrusted or a certificate is compromisedUpdate to the vendor build that ships the change, restart, rebuild imagesWaiting for the old JVM to notice
You cannot update the JDK yetVendor-published workaround (for example a replacement blocked.certs), or remove the affected roots in an application truststore copyDisabling distrust policies to make an error go away
Containers with minimal runtimesTrust changes in the Dockerfile, rebuild on every JDK updatePatching running containers
A service must trust only your private CAA private-only truststore and a negative testRelying on a property path without checking it exists

Common mistakes

  • Assuming Java uses the OS store. A vendor JDK on Windows or macOS reads its own cacerts. Group policy and keychain roots do nothing for it until you use the Windows or Keychain keystore types.
  • Editing JAVA_HOME/lib/security/cacerts and moving on. The next JDK update replaces the file and your change is gone, often months later, on a Friday.
  • A typo in javax.net.ssl.trustStore. In our test the JVM fell back to cacerts with a debug line and no error. A private-only service quietly trusted every public CA.
  • A forgotten jssecacerts. Its presence overrides cacerts for TLS. Somebody created one years ago to fix a single connection, and now the JDK's root updates no longer apply.
  • Believing revocation saves you. It is off by default in the usual trust manager path. A CA revoking an intermediate does not protect a default JVM; a block list or root change does.
  • Comparing aliases instead of fingerprints. OpenJDK, Temurin and Debian use different aliases for the same root.
  • Treating version numbers as root lists. Two builds of 21.0.12 from different vendors can trust different roots.
  • Forgetting jlink images and container layers. They keep the trust they were built with until rebuilt.
  • Setting properties after first use. The default SSLContext is created once. Late System.setProperty calls may not reach libraries that already connected.
  • Fixing a PKIX error with a trust-all manager. It removes server authentication entirely, and it tends to survive into production.
  • Turning off jdk.security.caDistrustPolicies to silence an error. That re-trusts certificates the JDK vendor decided to reject. Replace the server certificate instead.
  • Putting truststore passwords in process arguments. They end up in ps output, logs and tickets. Use options files or -storepass:env.

FAQ

Where is the Java cacerts file?

In JAVA_HOME/lib/security/cacerts on JDK 9 and later, and JAVA_HOME/jre/lib/security/cacerts in a JDK 8 JDK. On Debian, Ubuntu, RHEL and Fedora packaged JDKs it is usually a symbolic link into the distribution's CA directory. Run with -Djavax.net.debug=ssl:trustmanager to see the exact file a JVM opens.

What is the default password for Java cacerts?

The keytool manual gives the initial password as changeit. Since JDK 18, cacerts is a password-less PKCS12 file, so you can read it without a password, but writing to it still needs the store password. Distribution-managed stores can use a different password.

Is cacerts JKS or PKCS12?

PKCS12 since JDK 18 (JDK-8275252). JDK 9 made PKCS12 the default type for new keystores (JEP 229), but cacerts itself stayed JKS until JDK 18. Either type loads through the same KeyStore API thanks to keystore.type.compat=true.

How do I make Java use a custom truststore?

Set -Djavax.net.ssl.trustStore=/path/to/store.p12 and, if needed, -Djavax.net.ssl.trustStoreType and -Djavax.net.ssl.trustStorePassword before the JVM opens any TLS connection. For a single client, build an SSLContext from a TrustManagerFactory and pass it to that client instead. Then confirm the path in the debug output.

Does Java use the Windows certificate store?

Not by default. Set -Djavax.net.ssl.trustStoreType=Windows-ROOT (current user) or Windows-ROOT-LOCALMACHINE (machine-wide) together with -Djavax.net.ssl.trustStore=NONE. The SunMSCAPI provider in the JDK supplies these keystore types.

Does Java use the macOS keychain?

Not by default. The Apple provider offers KeychainStore for the user's keychain and, on JDKs that document it such as JDK 25, KeychainStore-ROOT for the system root certificates keychain. Use it with trustStore=NONE.

What does "PKIX path building failed" mean?

The trust manager could not build a chain from the server's certificate to any root in the truststore it is using. Either the server sent an incomplete chain, the root is missing from that truststore, or the JVM is not using the truststore you think. Check the trustStore is: debug line first, then the server's chain.

What is jdk.security.caDistrustPolicies?

A security property in java.security that tells the JDK to reject TLS server certificates anchored by named roots, sometimes only those issued after a cutoff date. The OpenJDK master branch lists SYMANTEC_TLS, ENTRUST_TLS, CAMERFIRMA_TLS and CHUNGHWA_TLS. The roots stay in cacerts so other uses, such as code signing, keep working.

Does updating the JDK update cacerts in running applications?

No. A running JVM keeps the truststore it loaded. Restart it on the new JDK, and rebuild any container or jlink image, because those carry their own copy.

How do I add a corporate root CA for all Java apps in Docker?

For Temurin official Linux images, mount the PEM files into /certificates and set USE_SYSTEM_CA_CERTS=1. For images that use the distribution's Java package, copy the root into the distribution anchor directory and run update-ca-certificates or update-ca-trust extract in the Dockerfile. For jlink runtimes, import it with keytool during the build.

Where Safeguard fits, honestly

Safeguard does not read truststores, check certificates or decide which roots a JVM should trust; that work belongs to the JDK, your configuration and the steps above. Where it helps is the step that slows every CA response down: finding every place a given runtime and library version is built in. Safeguard scans repositories, container images and SBOM files (safeguard scan --dir ., --image <name>, --sbom <file>) and shows which projects contain a given component and version, so when a JDK vendor ships a trust fix you can list the images still built on the old base before you start rebuilding. Libraries such as Netty, Apache HttpClient and Spring Boot are ordinary open source dependencies and are covered like any other.

Trust store changes ship inside JDK builds, base images and TLS libraries, and every one of those reaches production through the same dependency and image pipeline as the rest of your code. Keeping that pipeline current is what turns a CA distrust from a week of hunting into a routine rebuild, though the fit here is indirect: Safeguard handles the dependencies and images, not the certificates.

Fix it without waiting for upstream

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.

3. Gold Registry: start from components that are already fixed

registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

See how Safeguard would help

To be plain about scope: none of these demos inspects a truststore or a certificate. They show how Safeguard works on the code and images you build, which for this topic means the services, base images and TLS libraries that carry your Java trust configuration.

This first demo is a real CLI session. You install the CLI, run safeguard to open the interactive terminal and ask it to fix a lodash CVE; it reads the finding and advisory, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with your approval, prints a summary and pushes nothing. The same ask before every command flow applies when you point it at a Java service's repository and want a dependency or base image bumped after a JDK trust fix.

Click through it at your own pace, or open it full screen.

The second demo sets up the local runner on your own machine: you approve it with its device code, then scan from the terminal. That suits build hosts where you assemble jlink runtimes and container images and would rather the scan run where those artifacts already are.

Click through it at your own pace, or open it full screen.

The third demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. If your team already asks Claude to help with Java services, this is how to let it ask Safeguard which of your projects carry a given component and version, with only the tools you approve.

Click through it at your own pace, or open it full screen.

You can download Safeguard or read the CLI documentation to try it on your own build.

Quick check

A service runs with -Djavax.net.ssl.trustStore=/etc/app/trustore.p12, but the file is actually named truststore.p12. What happens on a current OpenJDK build?

The JVM cannot open the named file, logs "Inaccessible trust store" at debug level, and falls back to the default cacerts. The service keeps working, but it now trusts every public root in cacerts instead of only the roots in its own store. Only the debug output or a negative test reveals it.

Why does the JDK keep a distrusted CA's roots in cacerts instead of deleting them?

Because jdk.security.caDistrustPolicies only rejects TLS server certificates. Keeping the roots lets code signatures and timestamps made under them keep verifying, and a date-based policy lets certificates issued before the cutoff run until they expire, while newly issued TLS certificates fail.

Sources

Last reviewed: 24 October 2026. JDK trust behaviour changes with each release; check your JDK's own java.security file and your vendor's release notes for the version you run.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.