Fix Java PKIX Path Building Failed Using keytool Truststore

Fix javax.net.ssl.SSLHandshakeException PKIX path building failed by importing your private CA chain into a PKCS12 truststore with keytool, then point the JVM at it with javax.net.ssl.trustStore — without editing cacerts.

Published

Updated

Read time 10 min read

Reviewed byDeepak Prasad

Fix Java PKIX path building failed banner with truststore and TLS chain diagram

Your Java client connects to HTTPS and dies with PKIX path building failed. The server is up, the port is open, and curl may even work with -k — but HttpClient, JDBC over TLS, or a Spring RestTemplate call throws javax.net.ssl.SSLHandshakeException because the JVM does not trust the private CA that signed the server certificate.

This guide reproduces that failure on Ubuntu with OpenJDK 25, builds a custom PKCS12 truststore with keytool, imports a root plus intermediate CA chain, and confirms HTTP status: 200 from the lab TlsClient after setting javax.net.ssl.trustStore. If you are new to the difference between identity keystores and truststores, read Java keystore vs truststore first.

Tested on: Ubuntu 26.04 LTS (Resolute); OpenJDK 25.0.3; kernel 7.0.0-27-generic.


What PKIX path building failed means

Java validates TLS server certificates with the PKIX algorithm. It walks the chain from the leaf certificate toward a trusted root anchor stored in the truststore. When no anchor matches, you see:

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

Older JDK releases often omit the (certificate_unknown) prefix; the core message is the same: unable to find valid certification path.

Symptom Likely cause
PKIX on internal HTTPS only Private or corporate CA not in cacerts
PKIX after cert renewal Old intermediate removed from truststore
Works in browser, fails in Java Browser uses OS trust store; JVM uses cacerts or your custom file
curl https://... fails, curl -k works Verification disabled vs enabled

Prerequisites

Before reproducing the error, confirm you have the tools and CA material the fix depends on:

  • keytool installed (ships with OpenJDK JDK or JRE).
  • openssl for the lab CA chain (optional if you already have .crt files from your PKI team).
  • The CA certificate(s) that signed your server — usually root and intermediate PEM files.

If your production server is already running, skip the lab CA generation and import the PEM files your PKI team provides into trust.p12 using the same keytool -importcert steps later in this guide.


Lab: reproduce the failure

The goal here is a controlled HTTPS endpoint signed by a private CA that is not in the JVM default trust bundle. You will build a root CA, an intermediate CA, and a server certificate, then prove that a Java client fails until you add a custom truststore.

Create a working directory and a two-tier CA (root signs intermediate, intermediate signs the server). Password changeit throughout.

First, generate the root and intermediate CAs:

bash
mkdir -p ~/pkix-lab && cd ~/pkix-lab
STOREPASS=changeit

openssl genrsa -out root-ca.key 4096
openssl req -new -x509 -days 3650 -key root-ca.key -out root-ca.crt \
  -subj "/CN=Lab Root CA/O=PKIX Lab/C=US"

openssl genrsa -out intermediate-ca.key 4096
openssl req -new -key intermediate-ca.key -out intermediate-ca.csr \
  -subj "/CN=Lab Intermediate CA/O=PKIX Lab/C=US"
openssl x509 -req -in intermediate-ca.csr -CA root-ca.crt -CAkey root-ca.key -CAcreateserial \
  -out intermediate-ca.crt -days 1825 \
  -extfile <(printf 'basicConstraints=critical,CA:TRUE\nkeyUsage=critical,keyCertSign,cRLSign\n')
NOTE
The <(...) syntax requires Bash. If your shell does not support process substitution, write the extension text to a temporary file and pass that file with -extfile.

Next, sign the server certificate with a SAN for localhost and 127.0.0.1, plus keyUsage and extendedKeyUsage=serverAuth so the leaf looks closer to a production TLS server certificate. Then pack the leaf key and intermediate into server.p12 for the HTTPS server:

bash
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj "/CN=localhost/O=PKIX Lab/C=US"
cat > san.cnf <<'EOF'
subjectAltName=DNS:localhost,IP:127.0.0.1
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
EOF
openssl x509 -req -in server.csr -CA intermediate-ca.crt -CAkey intermediate-ca.key -CAcreateserial \
  -out server.crt -days 825 -extfile san.cnf

openssl pkcs12 -export -in server.crt -inkey server.key -out server.p12 \
  -name server -passout pass:$STOREPASS -certfile intermediate-ca.crt

Before blaming Java, confirm OpenSSL itself trusts the chain from root through intermediate to the server leaf:

bash
openssl verify -CAfile root-ca.crt -untrusted intermediate-ca.crt server.crt

When the chain is wired correctly, OpenSSL prints:

text
server.crt: OK

If verification fails here, fix the CA or signing step before importing anything into a Java truststore.

Confirm the lab CA is not already trusted by the JVM default bundle — we have not imported it yet:

bash
keytool -list -cacerts -storepass changeit -alias labroot 2>&1 | head -2

The alias is missing from cacerts, which is what we expect:

