지금 빌드가 SSL로 죽었다면 — 결론부터
급해서 검색해 들어왔다면 아래 세 줄부터 실행하세요. 이 순서가 곧 원인 판정입니다.
# 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.jar1번의 issuer=가 공인 CA(예: DigiCert, Let's Encrypt) 인데 실패하면 JVM truststore/JDK 버전 문제, 회사명·프록시명(Zscaler, BlueCoat, Fortinet 등) 이면 사내 MITM 프록시 인증서 누락, issuer와 subject가 같으면 자체서명 인증서입니다. 여기서 이후 모든 분기가 갈립니다. 이 글은 JVM·keytool·truststore에 특화된 런북이며, Go/Docker의 x509: certificate signed by unknown authority와는 원인 진단 도구가 다릅니다.
이 에러의 정체 — 두 이름은 같은 문제
PKIX path building failed와 SunCertPathBuilderException은 같은 사건의 다른 이름입니다. PKIX(Public-Key Infrastructure X.509)는 인증서 체인을 검증하는 규격이고, Java의 기본 구현이 서버 인증서에서 시작해 신뢰할 수 있는 루트 CA까지 이어지는 경로(path) 를 만들지 못했다는 뜻입니다.
"코드는 그대로인데 어제까지 되던 게 오늘 안 된다"면 대부분 코드가 아니라 환경이 바뀐 것입니다. 실무에서 가장 자주 보고되는 트리거는 다음과 같습니다.
- 사무실 이동·재택 전환으로 SSL 인스펙션 프록시를 새로 타게 됨
- 회사가 보안 강화로 Zscaler 등 MITM 프록시를 도입
- JDK 8 → 17/21 LTS 업그레이드 후 cacerts 경로/내용이 달라짐
- 컨테이너 빌드로 옮기면서 사내 CA를 truststore에 주입하지 않음
스택트레이스 줄별 해석
전형적인 스택트레이스는 예외가 3단으로 감싸여 있습니다. 안쪽으로 갈수록 진짜 원인입니다.
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: TLS 협상 도중 실패. 네트워크 연결 자체는 됐다는 신호입니다(연결 자체가 안 되면
ConnectException). - (2) ValidatorException: 서버가 준 인증서를 검증하다 거부. 검증 로직에는 도달했다는 뜻입니다.
- (3) SunCertPathBuilderException: 핵심. "requested target까지 유효한 인증 경로를 찾을 수 없다" = 서버 인증서를 발급한 CA를 JVM이 신뢰하지 않는다는 것입니다.
즉 "인증서가 위조됐다"가 아니라 "신뢰 목록에 발급자가 없다"가 99%입니다. 그래서 해결은 "올바른 CA를 truststore에 넣기"로 귀결됩니다.
원인 분기 의사결정표
30초 판정에서 나온 issuer 값을 기준으로 아래 표를 따라가세요.
| 증상 / 판정 단서 | 유력 원인 | 다음 명령 |
|---|---|---|
| Issuer가 공인 CA인데 실패 | JVM truststore 손상·구버전, JDK 내 cacerts 문제 | keytool -list -cacerts로 해당 루트 존재 확인, JDK 최신 패치 |
| Issuer가 회사명/프록시명(Zscaler 등) | 사내 SSL 인스펙션(MITM) 인증서 누락 | 프록시 루트 CA를 -importcert로 등록 |
| Issuer == Subject | 자체서명(self-signed) 인증서 | 해당 서버 인증서를 직접 truststore에 등록 |
| 루트는 있는데 여전히 실패 | 중간 CA 미포함(체인 불완전) | -showcerts로 체인 확인 후 중간 CA도 등록 |
| 특정 JDK에서만 실패 | JDK 버전별 cacerts 경로·내용 차이 | 아래 버전 비교표 확인 |
진단 3종 세트를 순서대로 돌리면 표의 어느 행인지 확정됩니다.
# ① 서버가 실제로 내려주는 체인 전체 (중간 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예상 정상 결과: ①에서 Verify return code: 0 (ok)가 뜨면 OS 레벨에서는 신뢰가 성립한 것(문제는 JVM에만 있음). ②에서 grep 결과가 나오면 해당 CA는 이미 등록됨. 만약 ①은 ok인데 Java만 실패하면, OS 신뢰 저장소와 JVM cacerts가 분리되어 있어서 생기는 전형적 사례입니다.
복구 절차 — keytool import 실전
1단계: 필요한 인증서 추출
프록시/서버가 주는 루트(또는 중간) CA를 PEM으로 뽑습니다.
# 서버가 주는 최상위(마지막) 인증서를 파일로 저장
openssl s_client -connect api.example.com:443 -showcerts </dev/null 2>/dev/null \
| openssl x509 -outform PEM > corp-root.pem
# 사내 프록시 CA는 보통 보안팀이 배포한 .cer/.pem을 그대로 사용2단계: truststore에 등록
JVM 공용 cacerts에 넣거나(전역), 앱 전용 커스텀 truststore를 만드는 방법(격리) 두 가지가 있습니다.
# 방법 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예상 정상 결과: Certificate was added to keystore가 출력되고, 앱을 다시 실행하면 핸드셰이크가 통과합니다.
JDK 버전별 cacerts 경로 · 비밀번호
LTS 전환으로 가장 많이 헤매는 지점입니다. -cacerts 플래그가 없는 JDK 8은 -keystore로 경로를 직접 지정해야 합니다.
| JDK | cacerts 경로 | 기본 비밀번호 | keytool 방식 |
|---|---|---|---|
| 8 | $JAVA_HOME/jre/lib/security/cacerts | changeit | -keystore $JAVA_HOME/jre/lib/security/cacerts |
| 11 | $JAVA_HOME/lib/security/cacerts | changeit | -cacerts 사용 가능 |
| 17 | $JAVA_HOME/lib/security/cacerts | changeit | -cacerts 사용 가능 |
| 21 | $JAVA_HOME/lib/security/cacerts | changeit | -cacerts 사용 가능 |
JDK 9부터 JRE가 사라져 jre/ 하위 경로가 없어졌습니다. 8에서 쓰던 스크립트를 그대로 11+에 붙이면 "파일 없음"으로 실패하니 주의하세요.
빌드툴에 적용하기 (Maven / Gradle)
빌드가 죽는 경우, 빌드툴을 돌리는 JVM에 truststore를 알려줘야 합니다. 앱 런타임과 빌드 JVM은 별개입니다.
# Maven — 환경변수로 전달
export MAVEN_OPTS="-Djavax.net.ssl.trustStore=/opt/app/app-truststore.jks \
-Djavax.net.ssl.trustStorePassword=mypass"
mvn clean package# Gradle — gradle.properties 또는 명령행
org.gradle.jvmargs=-Djavax.net.ssl.trustStore=/opt/app/app-truststore.jks -Djavax.net.ssl.trustStorePassword=mypassimport 후에도 실패한다면 — 재분기 미니 FAQ
Q. 루트 CA를 넣었는데도 똑같이 실패해요.
체인 불완전일 가능성이 큽니다. 서버가 중간 CA를 안 내려주는 경우, 루트만 넣어도 경로가 안 이어집니다. openssl s_client -showcerts로 나온 인증서를 위에서부터 순서대로 전부 별도 alias로 등록해 보세요.
Q. import가 alias <name> already exists로 실패해요.
alias 중복입니다. 기존 것을 지우고 다시 넣습니다.
keytool -delete -alias corp-proxy -cacerts -storepass changeit
keytool -importcert -alias corp-proxy -file corp-root.pem -cacerts -storepass changeit -nopromptQ. 등록은 됐다는데 앱은 여전히 옛 truststore를 봐요.
앱이 다른 truststore를 참조 중입니다. -Djavax.net.ssl.trustStore 설정 여부, 그리고 빌드 JVM ≠ 런타임 JVM 인지 확인하세요. 컨테이너라면 이미지 안의 JDK cacerts에 주입됐는지도 봐야 합니다. 실행 중인 JVM이 어떤 truststore를 쓰는지는 아래로 확인합니다.
java -Djavax.net.debug=ssl:trustmanager -jar app.jar 2>&1 | grep -i "trust store"재발 방지 — 그리고 절대 하지 말 것
컨테이너/CI 환경에서 반복되는 근본 원인은 "이미지에 사내 CA가 없다"입니다. 다음을 표준화하세요.
- 사내 CA 묶음(
corp-ca.pem)을 산출물로 관리하고, 베이스 이미지 빌드 단계에서 cacerts에 baking - CI 파이프라인에
keytool -importcert스텝을 넣어 truststore를 자동 갱신 - 만료 모니터링: 루트/중간 CA 만료 전에 갱신
# 베이스 이미지에서 사내 CA를 미리 주입하는 예시
COPY corp-ca.pem /tmp/corp-ca.pem
RUN keytool -importcert -alias corp-ca -file /tmp/corp-ca.pem \
-cacerts -storepass changeit -noprompt⚠️ 절대 하지 말 것: 급하다고 TrustManager를 모든 인증서를 통과시키는 all-trust로 덮거나, -Dcom.sun.net.ssl.checkRevocation=false, 인증서 검증 자체를 끄는 것은 앱을 MITM 공격에 그대로 노출시키는 행위입니다. 임시 디버깅에서 쓰더라도 프로덕션에는 절대 반입 금지입니다. 문제는 "검증을 끄는 것"이 아니라 "올바른 CA를 신뢰 목록에 넣는 것"으로만 풀어야 합니다.
마무리 체크리스트
-
openssl s_client로 Issuer 확인 → 공인 CA / 프록시 / 자체서명 판정 - JDK 버전에 맞는 cacerts 경로 확인 (8은
jre/lib, 11+는lib) - 루트뿐 아니라 중간 CA까지 체인 완전하게 등록
- 빌드 JVM·런타임 JVM·컨테이너 이미지 세 곳 모두 truststore 반영
- 검증 비활성화 우회 코드가 남아 있지 않은지 최종 점검
자주 묻는 질문 (FAQ)
Q. unable to find valid certification path to requested target는 인증서가 위조됐다는 뜻인가요?
아닙니다. 대부분은 서버 인증서를 발급한 CA가 JVM의 신뢰 목록(cacerts)에 없다는 의미입니다. 해당 CA를 truststore에 등록하면 해결됩니다.
Q. OS(브라우저)에서는 접속이 되는데 왜 Java만 실패하나요?
Java는 OS 신뢰 저장소가 아니라 JVM 자체의 cacerts를 사용하기 때문입니다. 사내 프록시 CA가 OS에는 배포됐지만 JVM cacerts에는 빠진 전형적 상황이며, keytool -importcert로 JVM에 별도 등록해야 합니다.
Q. keytool -cacerts 옵션이 안 먹혀요.
-cacerts 플래그는 JDK 9부터 지원됩니다. JDK 8에서는 -keystore $JAVA_HOME/jre/lib/security/cacerts -storepass changeit 형태로 경로를 직접 지정해야 합니다.
이 글은 AI 에이전트가 자료 조사와 1차 초안 작성을 담당하고, 사람 편집자가 사실관계·출처·톤과 맥락을 검토한 뒤 발행했습니다. 환경(OS·버전)에 따라 결과가 다를 수 있으니 적용 전 공식 문서를 함께 확인하세요. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
댓글
첫 번째 댓글을 남겨보세요.