Fix javax.net.ssl.SSLHandshakeException: PKIX path building failed in Java HTTPS Calls

intermediateโ˜• Java2026-03-24| Java 8โ€“21, any OS (Linux/macOS/Windows), any HTTPS client (HttpURLConnection, OkHttp, RestTemplate, Apache HttpClient)

Error Message

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
#java#ssl#https#certificate#truststore#pkix

The Situation

It's 2 AM. Your service just started throwing this in production:

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

The endpoint worked fine an hour ago. Nothing changed in your code. But something did change โ€” the remote server may have rotated its certificate, your JDK got updated, or you just deployed to a new server that's missing a CA cert.

Here's how to diagnose and fix it fast.

What's Actually Happening

Java's SSL engine tries to verify the remote server's certificate chain. It walks up that chain looking for a trusted root CA in its truststore ($JAVA_HOME/lib/security/cacerts). No match found โ€” it throws PKIX path building failed.

The usual suspects:

  • The server uses a certificate signed by a private/internal CA not in Java's default truststore
  • The server is using a self-signed certificate
  • The server's certificate chain is incomplete (missing intermediate CA)
  • Your JDK is outdated and missing newer root CAs โ€” Let's Encrypt's ISRG Root X1, for example, wasn't in Java's truststore until 8u291
  • A corporate proxy is doing SSL inspection, intercepting traffic and re-signing with its own cert

Step 1: Identify Which Certificate Is Missing

Start by checking what the server actually sends:

# Check the server's certificate chain
openssl s_client -connect your-api.example.com:443 -showcerts 2>/dev/null | openssl x509 -noout -issuer -subject

# Or see the full chain
openssl s_client -connect your-api.example.com:443 -showcerts 2>/dev/null

Look for Verify return code: 0 (ok). If OpenSSL verifies fine but Java doesn't, the problem is squarely in Java's truststore โ€” not the server.

Check whether Java already has the CA:

keytool -list -cacerts -storepass changeit | grep -i "issuer-name"

Step 2: Quick Fix โ€” Import the Certificate

Download the server's certificate (or the CA cert that signed it):

# Download the server cert
openssl s_client -connect your-api.example.com:443 -showcerts 2>/dev/null < /dev/null \
  | openssl x509 -outform PEM > server.crt

# If it's a chain, save all certs from the output manually
# Each -----BEGIN CERTIFICATE----- block is one cert

Import it into Java's truststore:

# Find your JAVA_HOME
java -XshowSettings:all -version 2>&1 | grep java.home

# Import the cert (run as root/admin if needed)
keytool -import -alias my-server-cert \
  -keystore $JAVA_HOME/lib/security/cacerts \
  -storepass changeit \
  -file server.crt \
  -noprompt

Restart your application. The error should be gone.

Path note: On Java 9+ the truststore lives at $JAVA_HOME/lib/security/cacerts. On Java 8 it's one level deeper: $JAVA_HOME/jre/lib/security/cacerts. Java 9+ can also inherit from the OS truststore depending on configuration.

Step 3: Permanent Fix โ€” Bundle a Custom Truststore

Importing into the system JDK truststore works, but JDK updates can wipe it. A safer approach: bundle the CA cert with your app and point the JVM at your own truststore.

Create a custom truststore

# Start from a copy of the default cacerts
cp $JAVA_HOME/lib/security/cacerts my-app-truststore.jks

# Add your CA cert to it
keytool -import -alias internal-ca \
  -keystore my-app-truststore.jks \
  -storepass changeit \
  -file internal-ca.crt \
  -noprompt

Point your JVM to it at startup

java -Djavax.net.ssl.trustStore=/path/to/my-app-truststore.jks \
     -Djavax.net.ssl.trustStorePassword=changeit \
     -jar your-app.jar

Or configure it in code (Spring Boot / programmatic)

System.setProperty("javax.net.ssl.trustStore", "/path/to/my-app-truststore.jks");
System.setProperty("javax.net.ssl.trustStorePassword", "changeit");

Place this before any HTTPS connection is made โ€” ideally at app startup, before the Spring context loads.

Special Case: Corporate SSL Inspection Proxy

On a corporate network, the proxy may intercept HTTPS traffic and re-sign responses with its own CA. The certificate your app receives is issued by something like "Zscaler" or your company name โ€” not the actual remote CA.

# Check who issued the cert you're actually seeing
openssl s_client -connect your-api.example.com:443 2>/dev/null | grep "issuer"

See your corporate proxy as the issuer? Ask IT for the proxy's root CA certificate and import it using the steps above.

Special Case: Let's Encrypt on Older JDK

ISRG Root X1 โ€” Let's Encrypt's root CA โ€” landed in Java's truststore with 8u291 and 11.0.11. Older JDKs simply don't have it.

# Check your JDK version
java -version

# Check if ISRG Root X1 is present
keytool -list -cacerts -storepass changeit | grep -i "isrg"

Update the JDK โ€” that's the cleanest fix. If an update isn't on the table right now, download ISRG Root X1 from letsencrypt.org and import it manually.

Verify the Fix

# Use Java's SSL debug output to confirm the handshake succeeds
java -Djavax.net.debug=ssl:handshake -jar your-app.jar 2>&1 | grep -E "(Certificate chain|Finished|Exception)"

A successful handshake shows Finished with no exception. The debug output includes the full handshake log and which certificates were validated.

Quick smoke test without deploying:

curl -v https://your-api.example.com/health
# bypass SSL entirely just to confirm the endpoint is alive
wget --no-check-certificate https://your-api.example.com/health

What NOT to Do

Stack Overflow is full of answers suggesting this:

// DO NOT DO THIS IN PRODUCTION
TrustManager[] trustAllCerts = new TrustManager[] {
    new X509TrustManager() {
        public X509Certificate[] getAcceptedIssuers() { return null; }
        public void checkClientTrusted(X509Certificate[] certs, String authType) {}
        public void checkServerTrusted(X509Certificate[] certs, String authType) {}
    }
};
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

This disables all certificate validation. Every connection becomes vulnerable to MITM attacks โ€” silently, with zero warnings. Fine for a 10-minute local debug session. Never acceptable in any deployed environment.

Tips

When downloading CA certs from the internet, verify the file before importing it. A corrupted or tampered cert will either silently fail or, worse, import something you didn't intend. ToolCraft's Hash Generator runs entirely in the browser โ€” paste your cert content, get a SHA-256 hash, compare it against the official source. No upload, no server involved.

Also worth wiring into your CI/CD pipeline: a keytool -list check that confirms your custom truststore contains all expected aliases before a deploy goes out. Beats finding a missing cert at 2 AM.

Related Error Notes