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:
javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested targetOlder 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).
opensslfor the lab CA chain (optional if you already have.crtfiles 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:
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')<(...) 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:
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.crtBefore blaming Java, confirm OpenSSL itself trusts the chain from root through intermediate to the server leaf:
openssl verify -CAfile root-ca.crt -untrusted intermediate-ca.crt server.crtWhen the chain is wired correctly, OpenSSL prints:
server.crt: OKIf 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:
keytool -list -cacerts -storepass changeit -alias labroot 2>&1 | head -2The alias is missing from cacerts, which is what we expect:
keytool error: java.lang.Exception: Alias <labroot> does not existReproduce 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:
java TlsClient defaultThe TLS handshake fails before any HTTP response because the private lab CA is not in cacerts:
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 targetThat 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:
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 -nopromptList entries to confirm both imports are trustedCertEntry and that no PrivateKeyEntry slipped in:
keytool -list -keystore trust.p12 -storepass changeitA healthy client truststore for this lab looks like this:
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.
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:
java \
-Djavax.net.ssl.trustStore=trust.p12 \
-Djavax.net.ssl.trustStorePassword=changeit \
-Djavax.net.ssl.trustStoreType=PKCS12 \
-jar your-app.jarUse 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:
java TlsClient custom trust.p12The handshake succeeds and the client prints the server response (same format as Java keystore vs truststore TlsClient):
HTTP status: 200
Body: OK from Java TLS serverJVM-property truststore loading leaves the client code on the default SSLContext path but overrides trust at the JVM level:
java \
-Djavax.net.ssl.trustStore=trust.p12 \
-Djavax.net.ssl.trustStorePassword=changeit \
-Djavax.net.ssl.trustStoreType=PKCS12 \
TlsClient defaultSame successful outcome — PKIX can now anchor the chain to labroot or labintermediate:
HTTP status: 200
Body: OK from Java TLS serverSpring 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:
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:
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=PKCS12Define 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:
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:
java \
-Djavax.net.debug=ssl,handshake,trustmanager \
-Djavax.net.ssl.trustStore=trust.p12 \
-Djavax.net.ssl.trustStorePassword=changeit \
-Djavax.net.ssl.trustStoreType=PKCS12 \
TlsClient defaultScroll 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
- keytool documentation (Oracle)
- Java Secure Socket Extension (JSSE) Reference Guide
- Spring Boot SSL bundles
- Install keytool on Ubuntu
- Java keystore vs truststore
- Use custom truststore instead of cacerts
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.

