Fixing PKIX path building failed: unable to find valid certification path in Java

intermediate๐Ÿ”’ SSL/TLS2026-05-20| Java 8/11/17/21, any OS (Linux/macOS/Windows), when making HTTPS connections to servers with self-signed, internal CA, or expired certificates

Error Message

sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
#java#ssl#tls#keystore#certificate#https

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 -import to 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 renew hooks, Kubernetes cert-manager, or at minimum a cron job that alerts 30 days before expiry.

Related Error Notes