Home/Development/PKIX path building failed / SunCertPathBuilderException: A 30-Minute Fix Runbook
DevelopmentPKIXkeytool

PKIX path building failed / SunCertPathBuilderException: A 30-Minute Fix Runbook

A hands-on runbook that branches Java PKIX path building failed and SunCertPathBuilderException with a 30-second issuer-based diagnosis, covering openssl and keytool commands, cacerts import for JDK 8/11/17/21, and how to stop it from comin

PKIX path building failed / SunCertPathBuilderException: A 30-Minute Fix Runbook
Contents
  1. If your build just died on SSL — start here
  2. What this error actually is — two names, one problem
  3. Reading the stack trace line by line
  4. Cause-branching decision table
  5. Recovery procedure — keytool import in practice
  6. Applying this to your build tool (Maven / Gradle)
  7. If it still fails after import — a mini re-branch FAQ
  8. Preventing recurrence — and what you must never do
  9. Frequently asked questions (FAQ)

If your build just died on SSL — start here

If you landed here in a hurry, run these three commands first. This sequence is the diagnosis.

Bash
# 1) Check the issuer and subject of the server (leaf) certificate (openssl x509 reads only the first certificate)
openssl s_client -connect api.example.com:443 -showcerts </dev/null 2>/dev/null | openssl x509 -noout -issuer -subject

# 2) JVM certificate list (JDK 9+)
keytool -list -cacerts -storepass changeit | head -n 20

# 3) check
java -Djavax.net.debug=ssl:handshake:verbose -jar app.jar

If command 1's issuer= is a public CA (e.g. DigiCert, Let's Encrypt) and it still fails, it's a JVM truststore / JDK version problem. If it's a company or proxy name (Zscaler, BlueCoat, Fortinet, etc.), you're missing the corporate MITM proxy certificate. If issuer and subject are the same, it's a self-signed certificate. Every later branch starts from here. This post is a runbook specialized for the JVM, keytool, and truststores; the diagnostic tools differ from Go/Docker's x509: certificate signed by unknown authority.

What this error actually is — two names, one problem

PKIX path building failed and SunCertPathBuilderException are two names for the same incident. PKIX (Public-Key Infrastructure X.509) is the spec for validating certificate chains, and it means Java's default implementation could not build a path from the server certificate up to a trusted root CA.

If "the code hasn't changed but it worked yesterday and fails today," it's almost never the code — the environment changed. The most common triggers in production:

  • You started hitting an SSL inspection proxy after moving offices or switching to remote work
  • The company rolled out a MITM proxy such as Zscaler as a security hardening measure
  • After a JDK 8 → 17/21 LTS upgrade, the cacerts path or contents changed
  • You moved to container builds and did not inject the corporate CA into the truststore

Reading the stack trace line by line

A typical stack trace wraps the exception in three layers. The deeper you go, the closer you get to the real cause.

TEXT
javax.net.ssl.SSLHandshakeException: PKIX path building failed: # ← (1) TLS
 sun.security.validator.ValidatorException: # ← (2) certificate
    PKIX path building failed:
 sun.security.provider.certpath.SunCertPathBuilderException: # ← (3) :
    unable to find valid certification path to requested target
  • (1) SSLHandshakeException: Failed during TLS negotiation. That's a signal the network connection itself succeeded (if the connection itself failed you'd see ConnectException).
  • (2) ValidatorException: The certificate the server presented was rejected during validation. You did reach the validation logic.
  • (3) SunCertPathBuilderException: The core. "unable to find valid certification path to requested target" = the JVM does not trust the CA that issued the server certificate.

In other words, it doesn't mean "the certificate is forged"; it means "the issuer is not in the trust list." So the fix boils down to putting the right CA into the truststore.

Cause-branching decision table

Follow the table below based on the issuer value from the 30-second diagnosis.

Symptom / diagnostic clueLikely causeNext command
Issuer is a public CA but it still failsJVM truststore corrupted/outdated, or a cacerts problem inside the JDKConfirm the root exists with keytool -list -cacerts; apply the latest JDK patch
Issuer is a company/proxy name (Zscaler, etc.)Missing corporate SSL inspection (MITM) certificateImport the proxy root CA with -importcert
Issuer == SubjectSelf-signed certificateImport that server certificate directly into the truststore
Root is present but it still failsIntermediate CA missing (incomplete chain)Inspect the chain with -showcerts and import the intermediate CA as well
Fails only on a specific JDKcacerts path/contents differ by JDK version; an old, unpatched JDK lacks a newer root CACheck the version comparison table below; update to the latest JDK patch
The message says PKIX path validation failed ... validity check failedThe certificate has expired (not a path-building problem)Check the dates with openssl x509 -in chain-1.pem -noout -dates, then renew the server certificate

