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.
# 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.jarIf 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.
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 clue | Likely cause | Next command |
|---|---|---|
| Issuer is a public CA but it still fails | JVM truststore corrupted/outdated, or a cacerts problem inside the JDK | Confirm 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) certificate | Import the proxy root CA with -importcert |
| Issuer == Subject | Self-signed certificate | Import that server certificate directly into the truststore |
| Root is present but it still fails | Intermediate CA missing (incomplete chain) | Inspect the chain with -showcerts and import the intermediate CA as well |
| Fails only on a specific JDK | cacerts path/contents differ by JDK version; an old, unpatched JDK lacks a newer root CA | Check the version comparison table below; update to the latest JDK patch |
The message says PKIX path validation failed ... validity check failed | The 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.
# ① ( 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.jarExpected 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.
# 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 filePiping 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).
# 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.jarExpected 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.
# 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.jarcacerts 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.
| JDK | cacerts path | Default password | keytool usage |
|---|---|---|---|
| 8 | $JAVA_HOME/jre/lib/security/cacerts | changeit | -keystore $JAVA_HOME/jre/lib/security/cacerts |
| 11 | $JAVA_HOME/lib/security/cacerts | changeit | -cacerts is available |
| 17 | $JAVA_HOME/lib/security/cacerts | changeit | -cacerts is available |
| 21 | $JAVA_HOME/lib/security/cacerts | changeit | -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.
# 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# Gradle — gradle.properties or
org.gradle.jvmargs=-Djavax.net.ssl.trustStore=/opt/app/app-truststore.jks -Djavax.net.ssl.trustStorePassword=mypassIf dependencies can only be downloaded through a corporate proxy, also configure the proxy for Maven in ~/.m2/settings.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.
keytool -delete -alias corp-proxy -cacerts -storepass changeit
keytool -importcert -alias corp-proxy -file corp-root.pem -cacerts -storepass changeit -nopromptQ. 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:
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 -importcertstep to the CI pipeline so the truststore is refreshed automatically - Expiry monitoring: renew the root/intermediate CA before it expires
# 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+ islib) - 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.
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.
Comments
Be the first to comment.