Error/개발환경 / / 2024. 9. 6. 18:55

[ElasticSearch] Spring data elasticSearch 인증 관련 오류 해결

개요

엘라스틱 서치 환경을 구축하고, Spring Data Elasticsearch를 연결하는데 문제가 발생하였다.

그 이유는 바로 로컬에서 테스트 할 때 와는 다르게 실서버에서는 인증(https, 인증서, Authorization)이 필요했다.

하지만 해당 객체를 통해 해결할 수 있다는 기본적인 설정이 있었지만 https와 cert 인증서를 포함하여 Authorization을 모두 포함하는 코드는 찾아볼 수 없었다. 결국 하나씩 디버깅을 해가며 원인 분석을 한 내용을 공유하고자 한다.

추가로 Spring data Elasticsearch는 처음 bean이 컨테이너에 생성되면서 handshake를 하게 되는데 이때 elasticsearch의 health를 하게 되는데 네트워크 in/out bound도 확인해야 할 필요성이 있다.

curl https://<host-in-other-env>:<port>/_cluster/health/<your-index>

해당 명령어를 통해 정책을 확인 후 아래 글을 읽는걸 추천한다.

1.연결이 안되는 문제

   @Configuration
   public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {

       @Override
       public RestHighLevelClient elasticsearchClient() {
            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.add("Authorization", apiKey);
            ClientConfiguration config = ClientConfiguration.builder()
               .connectedTo(new InetSocketAddress(host, port))
               .withDefaultHeaders(httpHeaders)
               .build();
            return RestClients.create(config).rest();
       }

 

@Bean
public RestHighLevelClient client() {
    final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
    credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(id, password));

    RestClientBuilder builder = RestClient.builder(new HttpHost(host, port, "http"))
       .setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)
          // API Key entity 추가
          .addInterceptorFirst((HttpRequestInterceptor) (request, context) ->
             request.addHeader("Authorization", apiKey)
          ));

    return new RestHighLevelClient(builder);
}
HttpHost httpHost = new HttpHost(host, port, "http");

Header[] headers = new Header[]{new BasicHeader("Authorization", apiKey)};


RestClientBuilder builder = RestClient.builder(httpHost)
    .setRequestConfigCallback(
       requestConfigBuilder -> requestConfigBuilder
          .setConnectTimeout(30000)
          .setSocketTimeout(300000))
    .setHttpClientConfigCallback(
       httpClientBuilder -> httpClientBuilder
          .setConnectionReuseStrategy((response, context) -> true)
          .setKeepAliveStrategy(((response, context) -> 300000))
          .setDefaultHeaders(Arrays.asList(headers))
          .setDefaultIOReactorConfig(IOReactorConfig.custom()
             .setIoThreadCount(4)
             .build())

    );

return new RestHighLevelClient(builder);

위 3가지 설정은 기본적으로 조금씩 다르지만 하고자 하는 것은 비슷하다. 하지만 3가지 코드 다 서버와 연결하는데 실패했다.위 코드들에서 발생한 에러는 동일하다.  Connection is closed 그 이유를 알지 못했고, 문제 원인을 찾기위해 디버깅을 시도했다.

연결 실패 오류

Connection is closed

org.apache.http.ConnectionClosedException: Connection is closed

java.util.concurrent.ExecutionException: org.apache.http.ConnectionClosedException: Connection is closed

java.util.concurrent.ExecutionException: org.apache.http.ConnectionClosedException: Connection is closed; nested exception is ElasticsearchException[java.util.concurrent.ExecutionException: org.apache.http.ConnectionClosedException: Connection is closed]; nested: ExecutionException[org.apache.http.ConnectionClosedException: Connection is closed]; nested: ConnectionClosedException[Connection is closed];

Failed to instantiate [org.springframework.data.elasticsearch.repository.support.SimpleElasticsearchRepository]: Constructor threw exception; nested exception is org.springframework.data.elasticsearch.UncategorizedElasticsearchException: java.util.concurrent.ExecutionException: org.apache.http.ConnectionClosedException: Connection is closed; nested exception is ElasticsearchException[java.util.concurrent.ExecutionException: org.apache.http.ConnectionClosedException: Connection is closed]; nested: ExecutionException[org.apache.http.ConnectionClosedException: Connection is closed]; nested: ConnectionClosedException[Connection is closed];

 

문제 해결 1

       HttpHost httpHost = new HttpHost(host, port, "https");
//     HttpHost httpHost = new HttpHost(host, port, "http");

결과부터 말하자면 허무하게도 https로 요청하지 않았기 때문에 발생한 문제였다. 네트워크 보안 상 http 요청은 차단하고 있었기에 발생하였고, https로 연결하자 위 문제가 해결되었다.  하지만 아래와 같은 오류가 발생하였다.

 

SSL 인증서 오류

인증서 오류는 아래와 같으며, 해결 방법또한 간단하다. Certificate를 설정해주고, 해당 설정을 적용하면 된다.

unable to find valid certification path to requested target

PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target; nested exception is ElasticsearchException[java.util.concurrent.ExecutionException: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target]; nested: ExecutionException[javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target]; nested: SSLHandshakeException[PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target]; nested: ValidatorException[PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target]; nested: SunCertPathBuilderException[unable to find valid certification path to requested target];

 

Path caCertificatePath = Paths.get("your_path/ca.crt");
CertificateFactory factory;
try {
    factory = CertificateFactory.getInstance("X.509");
} catch (CertificateException e) {
    throw new RuntimeException(e);
}
Certificate trustedCa;
try (InputStream is = Files.newInputStream(caCertificatePath)) {
    trustedCa = factory.generateCertificate(is);
}
KeyStore trustStore = KeyStore.getInstance("pkcs12");
trustStore.load(null, null);
trustStore.setCertificateEntry("ca", trustedCa);
SSLContextBuilder sslContextBuilder = SSLContexts.custom()
    .loadTrustMaterial(trustStore, null);
final SSLContext sslContext = sslContextBuilder.build();
RestClientBuilder builder = RestClient.builder(httpHost)
    .setRequestConfigCallback(
       requestConfigBuilder -> requestConfigBuilder
          .setConnectTimeout(30000)
          .setSocketTimeout(300000))
    .setHttpClientConfigCallback(
       httpClientBuilder -> httpClientBuilder
          .setConnectionReuseStrategy((response, context) -> true)
          .setSSLContext(sslContext)
          .setKeepAliveStrategy(((response, context) -> 300000))
          .setDefaultHeaders(Arrays.asList(headers))
          .setDefaultIOReactorConfig(IOReactorConfig.custom()
             .setIoThreadCount(4)
             .build())

    );

return new RestHighLevelClient(builder);

ca 인증서 파일의 종류는 다양하게 지원한다. p12, crt 등...

공식 사이트를 참조하여 본인이 편한 스타일로 개발하면 된다.

 

사설인증서 오류

서버에서는 사설 인증서를 사용하고 있기 때문에 사설인증서를 사용하는 경우 verify 검증 옵션을 꺼야한다.

아래와 같은 오류가 나타난다면 setSSLHostnameVerifier 속성을 NoopHostnameVerifier를 사용하여 끌 수 있다.

Host name 'localhost' does not match the certificate subject provided by the peer

javax.net.ssl.SSLPeerUnverifiedException: Host name 'localhost ' does not match the certificate subject provided by the peer

java.util.concurrent.ExecutionException: javax.net.ssl.SSLPeerUnverifiedException: Host name 'localhost' does not match the certificate subject provided by the peer

    .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)

 

결론) 최종 코드

public RestHighLevelClient elasticsearchClient() {
    Path caCertificatePath = Paths.get(certPath);
    CertificateFactory factory;
    try {
       factory = CertificateFactory.getInstance("X.509");
    } catch (CertificateException e) {
       throw new RuntimeException(e);
    }
    Certificate trustedCa;
    try (InputStream is = Files.newInputStream(caCertificatePath)) {
       trustedCa = factory.generateCertificate(is);
    }
    KeyStore trustStore = KeyStore.getInstance("pkcs12");
    trustStore.load(null, null);
    trustStore.setCertificateEntry("ca", trustedCa);
    SSLContextBuilder sslContextBuilder = SSLContexts.custom()
       .loadTrustMaterial(trustStore, null);
    final SSLContext sslContext = sslContextBuilder.build();

    HttpHost httpHost = new HttpHost(host, port, "https");

    Header[] headers = new Header[]{new BasicHeader("Authorization", apiKey)};


    RestClientBuilder builder = RestClient.builder(httpHost)
       .setRequestConfigCallback(
          requestConfigBuilder -> requestConfigBuilder
             .setConnectTimeout(30000)
             .setSocketTimeout(300000))
       .setHttpClientConfigCallback(
          httpClientBuilder -> httpClientBuilder
             .setConnectionReuseStrategy((response, context) -> true)
             .setSSLContext(sslContext)
             .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
             .setKeepAliveStrategy(((response, context) -> 300000))
             .setDefaultHeaders(Arrays.asList(headers))
             .setDefaultIOReactorConfig(IOReactorConfig.custom()
                .setIoThreadCount(4)
                .build())

       );

    return new RestHighLevelClient(builder);

해당 문제를 간단하게 풀었지만, 간단하지 않았다. 그 이유는 다양한 요인이 버그의 요소가 될 수 있었고, ES를 구축한 서버에서는 id/pw와 인증서 방식 등 다양한 방식의 인증을 거쳐야했다. in/out bound 등 신경 써야할 요소들이 많아 디버깅이 쉽지 않았다. 로컬에서 Docker를 통해 ElasticSearch를 구축하여 프로젝트를 만드는 건 여러번 시도했지만, 역시 실 운영 서버에 적용하는데는 고려해야 할 보안 사항들이 많이 있다고 다시 한번 느끼게 됐다.

 

Refference

아래 사이트를 참고하였다.

https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/_basic_authentication.html

 

Basic authentication | Elasticsearch Java API Client [8.15] | Elastic

Configuring basic authentication can be done by providing an HttpClientConfigCallback while building the RestClient through its builder. The interface has one method that receives an instance of org.apache.http.impl.nio.client.HttpAsyncClientBuilder as an

www.elastic.co

https://stackoverflow.com/questions/59379279/2-1-6-spring-boot-elasticsearch-healthcheck-failure

 

2.1.6 Spring Boot - Elasticsearch Healthcheck failure

***Update - I found a useful StackOverflow post where other people were having similar issues with the Healthcheck monitor failing for Elasticsearch Springboot elastic search health management :

stackoverflow.com

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유