Diagnose and Fix PKIX path building failed in 5 Minutes (keytool cacerts import)
Has a single line in the build log ever stopped a deployment cold?
javax.net.ssl.SSLHandshakeException: PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested targetIf the same code built fine yesterday and fails today, this is not a code bug. The JVM decided it cannot trust the remote server’s certificate — in other words, a truststore (cacerts) trust problem. Remember one thing: curl and pip use the operating system’s certificate store, but Java only looks at the JVM’s own store, cacerts. That’s why “the browser and curl work, but Java fails” is so common. Even if a corporate CA is installed on the OS, the JVM has no idea it exists.
This post is ordered diagnose → command → verify so someone who is stuck right now can follow from the top and get unblocked.
5-minute diagnosis: same error, five-cause decision table
The message looks identical, but the cause usually falls into one of five buckets. Narrow your case with the signals below.
| # | Cause | Distinguishing signal | First action |
|---|---|---|---|
| ① | Private CA / self-signed certificate | Only fails against internal systems/APIs; public sites work | Import the private/root CA into cacerts |
| ② | Missing intermediate certificate | Browser OK, Java fails; s_client shows a chain of length 1 | Import the intermediate as well |
| ③ | Corporate proxy MITM (Zscaler, Netskope) | Fails only on the company network; issuer is a security-product name | Import the proxy root CA |
| ④ | Stale JDK cacerts / old bundled roots | Old JDK 8 missing a newer CA (e.g. ISRG/Let's Encrypt) | Update the JDK or import the root CA |
| ⑤ | Certificate itself expired | s_client notAfter date is in the past | Ask the server owner to renew |
The decisive diagnostic tool is openssl s_client. Look at the actual chain with your own eyes.
openssl s_client -connect api.example.com:443 -showcerts < /dev/nullUnder Certificate chain, check whether certificates appear all the way from root → intermediate → server, and what verify return code is. A short chain points to ②; an unfamiliar corporate issuer points to ③.
Standard fix: extract with openssl → import into cacerts with keytool
This is the textbook recovery path: extract the blocking certificate and register it so the JVM trusts it.
1) Extract the certificate (save as PEM)
# Save the chain the server presents to a file
openssl s_client -connect api.example.com:443 -showcerts < /dev/null \
2>/dev/null | openssl x509 -outform PEM > example-ca.crtIf there are multiple certificates, split each -----BEGIN CERTIFICATE----- ~ -----END CERTIFICATE----- block into its own .crt. You must register both the root and the intermediate or case ② will stay broken.
2) Import into cacerts
keytool -importcert \
-alias example-ca \
-file example-ca.crt \
-keystore "$JAVA_HOME/lib/security/cacerts" \
-storepass changeit \
-noprompt3) Confirm the import (verify)
keytool -list -alias example-ca \
-keystore "$JAVA_HOME/lib/security/cacerts" -storepass changeitPath, password, and permission gotchas
- The cacerts path differs by JDK version.
- JDK 9+:
$JAVA_HOME/lib/security/cacerts - JDK 8:
$JAVA_HOME/jre/lib/security/cacerts
- JDK 9+:
- OS path examples
- macOS:
/Library/Java/JavaVirtualMachines/<jdk>/Contents/Home/lib/security/cacerts - Linux:
/usr/lib/jvm/<jdk>/lib/security/cacerts - Windows:
C:\Program Files\Java\<jdk>\lib\security\cacerts
- macOS:
- The default password is
changeit. - The system JDK cacerts needs write permission. Use
sudoon Linux/macOS, and an “Administrator” terminal on Windows.
Practitioner tip: I hit PKIX errors most often right after an LTS jump (11→17→21). Switching JDKs means a brand-new cacerts file, so previously imported corporate CAs vanish. Put “JDK swap = re-register cacerts” on the checklist and you won’t lose days to it.
Ops-friendly alternative: a custom truststore and build workarounds
If you don’t want to touch the system cacerts (production hosts, shared CI, etc.), create a dedicated truststore and point the JVM at it. That’s the safer pattern.
# Create a separate truststore, then import
keytool -importcert -alias example-ca -file example-ca.crt \
-keystore custom-truststore.p12 -storetype PKCS12 -storepass mypass -nopromptPass it as JVM options when you run the app.
java -Djavax.net.ssl.trustStore=/opt/app/custom-truststore.p12 \
-Djavax.net.ssl.trustStorePassword=mypass \
-Djavax.net.ssl.trustStoreType=PKCS12 \
-jar app.jarWhen a Maven build breaks (dependency download fails):
export MAVEN_OPTS="-Djavax.net.ssl.trustStore=/opt/app/custom-truststore.p12 \
-Djavax.net.ssl.trustStorePassword=mypass \
-Djavax.net.ssl.trustStoreType=PKCS12"In a proxy environment, also configure the proxy 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>When a Gradle build breaks — add this to gradle.properties:
org.gradle.jvmargs=-Djavax.net.ssl.trustStore=/opt/app/custom-truststore.p12 \
-Djavax.net.ssl.trustStorePassword=mypass \
-Djavax.net.ssl.trustStoreType=PKCS12Then re-fetch with gradle build --refresh-dependencies so the cache is ignored.
Containers/CI (GitHub Actions, Jenkins) often fail because the base image has no corporate CA. Bake the CA into the Dockerfile build stage.
FROM eclipse-temurin:21-jdk
COPY example-ca.crt /usr/local/share/ca-certificates/example-ca.crt
RUN keytool -importcert -alias example-ca \
-file /usr/local/share/ca-certificates/example-ca.crt \
-keystore "$JAVA_HOME/lib/security/cacerts" \
-storepass changeit -nopromptConclusion: what never to do + a safety checklist
Do not ship the “trust every certificate” snippet that shows up when you panic-search.
// ⚠️ Anti-pattern — leaves you wide open to MITM. Do not use!
TrustManager[] trustAll = new TrustManager[]{
new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] c, String a) {}
public void checkServerTrusted(X509Certificate[] c, String a) {} // no verification
public X509Certificate[] getAcceptedIssuers() { return null; }
}
};
HttpsURLConnection.setDefaultHostnameVerifier((h, s) -> true); // dangerous!🚨 Security warning The code above, and flags like
-Dcom.sun.net.ssl.checkRevocation=false, turn certificate validation off entirely. Anyone in the middle (MITM) can impersonate the server and the client will still talk. Passwords, tokens, and DB credentials can leak as if they were plaintext.
Safe-fix checklist
- ✅ Confirm the real chain, issuer, and expiry with
openssl s_clientfirst - ✅ Prefer registering the proper CA (root and intermediate) the proper way
- ✅ On prod/CI, use a custom truststore instead of the system cacerts
- ✅ Re-register cacerts whenever you change JDK versions
- ✅ Never commit validation-disabling code
FAQ
Q. What’s the keytool password?
A. The default JDK cacerts password is changeit. For a truststore you created yourself, use the -storepass value you set at creation time.
Q. Why does the browser work but Java doesn’t? A. Browsers and curl use the OS certificate store; Java only sees the JVM’s cacerts. This happens when a corporate proxy CA (Zscaler, etc.) or a private CA is on the OS but not in cacerts.
Q. I imported it and still get the same error.
A. Check three things: ① you imported the intermediate as well ② you imported into the cacerts of the JDK that actually runs (java -version / path) ③ custom truststore JVM options are actually applied.
Q. Can I edit cacerts on a production server directly?
A. Not recommended. A JDK update overwrites it and the blast radius is large. Point at a custom truststore with -Djavax.net.ssl.trustStore instead.
Q. Does the same approach apply to DB (JDBC) SSL? A. Yes — same truststore model. Import the DB server certificate (or its CA) the same way, or point the connection config at the truststore.
Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
Comments
Be the first to comment.