[트러블슈팅] PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException 해결

LTT·2025년 9월 29일

에러 분석


https 통신을 위해 로컬에서 자체 서명 인증서를 사용하여 개발하고 있었다.

WebClient로 다른 서버를 호출하니 위와 같이 에러가 발생했다.

javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:131)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:378)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:321)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:316)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1351)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.onConsumeCertificate(CertificateMessage.java:1226)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.consume(CertificateMessage.java:1169)
	at java.base/sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:396)
<28 folded frames>

여기서 핵심적으로 볼 파트는 여기다.

unable to find valid certification path to requested target

요청한 서버의 SSL 인증서 검증 과정에서 서버 인증서를 신뢰할 수 없어서 발생하는 문제이다.

즉, WebClient가 요청을 보내는 대상 서버의 SSL 인증서가 신뢰할 수 있는 CA(Certificate Authority)로 서명되어 있지 않거나, 로컬 JVM의 cacerts(truststore)에 해당 인증서 체인이 등록되어 있지 않아서 생긴 문제이다.

문제 해결


@Configuration
@RequiredArgsConstructor
public class WebClientConfig {
    private final String API_KEY_HEADER = "X-API-KEY";
    
    @Bean(name = "formatWebClient")
    public WebClient formatWebClient() throws SSLException {
		    // --- 이부분 추가 ---
        SslContext sslContext = SslContextBuilder
                .forClient()
                .trustManager(InsecureTrustManagerFactory.INSTANCE)
                .build();

        HttpClient httpClient = HttpClient.create()
                .secure(ssl -> ssl.sslContext(sslContext));
		    // ---

        return WebClient.builder()
                .baseUrl("<Other Server's baseURL>")
                .defaultHeader(API_KEY_HEADER, apiKeyProvider.getEncryptedApiKey())
						    // --- 이부분 추가 ---
                .clientConnector(new ReactorClientHttpConnector(httpClient))
						    // ---
                .filter((request, next) -> next.exchange(request)
                        .flatMap(response -> {
                            if (response.statusCode().isError()) {
                                return response.bodyToMono(String.class)
                                        .defaultIfEmpty("Unknown error")
                                        .flatMap(body -> {
                                            log.error("API Error Response: {}", body);
                                            return Mono.error(new RuntimeException("INTERNAL_SERVER_ERROR"));
                                        });
                            } else {
                                return Mono.just(response);
                            }
                        })
                )
                .build();
    }
}

위와 같이 WebClientConfig를 설정해주었다. 물론 권장되는 방법은 아니다.

SslContext sslContext = SslContextBuilder
        .forClient()
        .trustManager(InsecureTrustManagerFactory.INSTANCE)
        .build();

HttpClient httpClient = HttpClient.create()
        .secure(ssl -> ssl.sslContext(sslContext));

위의 코드는 코드레벨에서 SSL 검증을 무시하는 코드이다. Spring WebClient에 HttpClient를 커스터마이징해서 SSL 검증을 끈 것이다. 즉, 모든 SSL 인증서를 신뢰한다는 것이다.

이런 코드가 보안상 좋을 리가 없다. 그래서 테스트 또는 내부망에서만 사용해야 한다.

호출할 서버가 현재 이 서버만 거쳐서(내부망에서만 이용) 이렇게 작성하고 끝내도 괜찮았다.

하지만 JAVA 인증서 저장소에 인증서를 등록하여 해결하고 싶다면

https://ssow93.tistory.com/73

위의 블로그 글을 참고해봐도 괜찮을 것 같다.

Reference


질문과 피드백은 언제나 환영입니다 :)

profile
개발자에서 엔지니어로, 엔지니어에서 리더로

0개의 댓글