Skip to content
ecdsa.com
← All articles

PKIX path building failed in Java: causes and real fixes

Java does not trust what your browser trusts. It uses its own trust store, does not fetch missing intermediates by default, and says the same thing for every failure. Each cause below was reproduced on JDK 25, with the command that tells you which one you have.

Short answer. PKIX path building failed: ... unable to find valid certification path to requested target means the JVM could not build a chain from the server's certificate to any trusted root in the trust store it actually loaded. That is usually $JAVA_HOME/lib/security/cacerts, not the operating system's store. The usual causes are a private or proxy CA that is not in that file, a server that does not send its intermediate certificate, a JVM using a different trust store than you think, an old JDK without a newer root, and a missing SNI name. Find out which with openssl s_client -showcerts and -Djavax.net.debug=ssl:handshake:verbose. Then fix the trust store or the server. Never install a trust manager that accepts everything.

What the message means

On current JDKs the full exception looks like this. JDK 21 and 25 prefix the TLS alert name:

JDK 25.0.4.1, HttpClient against a server with a private CA
javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

The pieces:

  • PKIX is the X.509 certificate path model from RFC 5280. Java's implementation lives in sun.security.provider.certpath.
  • Path building is the first of two steps. Java tries to find some sequence of certificates from the server's leaf up to a trust anchor. Only if that succeeds does path validation check dates, key usage, constraints and revocation. Failures in the second step have their own messages, for example PKIX path validation failed: validity check failed.
  • "requested target" is the server's certificate.

Frameworks wrap this exception, for example Spring's ResourceAccessException, Maven's "Could not transfer artifact" or Gradle's "Could not GET". The PKIX text is still in the cause chain, so search for it in the full stack trace.

The five causes

All five were reproduced with eclipse-temurin:25.0.4.1_1-jdk-noble and a small java.net.http.HttpClient program. The test PKI has a root, an intermediate ("Issuing CA 1") and a leaf for api.example.test.

#SituationResult
1Private CA, default JDK cacertsPKIX path building failed
2Root imported into a copy of cacerts, server sends leaf and intermediateHTTP 200
3Same trust store, server sends only the leafPKIX path building failed
4Same as 3, -Dcom.sun.security.enableAIAcaIssuers=truePKIX path building failed
4bSame as 4, plus -Dcom.sun.security.allowedAIALocations=http://aia.example.test:8080/HTTP 200
5Server picks the certificate by SNI, client sends SNIHTTP 200
6Same server, -Djsse.enableSNIExtension=falsePKIX path building failed
7Same server, URL with an IP address (no SNI)PKIX path building failed

1. The certificate comes from a CA Java does not trust

This is the most common case: internal services, Kubernetes ingresses, Nexus or Artifactory with company certificates, databases, and every site seen through a TLS-inspecting proxy, which re-signs traffic with its own root. Installing that root in Windows or macOS makes browsers happy, but on most platforms Java does not read the OS store. The JDK's cacerts (118 entries in this Temurin 25 build) has no idea who "Example Corp Root CA" is.

2. The server does not send its intermediate

A server must send its leaf certificate and the intermediates up to, but not including, the root. Many servers are configured with only the leaf. Browsers often hide the mistake, because they cache intermediates or fetch them from the URL in the certificate's Authority Information Access (AIA) extension. Java does neither by default. Rows 3 and 4 of the table are that situation. The root is trusted, but the path cannot be built because Java never sees "Issuing CA 1".

You can see what a server sends in two lines:

openssl s_client -showcerts: the server on port 8443 sends the chain, 8444 only the leaf
$ openssl s_client -connect api.example.test:8443 -servername api.example.test -showcerts </dev/null 2>/dev/null | grep -E '^ *[0-9]+ s:|^ *i:'
 0 s:CN = api.example.test
   i:CN = Example Corp Issuing CA 1
 1 s:CN = Example Corp Issuing CA 1
   i:CN = Example Corp Root CA
$ openssl s_client -connect api.example.test:8444 -servername api.example.test -showcerts </dev/null 2>/dev/null | grep -E '^ *[0-9]+ s:|^ *i:'
 0 s:CN = api.example.test
   i:CN = Example Corp Issuing CA 1

If the last i: line names something other than a root you trust, an intermediate is missing. The certificate chain builder takes a pasted bundle, puts it in order, fetches missing intermediates through AIA, and gives you the exact chain the server should send.

3. The JVM uses a different trust store than you think

You imported the root, and it still fails. Common reasons:

  • Another JDK. IDEs, Gradle toolchains, Maven wrappers, application servers and Docker images bring their own JDK. keytool -cacerts edits the trust store of the JDK that runs keytool, and only that one.
  • A replaced trust store. -Djavax.net.ssl.trustStore=only-corp.p12 replaces cacerts, it does not add to it. A store that holds only the company root breaks every public site:
JDK 25: a trust store with only the company root, connecting to letsencrypt.org
javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
  • A typo in the trust store path. If the file in javax.net.ssl.trustStore does not exist, JDK 25 does not fail. It silently falls back to cacerts, so your root is simply not there. Only the trust manager debug output says so:
JDK 25: -Djavax.net.ssl.trustStore=/tmp/app-trustore.p12 (a typo), -Djavax.net.debug=ssl:trustmanager
Inaccessible trust store: /tmp/app-trustore.p12
trustStore is: /opt/java/openjdk/lib/security/cacerts
Fatal (CERTIFICATE_UNKNOWN): PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target (

A store that exists but holds no trusted certificates fails differently, with the trustAnchors parameter must be non-empty.

  • A framework-level store. Spring Boot SSL bundles, Tomcat connectors, Kafka and database drivers have their own trust store settings, and they win over the JVM defaults for their connections.

The first line of the JSSE debug output settles which file was loaded (see diagnosis).

4. The JDK is too old for the server's root

Each JDK update carries a snapshot of trusted roots. A public site whose chain ends at a newer root fails on a JDK that predates it. Let's Encrypt's compatibility page gives an example: ISRG Root X2 is trusted from Java 8u401, 11.0.22, 17.0.10 and 21.0.2. Removing two roots from a copy of cacerts shows what an outdated store does:

JDK 25 with ISRG Root X1 and X2 deleted from a cacerts copy
--- letsencrypt.org (chains to ISRG roots)
javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
--- www.google.com (different CA)
HTTP 200

If only some public sites fail, look at the root they chain to, and then at your JDK's update level. The same happens to a trust store that was copied from cacerts years ago and never refreshed.

5. No SNI, so the server sends a different certificate

A server that hosts several names picks the certificate by the Server Name Indication (SNI) in the ClientHello. Without SNI it sends a default certificate, which in the test was a self-signed default.invalid. That certificate chains to nothing, so the error is PKIX again (rows 6 and 7). Java sends no SNI when:

  • the URL uses an IP address (TLS forbids IP literals in SNI),
  • -Djsse.enableSNIExtension=false is set, often left over from an old workaround,
  • a custom SSLSocketFactory creates sockets without a host name.

openssl s_client without -servername shows what such a client receives.

Diagnosis, step by step

1. Look at the chain the server sends, from the machine that fails if you can. A proxy may serve that machine a different certificate:

What the server sends, and who signed each certificate
openssl s_client -connect host.example:443 -servername host.example -showcerts </dev/null 2>/dev/null \
  | grep -E '^ *[0-9]+ s:|^ *i:'

Through an inspecting proxy, the top issuer is the proxy's CA, not a public one. That alone explains the error.

2. Ask Java which trust store it loaded and what it received. The debug output is long, about 5,000 lines for one failed handshake in our test, so filter it:

JSSE debug, filtered
java -Djavax.net.debug=ssl:handshake:verbose -jar app.jar 2>&1 \
  | grep -E 'trustStore is|Consuming server Certificate|"subject"|"issuer"|PKIX'
Key lines from the JSSE debug output (JDK 25, private CA, abridged)
trustStore is: /opt/java/openjdk/lib/security/cacerts
...
javax.net.ssl ... Consuming server Certificate handshake message (
"issuer"             : "CN=Example Corp Issuing CA 1",
"subject"            : "CN=api.example.test",
...
"issuer"             : "CN=Example Corp Root CA",
"subject"            : "CN=Example Corp Issuing CA 1",
...
javax.net.ssl ... Fatal (CERTIFICATE_UNKNOWN): PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target (

The first line names the trust store file. The certificate blocks show the chain the server sent, leaf first; the issuer of the last one is the certificate Java could not connect to a root. If that issuer is a root or intermediate you expected to trust, check that exact file:

keytool: is the root in the store the JVM loaded?
$ keytool -list -cacerts -storepass changeit -alias corp-root
keytool error: java.lang.Exception: Alias <corp-root> does not exist

For a different file, use keytool -list -keystore /path/to/store.p12. -v prints the subject and fingerprint of each entry, so you can match the certificate rather than the alias.

3. For path building details, add -Djava.security.debug=certpath. It logs each candidate issuer the builder considered and, for AIA, whether a fetch was allowed.

Fixes that keep verification on

Fix the server first when an intermediate is missing. Configure the full chain (leaf, then intermediates) in nginx, Apache, the load balancer or the ingress. Every client benefits, not just Java.

Use an application trust store that adds your root when the CA is private or a proxy re-signs traffic. Start from a copy of cacerts, so public sites keep working:

An application trust store: JDK roots plus the company root
cp "$JAVA_HOME/lib/security/cacerts" app-truststore.p12
keytool -importcert -noprompt -alias corp-root -file corp-root-ca.pem \
  -keystore app-truststore.p12 -storepass changeit
java -Djavax.net.ssl.trustStore=app-truststore.p12 -Djavax.net.ssl.trustStorePassword=changeit -jar app.jar

For tools you don't start yourself, such as Maven, Gradle or IDE-run tests, pass the same properties through JAVA_TOOL_OPTIONS or the tool's own JVM options. keytool -importcert -cacerts changes the JDK's default store instead. That reaches every application on the JDK, but it disappears with the next JDK upgrade.

Give one client its own trust store when only one integration needs the extra root. This version was run against the private service and a public site:

Java: an SSLContext for one HttpClient
KeyStore ts = KeyStore.getInstance("PKCS12");
try (InputStream in = Files.newInputStream(Path.of("app-truststore.p12"))) {
    ts.load(in, "changeit".toCharArray());
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ts);
SSLContext ssl = SSLContext.getInstance("TLS");
ssl.init(null, tmf.getTrustManagers(), null);
HttpClient client = HttpClient.newBuilder().sslContext(ssl).build();
// https://api.example.test:8443/ -> HTTP 200
// https://letsencrypt.org/ -> HTTP 200

OkHttp, Apache HttpClient, Netty, gRPC and JDBC drivers all accept an SSLContext or a TrustManager built the same way.

In containers, Eclipse Temurin images import extra CAs at start-up when USE_SYSTEM_CA_CERTS is set and *.crt files are mounted at /certificates/:

docker run -e USE_SYSTEM_CA_CERTS=1 -v ./certs:/certificates/ eclipse-temurin:25.0.4.1_1-jdk-noble (abridged)
Adding certificate with alias Example Corp Root CA to the JVM truststore
Your keystore contains 240 entries

This only runs through the image's default entrypoint. With other base images, run keytool -importcert in the Dockerfile.

On macOS with JDK 23 or later, -Djavax.net.ssl.trustStoreType=KeychainStore-ROOT makes Java use the system root keychain, so a root installed by your IT department is trusted (JDK-8320362). On Windows, -Djavax.net.ssl.trustStoreType=Windows-ROOT does the same with the Windows root store.

Upgrade the JDK when a public root is missing. Patching cacerts by hand on an old JDK leaves the rest of its security fixes behind.

Why "trust all certificates" is not a fix

The most popular answer online is an X509TrustManager whose checkServerTrusted does nothing, often with a HostnameVerifier that returns true. It makes the exception go away by turning TLS into encryption with nobody on the other end verified. Anyone who can intercept the connection can present any certificate, read the traffic and change it: a hostile Wi-Fi network, a compromised router, a misconfigured proxy, a DNS hijack. The code also tends to outlive its "temporary" purpose and gets copied into the next project. Each fix above takes a few minutes and keeps verification on.

The same goes for importing the server's own certificate as a trust anchor. It works until the next renewal, which with shorter certificate lifetimes means within weeks. Trust the CA that issues the certificates, not the certificate.

Checklist

  1. openssl s_client -showcerts from the failing host: is the chain complete, and who is the top issuer?
  2. -Djavax.net.debug=ssl:handshake:verbose: which trustStore is: file did the JVM load?
  3. keytool -list on that file: is the issuing root there?
  4. Is a TLS-inspecting proxy in the path? Then its root is what you need.
  5. Is SNI sent? Check the IP literals, jsse.enableSNIExtension and custom socket factories.
  6. Is the JDK current enough for the site's root?

Sources

  • RFC 5280: certification path validation (§6).
  • Oracle, JDK 21.0.10 release notes: filter rules for URIs in the Authority Info Access extension (com.sun.security.allowedAIALocations).
  • The java.security file shipped with JDK 25 ($JAVA_HOME/conf/security/java.security), section "X.509 AuthorityInfoAccess caIssuer URI Filtering".
  • JDK-8320362: Load anchor certificates from Keychain keystore (KeychainStore-ROOT, JDK 23).
  • Let's Encrypt, Certificate compatibility.
  • Eclipse Temurin container images: the USE_SYSTEM_CA_CERTS entrypoint option.

Every result on this page was produced on 20 October 2026 in eclipse-temurin:25.0.4.1_1-jdk-noble (OpenJDK 25.0.4.1, OpenSSL 3.0.13 for the test servers), with test certificates generated in the container.