text
keytool error: java.lang.Exception: Alias <labroot> does not exist

Reproduce the PKIX failure

Start a minimal Java HTTPS server on 127.0.0.1:8443 that loads server.p12 as its keystore (see Java keystore vs truststore for the full TlsServer.java sample). Leave that terminal running.

In a second terminal, call the server with a client that relies only on the JVM default truststore — no custom trust.p12:

bash
java TlsClient default

The TLS handshake fails before any HTTP response because the private lab CA is not in cacerts:

text
Exception in thread "main" javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

That is the production-class error you fix in the next section by building trust.p12 and pointing the JVM at it.


Build a custom truststore with keytool

The fix is to give the JVM a PKCS12 file that contains the CA certificates it should trust for your internal HTTPS endpoint. Import public CA certificates only — never the server private key.

Import both the root and intermediate public certificates into a new PKCS12 file. Java needs a trusted anchor and a complete certificate path. Usually the truststore contains the root CA while the server sends the intermediate chain. In labs and misconfigured internal services, importing both the root and intermediate into the custom truststore makes the test self-contained and avoids missing-intermediate failures.

Run one keytool -importcert per CA file. Use distinct aliases so you can list or remove them later:

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

keytool -importcert -alias labintermediate -file intermediate-ca.crt \
  -keystore trust.p12 -storetype PKCS12 -storepass changeit -noprompt

List entries to confirm both imports are trustedCertEntry and that no PrivateKeyEntry slipped in:

bash
keytool -list -keystore trust.p12 -storepass changeit

A healthy client truststore for this lab looks like this:

text
Keystore type: PKCS12
Keystore provider: SUN

Your keystore contains 2 entries

labintermediate, Jul 2, 2026, trustedCertEntry,
labroot, Jul 2, 2026, trustedCertEntry,

Both rows should be trustedCertEntry. If you see PrivateKeyEntry, you imported a keystore file or a PEM bundle that included a private key — start over with CA .crt files only.


Point the JVM at the truststore

Creating trust.p12 is only half the fix — the running JVM must load it. The next subsections show three deployment patterns: JVM flags for any app, Spring Boot client trust, and container or systemd startup.

IMPORTANT
If javax.net.ssl.trustStore points to a file that does not exist, Java does not fall back to cacerts. JSSE can initialize with an empty truststore, which often produces another confusing PKIX error. Confirm the path and password before blaming the CA certificate — see custom truststore vs cacerts.

JVM system properties (any application)

For a plain java -jar process, pass the three JSSE truststore properties at startup. Oracle’s JSSE reference documents javax.net.ssl.trustStore, trustStorePassword, and trustStoreType:

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

Use an absolute path in production (/etc/ssl/trust/trust.p12) so the working directory does not matter.

Confirm the fix in the lab

With TlsServer still running and trust.p12 in your working directory, verify the fix using one approach — not both at once.

Code-level truststore loading loads trust.p12 inside TlsClient when you pass custom and the file path:

bash
java TlsClient custom trust.p12

The handshake succeeds and the client prints the server response (same format as Java keystore vs truststore TlsClient):

text
HTTP status: 200
Body: OK from Java TLS server

JVM-property truststore loading leaves the client code on the default SSLContext path but overrides trust at the JVM level:

bash
java \
  -Djavax.net.ssl.trustStore=trust.p12 \
  -Djavax.net.ssl.trustStorePassword=changeit \
  -Djavax.net.ssl.trustStoreType=PKCS12 \
  TlsClient default

Same successful outcome — PKIX can now anchor the chain to labroot or labintermediate:

text
HTTP status: 200
Body: OK from Java TLS server

Spring Boot

Spring Boot apps that call outbound HTTPS (RestClient, WebClient, JDBC over TLS) need client-side trust configured separately from embedded server SSL.

For a quick application-wide fix when you cannot wire an SSL bundle yet, export the same JVM flags through your service unit, container environment, or startup script:

bash
export JAVA_TOOL_OPTIONS="-Djavax.net.ssl.trustStore=/etc/ssl/trust/trust.p12 -Djavax.net.ssl.trustStorePassword=changeit -Djavax.net.ssl.trustStoreType=PKCS12"

For Spring Boot 3.1+, prefer SSL bundles when configuring client-side trust for supported clients:

properties
spring.ssl.bundle.jks.internal.truststore.location=classpath:trust.p12
spring.ssl.bundle.jks.internal.truststore.password=changeit
spring.ssl.bundle.jks.internal.truststore.type=PKCS12

Define the bundle first, then attach it to the specific client bean that makes the HTTPS call — a global property alone does not always propagate to every HTTP client.

Do not assume server.ssl.trust-store fixes outbound calls. server.ssl.* belongs to embedded server SSL configuration and is mainly relevant for server-side TLS or client-auth/mTLS.

Containers and systemd

In Docker or systemd services, mount trust.p12 read-only at a fixed path and inject the same three JVM properties through the environment so every Java child process inherits them:

