JVM

Spring 서버가 자체 서명 SSL 서버와 통신할 때 생기는 문제와 해결 방법

kyoulho 2025. 5. 29. 23:14

✅ 문제 배경

Spring 서버가 자체 서명된(Self-signed) SSL 인증서를 사용하는 서버와 HTTPS 통신을 시도할 때 다음과 같은 SSL 오류가 발생합니다.

javax.net.ssl.SSLHandshakeException:
    sun.security.validator.ValidatorException:
        PKIX path building failed:
            unable to find valid certification path to requested target

 

 

✅ 직접 생성한 인증서라면

🚩 1단계: OpenSSL과 CNF 파일로 인증서 생성

CNF 파일이란?
OpenSSL 인증서 생성 시 사용하는 설정파일로, 인증서에 포함될 정보를 정의합니다. 특히 subjectAltName 필드는 IP 주소와 도메인을 인증서에 추가해줍니다. 최신 브라우저 및 JVM은 이 필드를 반드시 요구합니다.

openssl.cnf 파일 예시

[ req ]
default_bits       = 2048
default_md         = sha256
prompt             = no
distinguished_name = dn
req_extensions     = v3_req

[ dn ]
C  = KR
ST = Seoul
L  = Seoul
O  = MyCompany
OU = IT Department
CN = 192.168.0.100

[ v3_req ]
subjectAltName = @alt_names

[ alt_names ]
IP.1   = 192.168.0.100
IP.2   = 127.0.0.1
DNS.1  = localhost
DNS.2  = my-internal-domain.local
  • [ req ]: 인증서 요청 기본 설정
    • default_bits: RSA 키 길이 (2048 권장)
    • default_md: 인증서 서명 알고리즘 (sha256 권장)
    • prompt: 정보 입력 없이 즉시 생성 (no)
    • distinguished_name: 인증서 소유자 정보 설정 (dn)
    • req_extensions: 추가 확장 필드(subjectAltName) 사용 지정
  • [ dn ]: 인증서의 주체 정보 설정
    • C: 국가 코드 (KR)
    • ST: 시/도
    • L: 도시
    • O: 조직명
    • OU: 부서명
    • CN: 주체 이름 (IP 주소나 도메인)
  • [ v3_req ]: 인증서에 추가되는 확장 필드 설정
    • subjectAltName: 인증서가 지원하는 IP 주소 및 도메인
  • [ alt_names ]: 실제 사용할 IP 및 DNS 목록 정의

CNF 파일을 사용한 인증서 생성 명령어

openssl req -x509 -nodes -days 365 \
  -newkey rsa:2048 \
  -keyout key.pem -out cert.pem \
  -config openssl.cnf -extensions v3_req
  • key.pem: 개인키 파일
  • cert.pem: 자체 서명된 SSL 인증서 파일


🚩 2단계: keytool을 이용하여 JVM truststore에 인증서 등록

keytool이란?
Java 인증서 관리 도구로, truststore(cacerts)에 인증서를 추가 및 관리합니다.

인증서 등록 명령어

keytool -importcert \
  -alias internal-keycloak \
  -keystore custom-cacerts \
  -file cert.pem \
  -storepass changeit \
  -noprompt
  • -importcert: 인증서를 truststore에 추가
  • -alias: 인증서 별칭
  • -keystore: 인증서 저장 위치
  • -file: 인증서 파일
  • -storepass: truststore 비밀번호 (changeit 기본값)
  • -noprompt: 확인 프롬프트 생략

 

✅ 외부 서버에서 자체 서명된 SSL 인증서를 사용하고 있다면

🚩 1단계: 원격 서버 인증서 추출

openssl s_client -showcerts -connect 192.168.0.100:443 </dev/null 2>/dev/null | openssl x509 -outform PEM > external-server-cert.pem
  • 192.168.0.100:443: 대상 서버의 IP 주소와 HTTPS 포트(일반적으로 443)
  • 생성된 external-server-cert.pem 파일이 원격 서버의 인증서입니다.

추출한 인증서를 검증하고자 할 경우 아래 명령으로 정보를 확인할 수 있습니다.

openssl x509 -in external-server-cert.pem -text -noout

인증서의 세부 정보를 통해 subjectAltName 필드, 유효기간, 주체 정보 등을 확인할 수 있습니다.


🚩 2단계: JVM Truststore에 등록하기

추출된 인증서를 Java의 Truststore(cacerts)에 추가합니다.

keytool -importcert \
  -alias external-server \
  -keystore custom-cacerts \
  -file external-server-cert.pem \
  -storepass changeit \
  -noprompt
  • 인증서 별칭(alias)은 식별 가능한 이름으로 설정하세요.


🚩 3단계: Docker 컨테이너에 truststore 볼륨 마운트

미리 준비한 custom-cacerts 파일을 컨테이너 JVM의 truststore 경로에 마운트합니다.

services:
  spring-app:
    image: your-spring-image
    volumes:
      - ./custom-cacerts:/usr/lib/jvm/java-17-openjdk/lib/security/cacerts:ro
  • JVM truststore 경로를 정확히 확인하고 일치시켜야 합니다.


🚩 4단계: JVM 옵션으로 truststore 경로 명시적 설정

JVM이 정확한 truststore 파일을 사용하도록 환경 변수를 설정합니다.

services:
  spring-app:
    image: your-spring-image
    volumes:
      - ./custom-cacerts:/usr/lib/jvm/java-17-openjdk/lib/security/cacerts:ro
    environment:
      - JAVA_OPTS=-Djavax.net.ssl.trustStore=/usr/lib/jvm/java-17-openjdk/lib/security/cacerts -Djavax.net.ssl.trustStorePassword=changeit
  • javax.net.ssl.trustStore: truststore 경로 설정
  • javax.net.ssl.trustStorePassword: truststore 파일 비밀번호 설정