/개발/How to Fix PKIX path building failed: A 5-Minute keytool cacerts import Guide
DevelopmentPKIX path building failedSunCertPathBuilderException

How to Fix PKIX path building failed: A 5-Minute keytool cacerts import Guide

Diagnose Java’s “PKIX path building failed” (SunCertPathBuilderException) across five root causes, then fix it in five minutes with copy-paste commands for openssl extraction, keytool cacerts import, and Maven, Gradle, and Docker workaround

How to Fix PKIX path building failed: A 5-Minute keytool cacerts import Guide

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?

CODE
javax.net.ssl.SSLHandshakeException: PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested target

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

#CauseDistinguishing signalFirst action
Private CA / self-signed certificateOnly fails against internal systems/APIs; public sites workImport the private/root CA into cacerts
Missing intermediate certificateBrowser OK, Java fails; s_client shows a chain of length 1Import the intermediate as well
Corporate proxy MITM (Zscaler, Netskope)Fails only on the company network; issuer is a security-product nameImport the proxy root CA
Stale JDK cacerts / old bundled rootsOld JDK 8 missing a newer CA (e.g. ISRG/Let's Encrypt)Update the JDK or import the root CA
Certificate itself expireds_client notAfter date is in the pastAsk the server owner to renew

The decisive diagnostic tool is openssl s_client. Look at the actual chain with your own eyes.

Bash
openssl s_client -connect api.example.com:443 -showcerts < /dev/null

Under 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)

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

If 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

Bash
keytool -importcert \
  -alias example-ca \
  -file example-ca.crt \
  -keystore "$JAVA_HOME/lib/security/cacerts" \
  -storepass changeit \
  -noprompt

3) Confirm the import (verify)

Bash
keytool -list -alias example-ca \
  -keystore "$JAVA_HOME/lib/security/cacerts" -storepass changeit

Path, 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
  • 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
  • The default password is changeit.
  • The system JDK cacerts needs write permission. Use sudo on 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.

Bash
# Create a separate truststore, then import
keytool -importcert -alias example-ca -file example-ca.crt \
  -keystore custom-truststore.p12 -storetype PKCS12 -storepass mypass -noprompt

Pass it as JVM options when you run the app.

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

When a Maven build breaks (dependency download fails):

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

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:

PROPERTIES
org.gradle.jvmargs=-Djavax.net.ssl.trustStore=/opt/app/custom-truststore.p12 \
  -Djavax.net.ssl.trustStorePassword=mypass \
  -Djavax.net.ssl.trustStoreType=PKCS12

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

Dockerfile
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 -noprompt

Conclusion: what never to do + a safety checklist

Do not ship the “trust every certificate” snippet that shows up when you panic-search.

JAVA
// ⚠️ 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

  1. ✅ Confirm the real chain, issuer, and expiry with openssl s_client first
  2. ✅ Prefer registering the proper CA (root and intermediate) the proper way
  3. ✅ On prod/CI, use a custom truststore instead of the system cacerts
  4. ✅ Re-register cacerts whenever you change JDK versions
  5. ✅ 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.

확인 정보
✦ ✦ ✦
편집 검토 · Editorial Review

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 · ·업데이트 ·

Comments

Be the first to comment.