bash
export JAVA_TOOL_OPTIONS="-Djavax.net.ssl.trustStore=/etc/ssl/trust/trust.p12 -Djavax.net.ssl.trustStorePassword=changeit -Djavax.net.ssl.trustStoreType=PKCS12"

Pair this with a secret manager or restricted env file for the password in production — see Add Java truststore in Docker and Kubernetes for image and initContainer patterns.


Verify which truststore Java loaded

If PKIX still fails after you created trust.p12, the JVM may not be loading the file you think it is. Enable JSSE trust manager tracing before changing CA imports again. Oracle’s JSSE Reference Guide documents javax.net.debug:

bash
java \
  -Djavax.net.debug=ssl,handshake,trustmanager \
  -Djavax.net.ssl.trustStore=trust.p12 \
  -Djavax.net.ssl.trustStorePassword=changeit \
  -Djavax.net.ssl.trustStoreType=PKCS12 \
  TlsClient default

Scroll the stderr output for trustmanager lines. You want to see anchors loaded from your trust.p12 path, not only the default cacerts location. If the trace shows an empty truststore, recheck the file path, permissions, and password — a typo there reproduces the same PKIX error even when the CA file itself is correct.


Do not disable TLS validation

Stack Overflow and forum threads around this error often suggest trust-all X509TrustManager implementations, hostname verifier bypass, or curl -k to “prove connectivity.” Those approaches hide PKIX failures by turning verification off.

Avoid them in production. They leave you open to person-in-the-middle attacks and make the next certificate rotation harder to reason about. The durable fix is to import the issuing CA into a proper truststore and point the JVM or framework client at it.


Troubleshooting

Use this table when the lab succeeded but your production app still throws PKIX, or when import commands completed without obvious errors:

Problem Fix
Still PKIX after import Import the missing intermediate, not just the root. Inspect the server chain with openssl s_client -connect host:443 -servername host -showcerts </dev/null.
Public HTTPS breaks after setting custom truststore Your custom file replaced cacerts
Wrong trustStore path Java may use an empty truststore instead of falling back to cacerts. Use an absolute path and confirm the file exists.
Wrong password Default lab password is changeit; match -storepass and trustStorePassword.
Relative path fails Use absolute paths in -Djavax.net.ssl.trustStore.
Multiple JDKs installed Each JDK has its own cacerts. Your custom truststore is JDK-independent — point all apps at the same file.
Proxy intercepts TLS Corporate HTTPS proxies need their CA in the truststore too.

If keytool -importcert fails with Public keys in reply and keystore don't match, you imported a signed certificate onto the wrong alias — see Fix keytool public keys mismatch.


References


Summary

PKIX path building failed means the JVM truststore lacks a CA anchor for the server certificate chain. Import your private root and intermediate CA certificates into a PKCS12 truststore with keytool -importcert, then set -Djavax.net.ssl.trustStore=trust.p12, -Djavax.net.ssl.trustStorePassword=changeit, and -Djavax.net.ssl.trustStoreType=PKCS12. Avoid editing global cacerts when a per-application truststore keeps upgrades safe and isolates trust to the services that need it.


Frequently Asked Questions

1. What causes Java PKIX path building failed?

The server certificate chains to a CA that is not in the JVM default truststore (cacerts). Common with internal HTTPS APIs, lab TLS, or corporate CAs. Java cannot build a trusted path from the server leaf to a root anchor it knows.

2. How do I fix PKIX path building failed without editing cacerts?

Import the signing CA and any missing intermediate CA into a custom PKCS12 truststore with keytool -importcert. Then start the JVM with -Djavax.net.ssl.trustStore, -Djavax.net.ssl.trustStorePassword, and -Djavax.net.ssl.trustStoreType=PKCS12. For Spring Boot 3.1+, prefer SSL bundles for client-side trust, or configure a dedicated SSLContext for the HTTP or JDBC client.

3. Do I need both root and intermediate CA in the truststore?

Java needs a trusted anchor and a complete certificate path. Usually the truststore contains the root CA while the server sends the intermediate chain. Import the intermediate too when the server omits it or when you want a self-contained lab truststore. Missing any link in the path causes the same PKIX error.

4. Why does curl -k work but Java fails?

curl -k disables certificate verification. Java HttpClient and most JVM TLS stacks validate the full chain against cacerts or your custom truststore by default. The server cert is fine; the trust anchor is missing on the Java side.

5. Where is the default Java truststore?

On Ubuntu and Debian OpenJDK, $JAVA_HOME/lib/security/cacerts is usually a symlink to /etc/ssl/certs/java/cacerts with default password changeit. On RHEL and Rocky Linux the canonical file is /etc/pki/java/cacerts. See java-custom-truststore-vs-cacerts for when to avoid editing it.

6. Can I pass the truststore only at runtime?

Yes. java -Djavax.net.ssl.trustStore=trust.p12 -Djavax.net.ssl.trustStorePassword=changeit -Djavax.net.ssl.trustStoreType=PKCS12 -jar app.jar works for any JVM app. Container images often mount trust.p12 and set JAVA_TOOL_OPTIONS with the same flags.
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 …