Run the three diagnostic commands in order and you'll know which row of the table you're on.

Bash
# ① ( CA to )
openssl s_client -connect api.example.com:443 -showcerts </dev/null 2>/dev/null

# ② JVM CA list
keytool -list -cacerts -storepass changeit | grep -i digicert

# ③ certificate
java -Djavax.net.debug=ssl:handshake:verbose -jar app.jar

Expected healthy result: If ① shows Verify return code: 0 (ok), trust is established at the OS level (the problem is JVM-only). If ② returns a grep hit, that CA is already registered. If ① is ok but Java still fails, that's the classic case of the OS trust store and JVM cacerts being separate.

Recovery procedure — keytool import in practice

Step 1: Extract the certificate you need

Pull the root (or intermediate) CA presented by the proxy/server into PEM form.

Bash
# Get the full chain the server sends with -showcerts and split it into one file per certificate (chain-1.pem, chain-2.pem ...)
openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null 2>/dev/null \
  | awk '/BEGIN CERTIFICATE/{n++; f="chain-" n ".pem"} f{print > f} /END CERTIFICATE/{close(f); f=""}'

# Check subject and issuer of each file: chain-1 is the server (leaf), the rest are intermediate (or proxy) CAs
for f in chain-*.pem; do echo "== $f"; openssl x509 -in "$f" -noout -subject -issuer; done

# Pick the CA certificate to import and rename it (e.g. if the proxy CA is chain-2)
cp chain-2.pem corp-root.pem

# For a corporate proxy CA, the .cer/.pem distributed by the security team is usually the right file

Piping straight into openssl s_client ... | openssl x509 -outform PEM saves only the first certificate (the server leaf), not the last one in the chain. Servers usually don't send the root CA either, so if you need the root, the file distributed by your security team or the CA is the accurate source.

Step 2: Import into the truststore

Two options: import into the JVM-wide cacerts (global), or create an app-specific custom truststore (isolated).

Bash
# A) JVM cacerts (JDK 9+ -cacerts )
keytool -importcert -alias corp-proxy -file corp-root.pem \
  -cacerts -storepass changeit -noprompt

# B) dedicated truststore ( when )
keytool -importcert -alias corp-proxy -file corp-root.pem \
  -keystore app-truststore.jks -storepass mypass -noprompt

# run when truststore set
java -Djavax.net.ssl.trustStore=/opt/app/app-truststore.jks \
     -Djavax.net.ssl.trustStorePassword=mypass -jar app.jar

Expected healthy result: You see Certificate was added to keystore, and the handshake succeeds when you restart the app.

When you point -Djavax.net.ssl.trustStore at a custom truststore, the JVM trusts only that file instead of the default cacerts. If the file contains only your corporate CA, public sites break instead, so the safer approach is to copy the default cacerts into a PKCS12 file and add the corporate CA to it.

Bash
# Copy the default cacerts into a PKCS12 file, then add the corporate CA (JDK 9+ path)
keytool -importkeystore -srckeystore "$JAVA_HOME/lib/security/cacerts" -srcstorepass changeit \
  -destkeystore app-truststore.p12 -deststoretype PKCS12 -deststorepass mypass
keytool -importcert -alias corp-proxy -file corp-root.pem \
  -keystore app-truststore.p12 -storetype PKCS12 -storepass mypass -noprompt

java -Djavax.net.ssl.trustStore=/opt/app/app-truststore.p12 \
     -Djavax.net.ssl.trustStorePassword=mypass \
     -Djavax.net.ssl.trustStoreType=PKCS12 -jar app.jar

cacerts path and password by JDK version

This is where people get lost most often during LTS migrations. JDK 8 has no -cacerts flag, so you must pass the path with -keystore.

JDKcacerts pathDefault passwordkeytool usage
8$JAVA_HOME/jre/lib/security/cacertschangeit-keystore $JAVA_HOME/jre/lib/security/cacerts
11$JAVA_HOME/lib/security/cacertschangeit-cacerts is available
17$JAVA_HOME/lib/security/cacertschangeit-cacerts is available
21$JAVA_HOME/lib/security/cacertschangeit-cacerts is available

Starting with JDK 9 the JRE is gone, so the jre/ subdirectory no longer exists. If you paste a JDK 8 script onto 11+ it will fail with "file not found" — watch out for that.

Applying this to your build tool (Maven / Gradle)

When the build itself dies, you must point the JVM that runs the build tool at the truststore. The app runtime JVM and the build JVM are separate.

Bash
# Maven — as an env var
export MAVEN_OPTS="-Djavax.net.ssl.trustStore=/opt/app/app-truststore.jks \
  -Djavax.net.ssl.trustStorePassword=mypass"
