문제 상황
사내 시스템에서 SAP CPI(Cloud Integration) 기반 IS서버로 HTTP 호출을 보내는 과정에서 아래와 같은 오류가 발생했습니다.
I/O error on POST request for "https://osstem-is-dev-npryjfo8.it-cpi015-rt.cfapps.ap12.hana.ondemand.com/http/QM0001_SO":
sun.security.validator.ValidatorException: PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target;
nested exception is javax.net.ssl.SSLHandshakeException: ...
특이한 점은 로컬 PC와 Postman으로 동일한 요청을 보내면 정상적으로 통신이 되는데, 개발서버에서만 이 오류가 발생했다는 것이었습니다. 같은 URL, 같은 요청인데 왜 서버에서만 실패하는지가 이번 트러블슈팅의 핵심이었습니다.
원인 분석
1. PKIX 오류의 의미
PKIX path building failed는 SSL/TLS 통신 시 클라이언트가 상대 서버의 인증서를 검증하는 과정에서, 그 인증서를 발급한 인증기관(CA, Certificate Authority)을 신뢰 목록에서 찾지 못했을 때 발생하는 오류입니다.
2. 인증서 체인 확인
openssl s_client 명령으로 대상 서버(IS서버)가 실제로 어떤 인증서 체인을 제시하는지 확인했습니다.
openssl s_client -connect 도메인:443 -showcerts
확인 결과, 인증서 체인은 다음과 같은 3단계 구조였습니다.
[루트 CA] DigiCert TLS RSA4096 Root G5 (자체 서명)
↓
[중간 CA] DigiCert G5 TLS RSA4096 SHA384 2021 CA1
↓
[서버 인증서] *.it-cpi015-rt.cfapps.ap12.hana.ondemand.com
여기서 루트 CA인 DigiCert TLS RSA4096 Root G5는 2021년에 새로 생성된 비교적 최신 루트 인증기관이었습니다.
3. 로컬과 서버의 차이
- 로컬 PC / Postman: OS(Windows, macOS) 시스템 인증서 저장소를 사용하며, 이미 최신 업데이트를 통해 해당 루트 CA가 포함되어 있어 정상 통신이 가능했습니다.
- 개발/운영서버: 실제 통신을 수행하는 것은 Java 애플리케이션(JDK 1.8, 오래된 패치 버전)이었고, Java는 OS와 무관하게 **자체 신뢰 저장소(cacerts)**를 별도로 관리합니다. 이 Java 버전이 오래되어 최근에 생성된 DigiCert 루트 CA가 등록되어 있지 않았던 것이 근본 원인이었습니다.
즉, OS 신뢰 저장소와 Java 신뢰 저장소는 완전히 독립된 별개의 저장소이며, 한쪽에 CA가 등록되어 있다고 해서 다른 쪽에 자동으로 반영되지 않는다는 점이 이번 이슈의 핵심이었습니다.
해결 과정
1단계. 인증서 파일 추출
문제가 발생한 서버에서 직접 openssl로 인증서 체인을 추출했습니다.
openssl s_client -connect <도메인>:443 -showcerts </dev/null 2>/dev/null | \
awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/{print > ("cert" n ".pem")} /END CERTIFICATE/{n++}'
- cert0.pem : 서버 자체 인증서 (leaf, 등록 불필요)
- cert1.pem : 중간 CA (등록 필요)
- cert2.pem : 루트 CA (등록 필요)
2단계. 실제 호출 애플리케이션의 Java 경로 확인
같은 서버에 여러 버전의 Java가 설치되어 있을 수 있으므로, JAVA_HOME 환경변수를 그대로 믿지 않고 실제 요청을 보내는 프로세스가 사용 중인 Java 경로를 확인했습니다.
ps -ef | grep java
3단계. keytool 절대경로 확인 (중요)
which keytool로 확인되는 경로가 실제 애플리케이션이 사용하는 JDK의 keytool과 다를 수 있다는 점을 발견했습니다. 실제로 시스템 PATH에 잡히는 keytool을 그대로 사용했을 때 다음과 같은 오류가 발생했습니다.
keytool error: gnu.javax.crypto.keyring.MalformedKeyringException: incorrect magic
이는 Oracle/OpenJDK의 keytool이 아닌 GNU Classpath 계열의 다른 keytool이 실행되었기 때문이었습니다. 이후로는 반드시 애플리케이션이 사용하는 JDK의 keytool을 절대경로로 지정해서 실행하도록 했습니다.
/webstore/java1.8-bms/bin/keytool -help | head -5
4단계. cacerts 백업
인증서 등록 작업은 기존 항목을 지우거나 변경하는 것이 아니라 새 항목을 추가하는 작업이지만, 만일의 상황에 대비해 원본을 백업했습니다.
cp <JAVA_HOME>/jre/lib/security/cacerts <JAVA_HOME>/jre/lib/security/cacerts.bak.$(date +%Y%m%d)
5단계. 인증서 등록
<JAVA_HOME>/bin/keytool -import -trustcacerts \
-alias digicert-g5-intermediate \
-file cert1.pem \
-keystore <JAVA_HOME>/jre/lib/security/cacerts \
-storepass changeit
<JAVA_HOME>/bin/keytool -import -trustcacerts \
-alias digicert-g5-root \
-file cert2.pem \
-keystore <JAVA_HOME>/jre/lib/security/cacerts \
-storepass changeit
6단계. 등록 확인
<JAVA_HOME>/bin/keytool -list \
-keystore <JAVA_HOME>/jre/lib/security/cacerts \
-storepass changeit -alias digicert-g5-root
7단계. 애플리케이션 재기동
cacerts는 JVM 구동 시점에 메모리로 로드되기 때문에, 등록 후 반드시 애플리케이션을 재기동해야 반영됩니다.
pwdx <PID> # 원래 실행 디렉토리 확인 (상대경로 옵션 사용 시 중요)
kill <PID>
nohup <JAVA_HOME>/bin/java -jar ... &
결과
개발서버와 운영서버 각각에서 실제 애플리케이션이 사용하는 정확한 JDK 경로를 찾아 DigiCert 루트/중간 CA 인증서를 등록한 뒤, 재기동을 통해 정상적으로 IS서버와의 HTTPS 통신이 복구되었습니다.
정리 및 배운 점
- PKIX 인증서 오류는 대부분 클라이언트 측 신뢰 저장소 문제입니다. 서버 인증서 자체는 정상이더라도, 호출하는 쪽이 그 인증서를 발급한 CA를 모르면 통신이 실패합니다.
- OS 신뢰 저장소와 Java 신뢰 저장소는 서로 완전히 독립적입니다. 로컬/Postman에서 되는데 서버(Java 애플리케이션)에서만 안 되는 경우, 이 차이를 가장 먼저 의심해봐야 합니다.
- 하나의 서버에 여러 버전의 Java가 설치되어 있을 수 있으므로, 반드시 실제 프로세스가 사용 중인 JDK 경로를 확인해야 합니다. JAVA_HOME 환경변수나 which 명령 결과를 그대로 신뢰하면 엉뚱한 곳에 인증서를 등록하는 실수를 할 수 있습니다.
- CA 인증서는 한 번 등록해두면 동일 CA로 발급된 다른 서버들에도 재사용되므로, 서버 하나하나가 아니라 CA 단위로 신뢰를 등록하는 개념이라는 점을 이해하고 접근하는 것이 중요합니다.