PKIX path building failed

The Java error "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 up to a CA in its own trust store. Java does not read the operating system trust store — it reads `cacerts`, a keystore shipped inside the JDK — so a corporate root, a TLS-inspecting proxy or a private CA that the rest of the machine trusts is still unknown to Java until it is imported with `keytool`. The fix is to capture the CA certificate the server chains up to, import it into `cacerts` or into a dedicated truststore named by `-Djavax.net.ssl.trustStore`, and restart the JVM. The underlying check is the same one OpenSSL reports as "unable to get local issuer certificate".

Updated August 19, 2026 Verified against OpenSSL 3.5.7

What the error actually means #

PKIX path building is Java’s implementation of chain validation: start at the certificate the server presented, find the certificate that signed it, then the one that signed that, and keep going until you reach a certificate the JVM already trusts. That set of already-trusted certificates is the truststore.

The message appears at the moment the walk runs out of road. Java holds a certificate, knows the name of its issuer, and has no certificate for that issuer — not in the chain the server sent, and not in its truststore.

So the usual situation is not that something is broken — it is that a CA which is perfectly legitimate on your network, typically a corporate root or a TLS-inspecting proxy, was never taught to the JVM.

Step 1 — see what the server actually sends #

Before importing anything, find out which certificates are in play. The last certificate in this output is the highest CA the server offers, and it is the one whose issuer must be trusted:

Dump the chain the server presents
openssl s_client -connect repo.example.com:443 -servername repo.example.com -showcerts </dev/null

Read the subject and issuer of every certificate returned, top to bottom:

Just the subject/issuer pairs
openssl s_client -connect repo.example.com:443 -servername repo.example.com -showcerts </dev/null 2>/dev/null \
| grep -E "^ *[0-9]+ s:|^ *i:"
Expected output
0 s:CN=repo.example.com
i:C=US, O=Example Corp, CN=Example Corp Issuing CA
1 s:C=US, O=Example Corp, CN=Example Corp Issuing CA
i:C=US, O=Example Corp, CN=Example Corp Root CA

In that example the top certificate is issued by Example Corp Root CA, which the server does not send — correctly, since a root is supposed to come from the truststore. That root is what you need to import.

What the chain looks like What is missing What to import
Leaf + intermediates, top issued by a root you recognise The root is not in cacerts The root CA
Leaf only, public CA The server is not sending intermediates Fix the server; see unable to get local issuer certificate
Leaf only, self-signed (s: equals i:) Nothing to chain to The certificate itself, as a trust anchor
Issuer names you have never seen A TLS-inspecting proxy is rewriting the chain The proxy’s root CA

Step 2 — confirm the JVM does not already trust it #

Java 9 and later can list the active cacerts without you locating the file:

Search the JDK truststore
keytool -list -cacerts -storepass changeit | grep -i "example corp"

On Java 8, name the file explicitly:

keytool -list -keystore "$JAVA_HOME/jre/lib/security/cacerts" -storepass changeit \
| grep -i "example corp"

No output means the CA is genuinely absent, which confirms the diagnosis.

Step 3 — get the CA certificate as a file #

If someone can hand you the root CA in PEM form, use that — it is the trustworthy path. Otherwise you can extract the top certificate from the chain the server sent, but only after confirming out of band that it is the CA you expect:

Write each certificate in the chain to its own file
openssl s_client -connect repo.example.com:443 -servername repo.example.com -showcerts </dev/null 2>/dev/null \
| awk '/BEGIN CERT/{n++} n{print > ("cert-" n ".pem")} /END CERT/{if (n) close("cert-" n ".pem")}'

Confirm the fingerprint of what you are about to import:

Fingerprint and identity of the CA
openssl x509 -in root-ca.crt -noout -subject -issuer -fingerprint -sha256

A root CA is self-signed, so its subject and issuer will be identical.

Step 4 — import it #

You have two options, and they are not equally good.

A separate truststore survives JDK upgrades, is reproducible in a container image, and can be committed to configuration management. Seed it from the JDK’s cacerts so you keep the public CAs, then add yours:

cp "$JAVA_HOME/lib/security/cacerts" truststore.jks
keytool -importcert -trustcacerts -noprompt \
-alias example-corp-root \
-file root-ca.crt \
-keystore truststore.jks \
-storepass changeit

Point the JVM at it. This must be set on the process that makes the connection:

java -Djavax.net.ssl.trustStore=/opt/app/truststore.jks \
   -Djavax.net.ssl.trustStorePassword=changeit \
   -jar app.jar

Editing the JDK’s own store is quicker and appropriate for a throwaway container, but it is lost on every JDK upgrade and invisible to anyone reading your configuration:

sudo keytool -importcert -trustcacerts -noprompt \
-alias example-corp-root \
-file root-ca.crt \
-cacerts -storepass changeit

On Debian and Ubuntu, $JAVA_HOME/lib/security/cacerts is a symlink to /etc/ssl/certs/java/cacerts, which update-ca-certificates regenerates. Add the root the Debian way instead, or your import will vanish at the next package update:

sudo cp root-ca.crt /usr/local/share/ca-certificates/example-corp-root.crt
sudo update-ca-certificates

Restart the JVM afterwards. The truststore is read once during initialisation, so a running process will keep failing no matter what you import.

When the chain is incomplete rather than untrusted #

If Step 1 showed a public CA and only one certificate came back, the server is at fault: it is not sending its intermediates. Browsers paper over this by fetching the missing intermediate from the Authority Information Access extension, and Java does not — unless you ask it to:

java -Dcom.sun.security.enableAIAcaIssuers=true -jar app.jar

That flag makes Java fetch missing intermediates the way a browser does, and it resolves this class of failure without importing anything. It is a workaround for someone else’s misconfigured server, not a substitute for fixing the chain, and it costs an outbound HTTP request on the first handshake.

Setting the flags for common tools #

The JVM that needs the truststore is the one opening the connection, which is often not the one you launched:

Tool Where the options go
Maven MAVEN_OPTS="-Djavax.net.ssl.trustStore=/opt/truststore.jks -Djavax.net.ssl.trustStorePassword=changeit"
Gradle org.gradle.jvmargs=-Djavax.net.ssl.trustStore=... in gradle.properties
Jenkins JAVA_OPTS in the service unit, then restart the controller
Tomcat CATALINA_OPTS in bin/setenv.sh
Spring Boot JAVA_TOOL_OPTIONS, or the same -D flags after java
Elasticsearch ES_JAVA_OPTS

Still failing after the import #

Turn on the JSSE handshake log. It names the truststore that was actually loaded and every anchor in it, which usually reveals that a different JVM, or a different store, is in play:

java -Djavax.net.debug=ssl:handshake:trustmanager -jar app.jar 2>&1 | head -60

The three things that log tends to expose, in order of frequency: the application runs on a different JDK from the one you imported into; the truststore path is set but the process never restarted; or an outbound proxy is terminating TLS and presenting a chain you have not seen yet.

What not to do #

Frequently asked questions

Why does curl work but Java fail against the same URL?
curl uses the operating system CA bundle. Java ignores it entirely and uses cacerts inside the JDK. Any CA added with update-ca-certificates or the macOS keychain is invisible to Java, so the two tools genuinely disagree about what is trusted.
Should I import the server certificate or the CA certificate?
The CA certificate. Importing the leaf appears to work and then breaks silently the next time the certificate is renewed, which for a public CA is every few months. Import the root, or the intermediate if the server is the only thing that will ever present it.
What is the cacerts password?
changeit, and it has been the default for the entire history of the JDK. If it was changed, whoever built the image changed it deliberately.
Does Java fetch missing intermediates like a browser does?
Not by default. Browsers follow the Authority Information Access extension to fetch a missing intermediate; Java only does this when started with -Dcom.sun.security.enableAIAcaIssuers=true. That single flag resolves a surprising number of these failures without touching a truststore at all.
Where is cacerts on my system?
On Java 9 and later it is $JAVA_HOME/lib/security/cacerts. On Java 8 it is $JAVA_HOME/jre/lib/security/cacerts. On Debian and Ubuntu it is usually a symlink to /etc/ssl/certs/java/cacerts, which is regenerated by update-ca-certificates and will discard manual edits.

References