자바 애플리케이션은 운영체제의 신뢰 저장소를 보지 않고 JDK 가 들고 있는 cacerts 를 본다. 브라우저에서는 정상인 사내 HTTPS 가 자바 클라이언트에서만 PKIX path building failed · unable to find valid certification path to requested target 로 실패하는 이유가 이것이다.
cacerts 위치는 JDK 버전에 따라 다르다.
| 버전 | 경로 |
|---|---|
| JDK 8 | $JAVA_HOME/jre/lib/security/cacerts |
| JDK 9 이상 | $JAVA_HOME/lib/security/cacerts |
# 실제로 쓰이는 JDK 확인
readlink -f "$(command -v java)"
alternatives --display java # RHEL 계열
기본 비밀번호는 changeit 이다. 여러 JDK 가 설치된 서버에서는 애플리케이션이 실제로 쓰는 JDK 의 파일을 고쳐야 한다. 이것을 착각해 "분명히 넣었는데 안 된다"가 되는 경우가 가장 많다.
# 백업 먼저
cp -a "$JAVA_HOME/lib/security/cacerts" "$JAVA_HOME/lib/security/cacerts.$(date +%F)"
keytool -import -trustcacerts -noprompt \
-keystore "$JAVA_HOME/lib/security/cacerts" \
-storepass changeit \
-alias corp-root-ca \
-file root.crt
확인과 삭제는 다음과 같다.
keytool -list -keystore "$JAVA_HOME/lib/security/cacerts" -storepass changeit | grep -i corp
keytool -list -v -keystore "$JAVA_HOME/lib/security/cacerts" -storepass changeit -alias corp-root-ca
keytool -delete -keystore "$JAVA_HOME/lib/security/cacerts" -storepass changeit -alias corp-root-ca
변경은 JVM 시작 시점에 읽히므로 애플리케이션을 다시 띄워야 반영된다.
별칭(alias)은 키스토어 안에서 유일해야 한다. 루트와 중간 CA 를 넣는다면 각각 별도 별칭으로 따로 넣는다.
keytool -import -trustcacerts -noprompt -keystore "$JAVA_HOME/lib/security/cacerts" \
-storepass changeit -alias corp-root-ca -file root.crt
keytool -import -trustcacerts -noprompt -keystore "$JAVA_HOME/lib/security/cacerts" \
-storepass changeit -alias corp-sub-ca -file subca.crt
여러 인증서를 이어 붙인 파일을 한 별칭으로 넣으면 첫 번째 인증서만 들어가거나 형식 오류가 난다. TrustStore 에 체인 파일을 한 번에 넣는 방식은 쓰지 않는다.
keytool error: java.lang.Exception: Input not an X.509 certificate 는 파일이 인증서가 아니거나 형식이 맞지 않다는 뜻이다.
# 내용이 읽히는지부터 확인
openssl x509 -in cert.crt -noout -subject -issuer
# DER 이면 PEM 으로 변환
openssl x509 -inform DER -in cert.crt -out cert.pem
흔한 원인은 세 가지다. 개인키나 CSR 파일을 잘못 지정한 경우, 복사하다 -----BEGIN CERTIFICATE----- 머리말이 깨진 경우, PKCS#12(.pfx · .p12) 파일을 인증서로 넘긴 경우다. PKCS#12 는 -import 가 아니라 -importkeystore 로 다룬다.
keytool -importkeystore -srckeystore server.pfx -srcstoretype PKCS12 \
-destkeystore server.jks -deststoretype PKCS12
JDK 9 부터 키스토어 기본 형식이 PKCS12 로 바뀌었다. 확장자가 .jks 라도 내용은 PKCS12 일 수 있으므로 keytool -list 에 나오는 키 저장소 유형(Keystore type)을 확인한다. 애플리케이션이 Invalid keystore format 을 낸다면 형식과 기대값이 어긋난 것이다.
keytool -list -keystore server.jks -storepass <PASSWORD> | head -3
# 형식 변환
keytool -importkeystore -srckeystore server.jks -srcstoretype JKS \
-destkeystore server.p12 -deststoretype PKCS12
읽는 쪽이 JKS 만 지원하는 오래된 제품이면 -deststoretype JKS 로 만든다.
공용 JDK 의 cacerts 를 고치기 어려우면 애플리케이션 전용 TrustStore 를 만들어 기동 옵션으로 지정한다. JDK 업데이트로 cacerts 가 교체돼도 설정이 살아남는다는 이점이 있다.
keytool -import -trustcacerts -noprompt -keystore /opt/app/conf/truststore.p12 \
-storetype PKCS12 -storepass "${TRUSTSTORE_PASSWORD}" \
-alias corp-root-ca -file root.crt
java -Djavax.net.ssl.trustStore=/opt/app/conf/truststore.p12 \
-Djavax.net.ssl.trustStorePassword="${TRUSTSTORE_PASSWORD}" \
-Djavax.net.ssl.trustStoreType=PKCS12 \
-jar app.jar
전용 TrustStore 를 지정하면 기본 cacerts 는 사용되지 않는다. 공인 CA 로 나가는 통신도 함께 쓴다면 필요한 공인 루트까지 이 파일에 넣어야 한다.
핸드셰이크가 왜 실패하는지 보려면 디버그를 켠다.
java -Djavax.net.debug=ssl:handshake:trustmanager -jar app.jar