Every OpenJDK install ships cacerts — the default truststore of public CA certificates. When your team hits PKIX path building failed against an internal API, the quick fix is keytool -importcert into cacerts. That works in a lab, but on a shared server or inside a container fleet it creates long-term problems: every JVM application inherits your private CA, package upgrades or CA bundle refreshes can regenerate, merge, or change the system truststore, and rollback is messy.
This guide compares cacerts with a custom truststore, shows where the file lives on Ubuntu 26.04, and walks through building trust.p12 that only your application loads via javax.net.ssl.trustStore. For the PKIX error itself, see Fix Java PKIX path building failed. For keystore vs truststore roles, see Java keystore vs truststore.
Tested on: Ubuntu 26.04 LTS (Resolute); OpenJDK 25.0.3; kernel 7.0.0-27-generic.
cacerts vs custom truststore
JDK cacerts |
Custom truststore (trust.p12) |
|
|---|---|---|
| Scope | All JVM apps on that JDK unless overridden | Only apps you configure |
| Contents | Distro-maintained public CAs | Your private CA (and optional public CAs you copy in) |
| Password | changeit (well known) |
You choose |
Survives apt upgrade openjdk-* |
Ubuntu/Debian may refresh /etc/ssl/certs/java/cacerts |
Yes — your file is independent |
| Best for | Public HTTPS out of the box | Internal TLS, lab CAs, per-service trust |
| Risk of editing | High — global blast radius | Low — isolated to one mount or config flag |
The JVM picks the truststore in this order when you do not set explicit SSL code:
javax.net.ssl.trustStoresystem property (if set)$JAVA_HOME/lib/security/jssecacerts(if it exists — rarely used today)$JAVA_HOME/lib/security/cacerts
Setting -Djavax.net.ssl.trustStore=trust.p12 replaces step 3 entirely for that process.
-Djavax.net.ssl.trustStore=/path/to/trust.p12 and the file path is wrong, Java does not fall back to cacerts. JSSE can initialize with an empty truststore, which usually causes confusing PKIX errors. Confirm the path exists before blaming the CA certificate.
A custom truststore that contains only your private CA is not a superset of cacerts. It may fix internal HTTPS but break public HTTPS calls to services such as GitHub, AWS, Maven Central, or external APIs. Setting javax.net.ssl.trustStore replaces the default bundle — it does not merge with it.
Where cacerts lives on Ubuntu (tested)
Resolve the active JDK and inspect the symlink:
KEYTOOL=$(readlink -f "$(command -v keytool)")
JAVA_HOME=$(dirname "$(dirname "$KEYTOOL")")
ls -la "$JAVA_HOME/lib/security/cacerts"lrwxrwxrwx 1 root root 27 Apr 26 15:14 /usr/lib/jvm/java-25-openjdk-amd64/lib/security/cacerts -> /etc/ssl/certs/java/cacertsFind all cacerts files under /usr/lib/jvm:
find /usr/lib/jvm -name cacerts/usr/lib/jvm/java-25-openjdk-amd64/lib/security/cacertsList a sample of trusted public CAs (password changeit):
keytool -list -cacerts -storepass changeit | head -6Keystore type: JKS
Keystore provider: SUN
Your keystore contains 121 entries
debian:ac_raiz_fnmt-rcm.pem, Jul 2, 2026, trustedCertEntry,On Debian and Ubuntu, ca-certificates-java owns /etc/ssl/certs/java/cacerts. Manual edits there affect every JDK that symlinks to it.
Where cacerts lives on RHEL and Rocky Linux (documented paths)
On RHEL-family systems the canonical Java truststore is the system-wide file maintained by update-ca-trust, not a vendor-bundled copy inside the JDK tree.
| Path | Role |
|---|---|
/etc/pki/java/cacerts |
System Java truststore (Red Hat documentation) |
/usr/lib/jvm/java-*/lib/security/cacerts |
Symlink into /etc/pki/java/cacerts on typical OpenJDK packages |
/etc/pki/ca-trust/source/anchors/ |
Drop PEM CAs here, then run update-ca-trust extract |
Red Hat documents /etc/pki/java/cacerts as the trust anchor repository for Red Hat build of OpenJDK. The update-ca-trust(8) man page describes how extracted Java keystores are regenerated from administrator and package sources.
For system-wide private CAs on RHEL or Rocky Linux, prefer:
cp my-root-ca.crt /etc/pki/ca-trust/source/anchors/
update-ca-trust extractThat updates /etc/pki/java/cacerts for all Java apps — still broader than a per-app custom file, but safer than hand-editing with keytool -importcert directly on a live JDK copy.
On RHEL-family systems, use update-ca-trust when every Java app on the host must trust a corporate CA. Use a custom trust.p12 when only one application should trust a private CA.
Build and use a custom truststore
Create a PKCS12 truststore with only the CA your service needs:
keytool -importcert -alias labroot -file root-ca.crt \
-keystore trust.p12 -storetype PKCS12 -storepass changeit -nopromptIf the server chain includes an intermediate, import it too (same pattern as the PKIX fix guide).
Point your application at the file. Set trustStoreType explicitly so the example works on Java 8, mixed JDK installs, and older distributions — PKCS12 is the default keystore type from Java 9 onward (JEP 229), but explicit types avoid surprises:
java \
-Djavax.net.ssl.trustStore=trust.p12 \
-Djavax.net.ssl.trustStorePassword=changeit \
-Djavax.net.ssl.trustStoreType=PKCS12 \
-jar app.jar-D flags can appear in process listings, CI logs, and container specs. Use a protected environment source, secret manager, systemd environment file with restricted permissions, or application-specific secret handling instead.
Test failure before success
The same Java client against a private HTTPS server failed when the JVM used default cacerts (the lab CA is not in the public bundle):
javax.net.ssl.SSLHandshakeException: PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested targetAfter starting the app with the custom truststore flags above, the request succeeded:
HTTP 200 body=OKNo change to cacerts was required.
Verify which truststore Java is using
When testing a custom truststore, confirm that Java is really loading the file you expect. Oracle’s JSSE Reference Guide documents javax.net.debug; trustmanager tracing shows which anchors the trust manager loaded:
java \
-Djavax.net.debug=ssl,trustmanager \
-Djavax.net.ssl.trustStore=trust.p12 \
-Djavax.net.ssl.trustStorePassword=changeit \
-Djavax.net.ssl.trustStoreType=PKCS12 \
-jar app.jarLook for trust manager lines showing certificates loaded from your custom file rather than the default cacerts path.
You can also verify the file on disk before starting the JVM:
keytool -list -v \
-keystore trust.p12 \
-storetype PKCS12 \
-storepass changeitThis catches the most common mistake: the right trust.p12 exists, but the running process never received -Djavax.net.ssl.trustStore (or points at a wrong path and ends up with an empty truststore).
When editing cacerts is still reasonable
| Scenario | Approach |
|---|---|
| Single-user dev laptop, one JDK | Temporary keytool -importcert into a copy of cacerts for experiments |
| Golden VM image for a dedicated app server | Bake a custom truststore into the image; avoid mutating cacerts in post-install scripts |
| All JVM apps on the host must trust a corporate root | RHEL: update-ca-trust; Debian/Ubuntu: update-ca-certificates / ca-certificates-java — not raw keytool on the live symlink target |
| Container with one Java process | Mount trust.p12 and set JAVA_TOOL_OPTIONS with trustStore, trustStorePassword, and trustStoreType=PKCS12 — never modify the JDK inside the image layer |
To practice import syntax safely, copy cacerts first — covered in Import certificate into Java cacerts on Linux.
Composite truststore pattern
Because setting javax.net.ssl.trustStore replaces cacerts, a truststore that only contains your private CA cannot validate public sites like https://github.com. Options:
- Import your private CA and copy required public roots into
trust.p12(large file). - Start from a copy of
cacertsand add your CA:
cp /etc/ssl/certs/java/cacerts cacerts-plus-lab.jks
keytool -importcert -alias labroot -file root-ca.crt \
-keystore cacerts-plus-lab.jks -storepass changeit -nopromptThe copied file keeps the original cacerts store type (JKS on Ubuntu/Debian). Verify with keytool -list -keystore cacerts-plus-lab.jks -storepass changeit before using it in production. Convert to PKCS12 with keytool convert JKS to PKCS12 if your runtime expects .p12.
- Use separate JVM instances or profiles — internal API client with custom truststore, public egress with default
cacerts.
References
- Java Secure Socket Extension (JSSE) Reference Guide (Oracle)
- JEP 229: Create PKCS12 keystores by default
- Spring Boot SSL bundles
- keytool documentation (Oracle)
- Red Hat build of OpenJDK — trust anchor repository
- update-ca-trust(8) — Fedora/RHEL man page
- Install keytool on Ubuntu
- Java keystore vs truststore
- Import certificate into cacerts on Linux
Common mistakes
| Mistake | Result |
|---|---|
Wrong trust.p12 path |
Java may initialize with an empty truststore and still fail with PKIX errors |
| Custom truststore contains only private CA | Public HTTPS calls to GitHub, AWS, Maven Central, or external APIs may fail |
| Imported the server certificate instead of the root/intermediate CA | Trust may break after server certificate renewal |
Edited one JDK's cacerts but the app uses another JDK |
The PKIX error remains unchanged |
Forgot trustStoreType=PKCS12 on older Java |
Java may fail to read the custom store correctly |
| Used Spring Boot server SSL properties for outbound client trust | The embedded server may be configured, but client calls may still fail |
Summary
cacerts is the JDK-wide public CA bundle — convenient for the open internet, risky to edit for private CAs. A custom PKCS12 truststore scoped with -Djavax.net.ssl.trustStore, -Djavax.net.ssl.trustStorePassword, and -Djavax.net.ssl.trustStoreType=PKCS12 isolates trust to the applications that need it and survives JDK package upgrades — but it replaces cacerts, so verify the path, debug with javax.net.debug=trustmanager, and use a composite truststore if you still need public HTTPS. On Ubuntu and Debian, cacerts symlinks to /etc/ssl/certs/java/cacerts; on RHEL and Rocky Linux, use update-ca-trust for system-wide CAs or a dedicated trust.p12 when only one app needs a private CA.

