You built trust.p12 with keytool -importcert, but the application still throws PKIX path building failed. The missing step is telling the JVM which file to load at runtime. Java reads TLS material from system properties whose names start with javax.net.ssl. — not from the filename alone, and not from environment variables unless your startup script maps them to -D flags.
This guide lists every trustStore and keyStore property, builds a PKCS12 truststore with keytool, runs a client with and without -Djavax.net.ssl.trustStore, and shows how to confirm what a live process actually picked up. For the conceptual split between identity and trust files, see Java keystore vs truststore. For when to avoid editing global cacerts, see Custom truststore vs cacerts.
Tested on: Ubuntu 26.04 LTS; OpenJDK 25.0.3; kernel 7.0.0-27-generic.
Prerequisites
- OpenJDK with
keytool— Install keytool on Ubuntu — see keytool command. opensslfor the lab HTTPS server and CA (OpenSSL).
JVM property reference
Oracle's JSSE reference guide documents these system properties. IBM and other vendors use the same names for plain Java TLS:
| Property | Purpose | Typical value |
|---|---|---|
javax.net.ssl.trustStore |
Path to truststore file | /opt/app/trust.p12 |
javax.net.ssl.trustStorePassword |
Truststore password | changeit (or your secret) |
javax.net.ssl.trustStoreType |
Keystore format | PKCS12 or JKS |
javax.net.ssl.keyStore |
Path to identity keystore | /opt/app/server.p12 |
javax.net.ssl.keyStorePassword |
Identity keystore password | your store password |
javax.net.ssl.keyStoreType |
Identity keystore format | PKCS12 |
Trust properties control who the JVM trusts on outbound HTTPS, JDBC over TLS, LDAPS, and similar client connections. Key store properties supply the default identity keystore some APIs read when you do not configure SSL in application code.
javax.net.ssl. prefix. Forum posts sometimes shorten them to ssl.trustStore — that is not a valid JSSE property and is silently ignored.
Default behavior when properties are unset
JSSE resolves the default truststore in this order:
- File set with
-Djavax.net.ssl.trustStore java.home/lib/security/jssecacertsif it existsjava.home/lib/security/cacerts
When javax.net.ssl.trustStore is null, JSSE skips step 1 and falls back to jssecacerts, then cacerts.
When javax.net.ssl.keyStore is null, there is no global default identity keystore — embedded servers and frameworks set identity through their own config (server.xml, Spring Boot server.ssl.*, etc.).
Print what the JVM sees before you add flags:
cat > PropsProbe.java << 'EOF'
public class PropsProbe {
public static void main(String[] args) {
String[] keys = {
"javax.net.ssl.trustStore", "javax.net.ssl.trustStorePassword", "javax.net.ssl.trustStoreType",
"javax.net.ssl.keyStore", "javax.net.ssl.keyStorePassword", "javax.net.ssl.keyStoreType",
"java.home"
};
for (String k : keys) System.out.println(k + "=" + System.getProperty(k));
}
}
EOF
javac PropsProbe.java
java PropsProbejavax.net.ssl.trustStore=null
javax.net.ssl.trustStorePassword=null
javax.net.ssl.trustStoreType=null
javax.net.ssl.keyStore=null
javax.net.ssl.keyStorePassword=null
javax.net.ssl.keyStoreType=null
java.home=/usr/lib/jvm/java-25-openjdk-amd64All null means the process uses JSSE default truststore lookup: jssecacerts if present, otherwise cacerts. It also has no global identity keystore from system properties.
Build a truststore with keytool
Create a PKCS12 file with your private CA (or copy from your PKI team). The snippet below uses a quick lab CA; for a dedicated root signer see create a root CA on Linux:
mkdir -p ~/ssl-props-lab/certs && cd ~/ssl-props-lab
openssl req -x509 -newkey rsa:2048 -keyout certs/lab-ca.key -out certs/lab-ca.crt \
-days 365 -nodes -subj "/CN=Props Lab CA/O=Lab/C=US" 2>/dev/null
keytool -importcert -alias props-lab-ca -file certs/lab-ca.crt \
-keystore trust.p12 -storetype PKCS12 -storepass changeit -noprompt
keytool -list -keystore trust.p12 -storepass changeit | head -8Certificate was added to keystore
Keystore type: PKCS12
Keystore provider: SUN
Your keystore contains 1 entry
props-lab-ca, Jul 3, 2026, trustedCertEntry,
Certificate fingerprint (SHA-256): E1:DA:CB:10:D0:9E:EF:CD:E8:7E:E3:DE:C4:0F:20:EE:74:BD:36:C1:B7:F1:44:4E:49:4B:62:C1:19:E2:B6:73Only trustedCertEntry rows belong in a truststore. Do not import your server's private key here — that belongs in a keystore loaded with javax.net.ssl.keyStore.
Lab: client without and with trustStore
Start a local HTTPS server signed by the lab CA (SAN includes 127.0.0.1):
openssl genrsa -out certs/server.key 2048 2>/dev/null
openssl req -new -key certs/server.key -out certs/server.csr \
-subj "/CN=internal.lab.local/O=Lab/C=US" 2>/dev/null
cat > certs/server.ext << 'EOF'
subjectAltName = IP:127.0.0.1,DNS:internal.lab.local
extendedKeyUsage = serverAuth
EOF
openssl x509 -req -in certs/server.csr -CA certs/lab-ca.crt -CAkey certs/lab-ca.key \
-CAcreateserial -out certs/server.crt -days 365 -extfile certs/server.ext 2>/dev/null
openssl s_server -accept 8443 -cert certs/server.crt -key certs/server.key -www \
</dev/null >/dev/null 2>&1 &
SERVER_PID=$!
sleep 1TLS client without custom truststore — uses JSSE default truststore lookup:
cat > TlsClient.java << 'EOF'
import javax.net.ssl.HttpsURLConnection;
import java.net.URL;
public class TlsClient {
public static void main(String[] args) throws Exception {
try {
HttpsURLConnection c = (HttpsURLConnection) new URL("https://127.0.0.1:8443/").openConnection();
c.setConnectTimeout(5000);
c.setReadTimeout(5000);
c.connect();
System.out.println("HTTP status: " + c.getResponseCode());
} catch (Exception e) {
String msg = e.getMessage();
if (msg != null && msg.contains("\n")) msg = msg.split("\n")[0];
System.out.println("FAILED: " + e.getClass().getSimpleName() + ": " + msg);
}
}
}
EOF
javac TlsClient.java
java TlsClientFAILED: SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested targetThe lab CA is not in the default JSSE truststore (jssecacerts or cacerts under java.home). Point JSSE at your PKCS12 file by passing all three trust properties on the same java command line, before the main class name:
| Property | Value in this lab |
|---|---|
javax.net.ssl.trustStore |
Path to trust.p12 ($PWD resolves to the lab directory) |
javax.net.ssl.trustStorePassword |
changeit — same password used during keytool -importcert |
javax.net.ssl.trustStoreType |
PKCS12 — required for .p12 files on older JDKs and mixed fleets |
java \
-Djavax.net.ssl.trustStore="$PWD/trust.p12" \
-Djavax.net.ssl.trustStorePassword=changeit \
-Djavax.net.ssl.trustStoreType=PKCS12 \
TlsClientWhen the flags reach the JVM that runs TlsClient, JSSE loads props-lab-ca from trust.p12, validates the server chain, and the HTTPS call completes:
HTTP status: 200That single line is enough to confirm PKIX trust succeeded — the TLS handshake finished and the server returned a normal HTTP response.
Before you copy the same flags into systemd or Docker, run PropsProbe with the identical -D arguments. It prints what System.getProperty() returns at startup — the same values jcmd PID VM.system_properties reports on a live process:
java \
-Djavax.net.ssl.trustStore="$PWD/trust.p12" \
-Djavax.net.ssl.trustStorePassword=changeit \
-Djavax.net.ssl.trustStoreType=PKCS12 \
PropsProbejavax.net.ssl.trustStore=/root/ssl-props-lab/trust.p12
javax.net.ssl.trustStorePassword=changeit
javax.net.ssl.trustStoreType=PKCS12
javax.net.ssl.keyStore=null
javax.net.ssl.keyStorePassword=null
javax.net.ssl.keyStoreType=null
java.home=/usr/lib/jvm/java-25-openjdk-amd64All three trust properties point at your lab file. The keyStore* lines stay null because this client only validates the server — it does not present a client certificate. java.home shows which JDK directory JSSE uses for default trust when trustStore is unset.
trustStore replaces the default JSSE truststore lookup for this process — it does not merge with cacerts.
Clean up:
kill $SERVER_PID 2>/dev/null
rm -rf ~/ssl-props-labPass properties at runtime
-D arguments may be visible through process inspection tools such as ps command, /proc/<pid>/cmdline, or jcmd. For production, use a protected systemd environment file, Kubernetes Secret, container runtime secret, or application-specific secret mechanism.
Command line
java \
-Djavax.net.ssl.trustStore=/opt/app/trust.p12 \
-Djavax.net.ssl.trustStorePassword=changeit \
-Djavax.net.ssl.trustStoreType=PKCS12 \
-jar myapp.jarUse absolute paths. Relative paths depend on the process working directory, which systemd, Docker, and cron jobs often set differently than your SSH session.
JAVA_TOOL_OPTIONS
Most JVM processes launched through the standard Java launcher pick this up automatically, which makes it useful in containers and wrapper scripts. In locked-down environments, verify it with jcmd or PropsProbe because some launches may ignore or restrict it.
export JAVA_TOOL_OPTIONS="-Djavax.net.ssl.trustStore=/opt/app/trust.p12 -Djavax.net.ssl.trustStorePassword=changeit -Djavax.net.ssl.trustStoreType=PKCS12"
java -jar myapp.jarPatterns for Docker and Kubernetes are in Java truststore in Docker and Kubernetes.
Tomcat and systemd
Tomcat setenv.sh or CATALINA_OPTS often carry both trust and identity flags:
JAVA_OPTS="$JAVA_OPTS -Djavax.net.ssl.trustStore=/opt/tomcat/trust.p12"
JAVA_OPTS="$JAVA_OPTS -Djavax.net.ssl.trustStorePassword=changeit"
JAVA_OPTS="$JAVA_OPTS -Djavax.net.ssl.trustStoreType=PKCS12"For a generic systemd service, use a drop-in. See systemctl command for daemon-reload and unit edit workflows if you rarely touch systemd units.
sudo systemctl edit myapp.service[Service]
Environment="JAVA_TOOL_OPTIONS=-Djavax.net.ssl.trustStore=/opt/app/trust.p12 -Djavax.net.ssl.trustStorePassword=changeit -Djavax.net.ssl.trustStoreType=PKCS12"Then reload and restart:
sudo systemctl daemon-reload
sudo systemctl restart myapp.serviceFor a running service, confirm what loaded — replace $PID with the Java process ID from systemctl status or pgrep -f myapp:
jcmd "$PID" VM.command_line
jcmd "$PID" VM.system_properties | grep -E 'javax.net.ssl|java.home'javax.net.ssl.trustStore=/opt/app/trust.p12
javax.net.ssl.trustStorePassword=changeit
javax.net.ssl.trustStoreType=PKCS12
java.home=/usr/lib/jvm/java-25-openjdk-amd64When javax.net.ssl.trustStore lines are absent, the JVM falls back to jssecacerts then cacerts under java.home. See Find the correct Java cacerts file for that path.
keyStore properties for server identity
javax.net.ssl.keyStore* configures the JVM default identity keystore — the file with your PrivateKeyEntry that proves who the TLS endpoint is.
java \
-Djavax.net.ssl.keyStore=/opt/app/server.p12 \
-Djavax.net.ssl.keyStorePassword=changeit \
-Djavax.net.ssl.keyStoreType=PKCS12 \
-jar myapp.jarMost application servers do not rely on these globals alone:
| Runtime | Identity configuration |
|---|---|
| Tomcat | server.xml certificateKeystoreFile / PKCS12 <Certificate> attributes — see Tomcat SSL with keytool |
| Spring Boot | server.ssl.key-store in application.properties — see Spring Boot HTTPS with keytool |
Plain HttpsServer / custom code |
KeyManagerFactory initialized from your PKCS12 file |
Do not point keyStore and trustStore at the same file unless you deliberately merged roles — mixing server private keys into a truststore widens blast radius if the file leaks.
Common mistakes
| Mistake | What happens |
|---|---|
-Dssl.trustStore=... (missing javax.net.ssl.) |
Property ignored; JSSE default truststore lookup is still used |
trustStore set without trustStoreType in mixed JKS/PKCS12 environments |
Usually works on modern JDKs, but can fail on older JDKs or vendor runtimes |
Private-CA-only trust.p12 without public CAs |
Internal HTTPS works; Maven Central / public APIs fail |
| Flags in your shell, not in the service unit | App still uses default trust after restart |
Editing cacerts while app uses -Djavax.net.ssl.trustStore |
Import never reaches the running JVM |
Wrong property name demo — ssl.trustStore is not a valid JSSE property, so Java ignores it and PKIX still fails the same way as the first TlsClient run:
java -Dssl.trustStore="$PWD/trust.p12" TlsClientFAILED: SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested targetTroubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
PKIX after creating trust.p12 |
Properties not passed to the JVM that runs the app | jcmd PID VM.system_properties; fix systemd/Docker env |
jcmd shows trustStore, but app still fails |
Framework or library builds its own SSLContext and ignores JVM defaults | Configure truststore in the app or library settings — Spring Boot SSL bundles, Apache HttpClient, Netty, Kafka, Elasticsearch client, or JDBC driver SSL options |
Keystore was tampered with on startup |
Wrong trustStorePassword |
Match password used at keytool -importcert time |
Property set but debug shows cacerts |
Typo in name or child process without inherited flags | VM.command_line; use JAVA_TOOL_OPTIONS |
| Server starts but wrong cert presented | keyStore path wrong; trust flags unrelated |
Fix keyStore* or Tomcat/Spring identity config |
Trigger SSL initialization and read the loaded file:
java -Djavax.net.debug=trustmanager -Djavax.net.ssl.trustStore=/opt/app/trust.p12 \
-Djavax.net.ssl.trustStorePassword=changeit -Djavax.net.ssl.trustStoreType=PKCS12 \
-cp . TrustProbeUse the TrustProbe helper from Find the correct Java cacerts file — it calls SSLContext.getDefault() so trustStore is: appears in debug output.
References
- Java Secure Socket Extension (JSSE) Reference Guide —
javax.net.ssl.trustStoreandkeyStoresystem properties - keytool documentation — build PKCS12 truststores
- JEP 229: Create PKCS12 keystores by default — default keystore type from Java 9
Summary
keytool creates the file; -Djavax.net.ssl.trustStore* tells the JVM to load it. Set trustStore, trustStorePassword, and trustStoreType for outbound trust; set keyStore* only when you need a global identity keystore. Unset trustStore skips step 1 and falls back to jssecacerts then cacerts. Verify with PropsProbe, jcmd VM.system_properties, or trustmanager debug — and pass the same flags to the process that actually runs your application, not only your interactive shell.

