The Error
You're making an HTTPS call โ a REST client, HttpURLConnection, OkHttp, Spring's RestTemplate, whatever โ and Java throws this:
sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
The service is up. The URL opens fine in a browser. But Java refuses to connect. Here's exactly why, and how to fix it.
Root Cause
Java keeps its own truststore โ a file called cacerts โ completely separate from your OS certificate store. It ships with roughly 150 trusted root CAs out of the box. When you connect to an HTTPS endpoint, Java walks the server's certificate chain and tries to match it against one of those roots. No match? Exception.
Three situations trigger this:
- The server uses a self-signed certificate not in Java's truststore
- The server uses an internal/corporate CA that Java doesn't know about
- The server is missing intermediate certificates in its chain
Fix 1: Import the Certificate into Java's Truststore (Correct Fix)
This is the right call for production. You add the server's certificate โ or its CA root โ to Java's cacerts file directly.
Step 1: Get the Certificate
Pull the certificate off the server with openssl:
openssl s_client -connect your-host.example.com:443 -showcerts </dev/null 2>/dev/null \
| openssl x509 -outform PEM > server-cert.pem
If the server uses a chain, you want the root or intermediate CA cert โ not just the leaf. Inspect the full chain first:
openssl s_client -connect your-host.example.com:443 -showcerts </dev/null 2>/dev/null
You'll see one or more -----BEGIN CERTIFICATE----- blocks. The last block is typically the root CA โ that's what you import.
Step 2: Find Your JVM's cacerts
# Linux / macOS
java -XshowSettings:all -version 2>&1 | grep java.home
# Then look for: $JAVA_HOME/lib/security/cacerts
# Or older JDKs: $JAVA_HOME/jre/lib/security/cacerts
# Quick find
find /usr/lib/jvm -name cacerts 2>/dev/null
Step 3: Import the Certificate
keytool -import \
-alias my-server-cert \
-file server-cert.pem \
-keystore /path/to/jdk/lib/security/cacerts \
-storepass changeit
The default truststore password is changeit. When prompted "Trust this certificate?" โ type yes.
Step 4: Verify
keytool -list -keystore /path/to/jdk/lib/security/cacerts \
-storepass changeit | grep my-server-cert
Restart your application. The PKIX error should be gone.
Fix 2: Use a Custom Truststore for Your Application
No root access? Shared JVM you can't touch? Create a separate truststore and point your app at it.
# Create a new truststore and import the cert
keytool -import \
-alias my-server-cert \
-file server-cert.pem \
-keystore /opt/myapp/custom-truststore.jks \
-storepass mypassword
# Run your app with JVM flags pointing to it
java -Djavax.net.ssl.trustStore=/opt/myapp/custom-truststore.jks \
-Djavax.net.ssl.trustStorePassword=mypassword \
-jar your-app.jar
You can also set this programmatically at startup:
System.setProperty("javax.net.ssl.trustStore", "/opt/myapp/custom-truststore.jks");
System.setProperty("javax.net.ssl.trustStorePassword", "mypassword");
Fix 3: Merge cacerts with a Custom Truststore
Sometimes you need both: all the default Java CAs and your own internal CA. Start by copying cacerts, then import your cert into the copy:
cp $JAVA_HOME/lib/security/cacerts /opt/myapp/custom-truststore.jks
keytool -import \
-alias my-server-cert \
-file server-cert.pem \
-keystore /opt/myapp/custom-truststore.jks \
-storepass changeit
Then wire it up using the JVM flags from Fix 2. Your app gets all 150+ default trusted CAs plus your custom one.
Fix 4: Debug SSL to Pinpoint What's Missing
Don't guess. Before importing anything, turn on SSL debug output to see the exact failure point:
java -Djavax.net.debug=ssl:handshake -jar your-app.jar 2>&1 | head -100
Look for output like this:
%% No cached client session
*** ClientHello
...
Certificate chain
chain [0] = ...
chain [1] = ...
***
FATAL ALERT: certificate_unknown
The chain[N] entries tell you which certificates the server sent. Cross-reference them against your truststore to find the gap.
Fix 5: Disable SSL Verification (Only for Internal Dev/Test)
Testing against an internal service and just need it to unblock right now? This skips all certificate validation. Never ship this to production.
import javax.net.ssl.*;
import java.security.cert.X509Certificate;
public static void disableSSLVerification() throws Exception {
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("TLS");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);
}
Found something like this already committed in your codebase? Replace it with Fix 1 or 2. It's a security hole, not a workaround.
Docker / Container Environments
Containers bake the JVM's cacerts into the base image. Importing at runtime works, but building it into the image is cleaner and avoids per-startup overhead:
# Option A: Import at container startup in your entrypoint.sh
keytool -import -noprompt \
-alias internal-ca \
-file /certs/internal-ca.crt \
-keystore $JAVA_HOME/lib/security/cacerts \
-storepass changeit
# Option B: Build it into the image (preferred)
FROM eclipse-temurin:17
COPY internal-ca.crt /tmp/
RUN keytool -import -noprompt \
-alias internal-ca \
-file /tmp/internal-ca.crt \
-keystore $JAVA_HOME/lib/security/cacerts \
-storepass changeit
Prevention
- Control the server? Use a cert from a public CA โ Let's Encrypt is free and auto-renews. Or bake your internal CA root into your base Docker image so every container already trusts it.
- Running microservices? A service mesh like Istio or Linkerd can handle mTLS termination at the sidecar level, so your Java code never touches raw certs.
- Automate the cert import. Add
keytool -importto your Ansible playbooks, Terraform userdata scripts, or Dockerfiles โ otherwise it's a manual step that someone skips during the 2 AM incident. - Watch for expiry. A cert expiring mid-run produces this exact same error. Set up
certbot renewhooks, Kubernetes cert-manager, or at minimum a cron job that alerts 30 days before expiry.

