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 targetsun.security.validator.ValidatorException: PKIX path building failed- Works with curl/browser but fails in Java
- Maven/Gradle cannot download dependencies from HTTPS repositories
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.Diagnose which certificate is missing:
- 2.```bash
- 3.# Use InstallCert to identify the missing certificate
- 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.Import certificate into JVM truststore:
- 2.```bash
- 3.# Download the server certificate
- 4.echo | openssl s_client -connect api.example.com:443 -showcerts 2>/dev/null | \
- 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.Use a custom truststore without modifying cacerts:
- 2.```java
- 3.import java.io.FileInputStream;
- 4.import java.security.KeyStore;
- 5.import javax.net.ssl.SSLContext;
- 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.For Maven/Gradle build failures:
- 2.```bash
- 3.# Set truststore for Maven
- 4.export MAVEN_OPTS="-Djavax.net.ssl.trustStore=/path/to/truststore.jks -Djavax.net.ssl.trustStorePassword=changeit"
- 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.Corporate proxy: import CA certificate:
- 2.```bash
- 3.# Export the corporate CA from browser or IT department
- 4.# Import into cacerts
- 5.sudo keytool -importcert \
- 6.-alias corporate-ca \
- 7.-file corporate-ca.crt \
- 8.-keystore $JAVA_HOME/lib/security/cacerts \
- 9.-storepass changeit
- 10.
`
Prevention
- Keep JDK updated - newer versions include updated CA certificates
- Use a custom truststore file rather than modifying cacerts
- Set
javax.net.ssl.trustStoreas 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