Introduction

Java's SSL implementation validates server certificates against a truststore (by default $JAVA_HOME/lib/security/cacerts). When the server presents a certificate signed by an authority not in the truststore, the handshake fails with PKIX path building failed. This commonly happens with self-signed certificates, new CAs not yet in the JDK, or corporate MITM proxies.

Symptoms

  • javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
  • sun.security.validator.ValidatorException: PKIX path building failed
  • Works with curl/browser but fails in Java
  • Maven/Gradle cannot download dependencies from HTTPS repositories
bash
Exception in thread "main" javax.net.ssl.SSLHandshakeException:
PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested target
    at sun.security.ssl.Alert.createSSLException(Alert.java:131)
    at sun.security.ssl.TransportContext.fatal(TransportContext.java:369)
    at sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCertificates(CertificateMessage.java:1010)

Common Causes

  • Self-signed certificate not imported into truststore
  • Corporate MITM proxy using internal CA
  • JDK cacerts missing newer Let's Encrypt R3 certificate
  • Server not sending intermediate certificates in chain
  • Custom JVM not bundling standard cacerts

Step-by-Step Fix

  1. 1.Diagnose which certificate is missing:
  2. 2.```bash
  3. 3.# Use InstallCert to identify the missing certificate
  4. 4.java -Djavax.net.debug=ssl,handshake -jar InstallCert.jar api.example.com:443

# Or use openssl to inspect the chain openssl s_client -connect api.example.com:443 -showcerts # Look for the issuer of each certificate in the chain ```

  1. 1.Import certificate into JVM truststore:
  2. 2.```bash
  3. 3.# Download the server certificate
  4. 4.echo | openssl s_client -connect api.example.com:443 -showcerts 2>/dev/null | \
  5. 5.sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p' > server.pem

# Import into cacerts (default password: changeit) sudo keytool -importcert \ -alias api-example-com \ -file server.pem \ -keystore $JAVA_HOME/lib/security/cacerts \ -storepass changeit \ -noprompt

# Verify import keytool -list -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit | \ grep api-example-com ```

  1. 1.Use a custom truststore without modifying cacerts:
  2. 2.```java
  3. 3.import java.io.FileInputStream;
  4. 4.import java.security.KeyStore;
  5. 5.import javax.net.ssl.SSLContext;
  6. 6.import javax.net.ssl.TrustManagerFactory;

// Load custom truststore KeyStore trustStore = KeyStore.getInstance("JKS"); try (FileInputStream fis = new FileInputStream("/path/to/truststore.jks")) { trustStore.load(fis, "truststore-password".toCharArray()); }

TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(trustStore);

SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, tmf.getTrustManagers(), null);

// Use with HTTP client HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()); ```

  1. 1.For Maven/Gradle build failures:
  2. 2.```bash
  3. 3.# Set truststore for Maven
  4. 4.export MAVEN_OPTS="-Djavax.net.ssl.trustStore=/path/to/truststore.jks -Djavax.net.ssl.trustStorePassword=changeit"
  5. 5.mvn clean install

# For Gradle export GRADLE_OPTS="-Djavax.net.ssl.trustStore=/path/to/truststore.jks" gradle build

# Or in ~/.m2/settings.xml <server> <id>repo</id> <configuration> <sslTrustStore>/path/to/truststore.jks</sslTrustStore> </configuration> </server> ```

  1. 1.Corporate proxy: import CA certificate:
  2. 2.```bash
  3. 3.# Export the corporate CA from browser or IT department
  4. 4.# Import into cacerts
  5. 5.sudo keytool -importcert \
  6. 6.-alias corporate-ca \
  7. 7.-file corporate-ca.crt \
  8. 8.-keystore $JAVA_HOME/lib/security/cacerts \
  9. 9.-storepass changeit
  10. 10.`

Prevention

  • Keep JDK updated - newer versions include updated CA certificates
  • Use a custom truststore file rather than modifying cacerts
  • Set javax.net.ssl.trustStore as a JVM argument in deployment scripts
  • For CI/CD, include truststore setup in Dockerfile:
  • ```dockerfile
  • COPY corporate-ca.crt /tmp/
  • RUN keytool -importcert -alias corp-ca -file /tmp/corporate-ca.crt \
  • -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit -noprompt
  • `
  • Test SSL connections in CI to detect certificate issues before deployment
  • Monitor SSL handshake failures in application logs