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:
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.
| # | Situation | Result |
|---|---|---|
| 1 | Private CA, default JDK cacerts | PKIX path building failed |
| 2 | Root imported into a copy of cacerts, server sends leaf and intermediate | HTTP 200 |
| 3 | Same trust store, server sends only the leaf | PKIX path building failed |
| 4 | Same as 3, -Dcom.sun.security.enableAIAcaIssuers=true | PKIX path building failed |
| 4b | Same as 4, plus -Dcom.sun.security.allowedAIALocations=http://aia.example.test:8080/ | HTTP 200 |
| 5 | Server picks the certificate by SNI, client sends SNI | HTTP 200 |
| 6 | Same server, -Djsse.enableSNIExtension=false | PKIX path building failed |
| 7 | Same 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 -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 -cacertsedits the trust store of the JDK that runskeytool, and only that one. - A replaced trust store.
-Djavax.net.ssl.trustStore=only-corp.p12replacescacerts, it does not add to it. A store that holds only the company root breaks every public site:
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.trustStoredoes not exist, JDK 25 does not fail. It silently falls back tocacerts, so your root is simply not there. Only the trust manager debug output says so:
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:
--- 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=falseis set, often left over from an old workaround,- a custom
SSLSocketFactorycreates 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:
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:
java -Djavax.net.debug=ssl:handshake:verbose -jar app.jar 2>&1 \
| grep -E 'trustStore is|Consuming server Certificate|"subject"|"issuer"|PKIX'
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 -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:
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:
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/:
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
openssl s_client -showcertsfrom the failing host: is the chain complete, and who is the top issuer?-Djavax.net.debug=ssl:handshake:verbose: whichtrustStore is:file did the JVM load?keytool -liston that file: is the issuing root there?- Is a TLS-inspecting proxy in the path? Then its root is what you need.
- Is SNI sent? Check the IP literals,
jsse.enableSNIExtensionand custom socket factories. - 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.securityfile 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_CERTSentrypoint 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.