/개발/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

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) 서버가 실제로 주는 인증서 체인 확인
openssl s_client -connect api.example.com:443 -showcerts </dev/null 2>/dev/null | openssl x509 -noout -issuer -subject

# 2) 지금 쓰는 JVM이 신뢰하는 인증서 목록 조회 (JDK 9+)
keytool -list -cacerts -storepass changeit | head -n 20

# 3) 그래도 모르겠으면 핸드셰이크 로그로 어디서 끊기는지 확인
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) 인증서 검증기가 거부
    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's not "the certificate is forged" — 99% of the time it's "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 versionCheck the version comparison table below

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

Bash
# ① 서버가 실제로 내려주는 체인 전체 (중간 CA 포함 여부까지 보임)
openssl s_client -connect api.example.com:443 -showcerts </dev/null 2>/dev/null

# ② JVM이 신뢰하는 CA 목록에서 특정 발급자 검색
keytool -list -cacerts -storepass changeit | grep -i digicert

# ③ 핸드셰이크에서 어느 인증서에서 끊기는지 상세 로그
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
# 서버가 주는 최상위(마지막) 인증서를 파일로 저장
openssl s_client -connect api.example.com:443 -showcerts </dev/null 2>/dev/null \
  | openssl x509 -outform PEM > corp-root.pem

# 사내 프록시 CA는 보통 보안팀이 배포한 .cer/.pem을 그대로 사용

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) 앱 전용 커스텀 truststore 생성 (건드리기 부담스러울 때 권장)
keytool -importcert -alias corp-proxy -file corp-root.pem \
  -keystore app-truststore.jks -storepass mypass -noprompt

# 실행 시 커스텀 truststore 지정
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.

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 — 환경변수로 전달
export MAVEN_OPTS="-Djavax.net.ssl.trustStore=/opt/app/app-truststore.jks \
  -Djavax.net.ssl.trustStorePassword=mypass"
mvn clean package
PROPERTIES
# Gradle — gradle.properties 또는 명령행
org.gradle.jvmargs=-Djavax.net.ssl.trustStore=/opt/app/app-truststore.jks -Djavax.net.ssl.trustStorePassword=mypass

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. Take every certificate from openssl s_client -showcerts from the top down, in order, and import each 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를 미리 주입하는 예시
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, setting -Dcom.sun.net.ssl.checkRevocation=false, or disabling certificate validation entirely leaves the app wide open to MITM attacks. 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.

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

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

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

Comments

Be the first to comment.