mvn clean package
PROPERTIES
# Gradle — gradle.properties or
org.gradle.jvmargs=-Djavax.net.ssl.trustStore=/opt/app/app-truststore.jks -Djavax.net.ssl.trustStorePassword=mypass

If dependencies can only be downloaded through a corporate proxy, also configure the proxy for Maven in ~/.m2/settings.xml.

XML
<settings>
  <proxies>
    <proxy>
      <id>corp</id><active>true</active>
      <protocol>https</protocol>
      <host>proxy.corp.com</host><port>8080</port>
    </proxy>
  </proxies>
</settings>

For Gradle, after setting the truststore, run gradle build --refresh-dependencies to ignore cached failures and download again.

If it still fails after import — a mini re-branch FAQ

Q. I imported the root CA and it still fails the same way. Most likely an incomplete chain. If the server does not send the intermediate CA, importing only the root will not complete the path, and the intermediate won't appear in the -showcerts output either. The real fix is to make the server send the full chain (fullchain). If you must work around it on the client, download the intermediate CA from the CA Issuers URL in the leaf certificate's AIA extension (openssl x509 -in chain-1.pem -noout -ext authorityInfoAccess) and import it under its own alias.

Q. Import fails with alias <name> already exists. The alias is already taken. Delete the existing one and import again.

Bash
keytool -delete -alias corp-proxy -cacerts -storepass changeit
keytool -importcert -alias corp-proxy -file corp-root.pem -cacerts -storepass changeit -noprompt

Q. Import succeeded but the app still uses the old truststore. The app is pointing at a different truststore. Check whether -Djavax.net.ssl.trustStore is set, and whether the build JVM ≠ the runtime JVM. If you're in a container, also check whether it was injected into the JDK cacerts inside the image. Confirm which truststore the running JVM is using with:

Bash
java -Djavax.net.debug=ssl:trustmanager -jar app.jar 2>&1 | grep -i "trust store"

Preventing recurrence — and what you must never do

The recurring root cause in container/CI environments is "the image has no corporate CA." Standardize the following.

  • Manage the corporate CA bundle (corp-ca.pem) as an artifact, and bake it into cacerts at base-image build time
  • Add a keytool -importcert step to the CI pipeline so the truststore is refreshed automatically
  • Expiry monitoring: renew the root/intermediate CA before it expires
Dockerfile
# CA example
COPY corp-ca.pem /tmp/corp-ca.pem
RUN keytool -importcert -alias corp-ca -file /tmp/corp-ca.pem \
    -cacerts -storepass changeit -noprompt

⚠️ Never do this: In a hurry, overriding TrustManager with an all-trust implementation that accepts every certificate, turning off hostname verification, or otherwise disabling certificate validation leaves the app wide open to MITM attacks. Note that -Dcom.sun.net.ssl.checkRevocation=false only turns off revocation checking; it doesn't bypass chain validation and doesn't fix a PKIX error either. Even if you use it for temporary debugging, never ship it to production. The problem is not solved by turning validation off — only by putting the correct CA into the trust list.

Closing checklist

  • Confirm the Issuer with openssl s_client → classify as public CA / proxy / self-signed
  • Confirm the cacerts path for your JDK version (8 is jre/lib, 11+ is lib)
  • Import the intermediate CA as well as the root so the chain is complete
  • Apply the truststore in all three places: build JVM, runtime JVM, and container image
  • Final check that no validation-bypass code remains

Frequently asked questions (FAQ)

Q. Does unable to find valid certification path to requested target mean the certificate is forged? No. In most cases it means the CA that issued the server certificate is not in the JVM's trust list (cacerts). Import that CA into the truststore and it will be resolved.

Q. The OS (and browser) can connect — why does only Java fail? Because Java does not use the OS trust store; it uses the JVM's own cacerts. This is the classic situation where the corporate proxy CA was deployed to the OS but never made it into JVM cacerts, so you must register it separately with keytool -importcert.

Q. The keytool -cacerts option doesn't work. The -cacerts flag is supported from JDK 9 onward. On JDK 8 you must specify the path directly: -keystore $JAVA_HOME/jre/lib/security/cacerts -storepass changeit.

Verification
✦ ✦ ✦
Sources and verification

Sources and checks vary by article. Only articles with a recorded check date, scope and sources below were checked, and only within that scope. For other articles, verify against the current docs for your version before applying anything, and email us about errors. Editorial policy · email us

This article has no recorded human check (date and scope) yet.

Published · ·Updated ·
Topic-related official docs (auto-linked · not per-claim sources)Oracle Java documentation ↗OpenSSL documentation ↗

Comments

Be the first to comment.