Use Custom Java Truststore Instead of Editing cacerts

Prefer a custom PKCS12 truststore over editing JDK cacerts: isolate private CA trust per app, survive package upgrades, and set javax.net.ssl.trustStore without touching the global Java CA bundle.

Published

Updated

Read time 7 min read

Reviewed byDeepak Prasad

Custom Java truststore vs cacerts banner with JVM and application isolation diagram

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:

  1. javax.net.ssl.trustStore system property (if set)
  2. $JAVA_HOME/lib/security/jssecacerts (if it exists — rarely used today)
  3. $JAVA_HOME/lib/security/cacerts

Setting -Djavax.net.ssl.trustStore=trust.p12 replaces step 3 entirely for that process.

IMPORTANT
If you set -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:

bash
KEYTOOL=$(readlink -f "$(command -v keytool)")
JAVA_HOME=$(dirname "$(dirname "$KEYTOOL")")
ls -la "$JAVA_HOME/lib/security/cacerts"
text
lrwxrwxrwx 1 root root 27 Apr 26 15:14 /usr/lib/jvm/java-25-openjdk-amd64/lib/security/cacerts -> /etc/ssl/certs/java/cacerts

Find all cacerts files under /usr/lib/jvm:

bash
find /usr/lib/jvm -name cacerts
text
/usr/lib/jvm/java-25-openjdk-amd64/lib/security/cacerts

List a sample of trusted public CAs (password changeit):

bash
keytool -list -cacerts -storepass changeit | head -6
text
Keystore 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:

bash
cp my-root-ca.crt /etc/pki/ca-trust/source/anchors/
update-ca-trust extract

That 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:

bash
keytool -importcert -alias labroot -file root-ca.crt \
  -keystore trust.p12 -storetype PKCS12 -storepass changeit -noprompt

If 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:

bash
java \
  -Djavax.net.ssl.trustStore=trust.p12 \
  -Djavax.net.ssl.trustStorePassword=changeit \
  -Djavax.net.ssl.trustStoreType=PKCS12 \
  -jar app.jar
NOTE
For production, avoid putting truststore passwords directly in shell history, process arguments, or committed service files. Passwords passed as -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):

text
javax.net.ssl.SSLHandshakeException: PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested target

After starting the app with the custom truststore flags above, the request succeeded:

text
HTTP 200 body=OK

No 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:

bash
java \
  -Djavax.net.debug=ssl,trustmanager \
  -Djavax.net.ssl.trustStore=trust.p12 \
  -Djavax.net.ssl.trustStorePassword=changeit \
  -Djavax.net.ssl.trustStoreType=PKCS12 \
  -jar app.jar

Look 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:

bash
keytool -list -v \
  -keystore trust.p12 \
  -storetype PKCS12 \
  -storepass changeit

This 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:

  1. Import your private CA and copy required public roots into trust.p12 (large file).
  2. Start from a copy of cacerts and add your CA:
bash
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 -noprompt

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

  1. Use separate JVM instances or profiles — internal API client with custom truststore, public egress with default cacerts.

References


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.


Frequently Asked Questions

1. Should I edit Java cacerts for a private CA?

Usually no for application-specific private CAs. Editing cacerts is system-wide and affects every JVM app that uses that JDK. For a corporate CA every Java app must trust, use the OS or distro trust mechanism: update-ca-trust on RHEL/Rocky, or update-ca-certificates on Debian/Ubuntu with ca-certificates-java installed. Otherwise create a custom truststore with keytool -importcert and point only the apps that need the private CA at it.

2. What is the default Java truststore password?

The default cacerts password is changeit on OpenJDK. Custom truststores use whatever password you set at creation. For plain JVM apps, pass -Djavax.net.ssl.trustStorePassword and optionally -Djavax.net.ssl.trustStoreType=PKCS12. For Spring Boot 3.1+, prefer SSL bundles such as spring.ssl.bundle.jks..truststore.location and spring.ssl.bundle.jks..truststore.password when configuring specific clients or server components.

3. Where is cacerts on Ubuntu and Debian?

OpenJDK packages symlink $JAVA_HOME/lib/security/cacerts to /etc/ssl/certs/java/cacerts. The file is maintained by the ca-certificates-java package and updated when the distro refreshes the Mozilla CA bundle.

4. Where is cacerts on RHEL and Rocky Linux?

The system Java truststore is /etc/pki/java/cacerts, generated by update-ca-trust. JDK paths such as /usr/lib/jvm/java-*/lib/security/cacerts typically symlink to that file. Prefer update-ca-trust for system-wide CAs on RHEL-family distros.

5. Can I use both cacerts and a custom truststore?

When javax.net.ssl.trustStore is set, the JVM uses that file instead of cacerts — it does not merge them. Import public CAs you still need into your custom file, or use a composite truststore built from cacerts plus your private CA.

6. How do I build a custom truststore from scratch?

keytool -importcert -alias myca -file ca.crt -keystore trust.p12 -storetype PKCS12 -storepass changeit -noprompt. Repeat for each CA. Run keytool -list -keystore trust.p12 to verify trustedCertEntry entries only.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …