# 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".

Source: https://certguru.io/errors/pkix-path-building-failed/
Last updated: 2026-08-19
Commands verified against OpenSSL 3.5.7.

---

> **The exact message**
>
> ```
>   javax.net.ssl.SSLHandshakeException: PKIX path building failed:
>     sun.security.provider.certpath.SunCertPathBuilderException:
>     unable to find valid certification path to requested target
>   ```
>   Maven reports it as `Non-resolvable parent POM ... PKIX path building failed`, Gradle as
>   `Could not GET ... PKIX path building failed`, and Jenkins as a plugin update-centre failure.
>   The cause is identical in all three.

## 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.

> **Java does not use the system trust store**
>
> This is the single fact that makes the error confusing. `curl`, `wget` and your browser read the
>   operating system CA bundle. The JVM reads `cacerts`, a Java keystore shipped inside the JDK, and
>   nothing else. Adding a root with `update-ca-certificates` or the macOS keychain does not affect
>   Java at all.

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*

```bash
openssl s_client -connect repo.example.com:443 -servername repo.example.com -showcerts /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](/errors/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 |

> **Do not omit -servername**
>
> Without `-servername`, OpenSSL sends no SNI and a virtual-hosted server answers with its default
>   certificate. You will then diagnose the wrong chain entirely.

## 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*

```bash
keytool -list -cacerts -storepass changeit | grep -i "example corp"
```

On Java 8, name the file explicitly:

```bash
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*

```bash
openssl s_client -connect repo.example.com:443 -servername repo.example.com -showcerts /dev/null \
  | awk '/BEGIN CERT/{n++} n{print > ("cert-" n ".pem")} /END CERT/{if (n) close("cert-" n ".pem")}'
```

> **Trusting a certificate you fetched over the broken connection**
>
> Downloading a CA certificate from the same server you cannot verify, then trusting it, adds a
>   permanent trust anchor on the word of an unauthenticated connection. If a proxy is intercepting
>   that connection, you have just trusted the proxy for every future TLS connection the JVM makes.
>   Check the fingerprint against a copy from your platform team before importing.

Confirm the fingerprint of what you are about to import:

*Fingerprint and identity of the CA*

```bash
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.

#### Dedicated truststore (preferred)

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:

    ```bash
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:

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

    > **A truststore replaces, it does not merge**
>
> Setting `javax.net.ssl.trustStore` makes the JVM use that file **instead of** `cacerts`, not
>       in addition to it. A truststore containing only your corporate root will fail against every
>       public site on the internet. That is why the copy above is the first step.

#### Edit cacerts in place

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:

    ```bash
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:

    ```bash
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:

```bash
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](/errors/unable-to-get-local-issuer-certificate/), 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:

```bash
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

> **Do not install an all-trusting TrustManager**
>
> Every forum thread about this error eventually suggests an `X509TrustManager` whose
>   `checkServerTrusted` method has an empty body, or `-Dcom.sun.net.ssl.checkRevocation=false`
>   alongside it. That disables authentication for the whole JVM: the connection stays encrypted, but
>   anything on the network path can present any certificate and be believed. It also has a habit of
>   surviving into production because it makes the symptom disappear completely.


## 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

- [JSSE Reference Guide: X509TrustManager and truststores](https://docs.oracle.com/en/java/javase/21/security/java-secure-socket-extension-jsse-reference-guide.html) — how the JVM locates a truststore and which system properties override it
- [keytool(1), JDK 21](https://docs.oracle.com/en/java/javase/21/docs/specs/man/keytool.html) — the -importcert and -cacerts options used below
- [RFC 5280 section 6: certification path validation](https://www.rfc-editor.org/rfc/rfc5280#section-6) — the algorithm PKIX path building implements, and why it needs a trust anchor

## Related pages

- https://certguru.io/errors/unable-to-get-local-issuer-certificate/
- https://certguru.io/errors/keytool-not-an-x509-certificate/
- https://certguru.io/concepts/certificate-chain/
- https://certguru.io/concepts/root-vs-intermediate-ca/
- https://certguru.io/errors/x509-signed-by-unknown-authority-go/
- https://certguru.io/convert/pem-to-jks/
- https://certguru.io/openssl/get-certificate-from-server/
- https://certguru.io/errors/self-signed-certificate-in-chain/

--
CertGuru — https://certguru.io
