이룸 프로젝트

[이룸] elasticsearch 관련 트러블슈팅 1 - config에 ssl 관련 설정 삽입

쿠마냥 2024. 4. 17. 01:25

[문제]

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'boardDocumentRepository' defined in com.sparta.elasticsearch_practice.BoardDocumentRepository defined in @EnableElasticsearchRepositories declared on ElasticSearchConfig: Failed to instantiate [org.springframework.data.elasticsearch.repository.support.SimpleElasticsearchRepository]: Constructor threw exception

 

Caused by: org.springframework.dao.DataAccessResourceFailureException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

 

Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

 

[해결방안 고심]

1. 먼저, elasticsearch config 클래스와 관련된 문제라고 생각하여 코드를 살펴보았다. 이전 코드는 다음과 같다. 

@Configuration
@EnableElasticsearchRepositories(basePackageClasses = BoardDocumentRepository.class)
public class ElasticSearchConfig extends ElasticsearchConfiguration {

    @Override
    public ClientConfiguration clientConfiguration() {
        return ClientConfiguration.builder().connectedTo("localhost:9200").build();
    }

 

elasticsearch는 8버전부터 https로만 통신이 가능하다. 엘라스틱서치를 통해 주고받는 데이터를 보호하여 사용자 프라이버시를 지키고 안전성을 높이기 위함이다. 따라서 config에 ssl 인증서와 기본 인증을 위한 username과 (엘라스틱서치 설치  당시 부여받은) password가 필요하다. 다음은 수정된 코드이다. 

@Configuration
@EnableElasticsearchRepositories(basePackageClasses = com.sparta.elasticsearce_practice.BoardDocumentRepository.class)
public class ElasticSearchConfig extends ElasticsearchConfiguration {

    @Override
    public ClientConfiguration clientConfiguration() {
        return ClientConfiguration.builder()
                .connectedTo("localhost:9200")
                .usingSsl() // ssl 인증을 사용하겠다는 뜻
                .withBasicAuth("elastic", "paaaasssswoooorrrdd") // username과 password
                .build();
    }
}

 

2. 그럼에도 불구하고 문제가 해결되지 않았다. 인덱스 설정 파일에 문제가 있어도 bean 이슈가 생길 수 있다는 블로그 글을 보고 수정을 진행하였다. 문제 해결에는 도움이 되지 않았으나 인덱스 설정을 이해하는 데 도움이 되었으므로 이곳에 함께 작성한다. 다음은 수정 전 yml 파일이다. 

{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "noriAnalyzer": {
            "type": "custom",
            "tokenizer": "nori_tokenizer"
          }
        },
        "tokenizer": {
          "nori_tokenizer": {
            "type": "nori_tokenizer"
          }
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "content": {
        "type": "text",
        "analyzer": "noriAnalyzer"
      }
    }
  }
}

 

하지만 아직 synonym 파일을 정의하지 않았고 mapping 임의로 작성한 것이기 때문에 아예 기본 설정으로 다시 변경하였다. 

{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "noriAnalyzer": {
            "type": "custom",
            "tokenizer": "nori_tokenizer"
          }
        },
        "tokenizer": {
          "nori_tokenizer": {
            "type": "nori_tokenizer"
          }
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "content": {
        "type": "text",
        "analyzer": "noriAnalyzer"
      }
    }
  }
}

 

3. 문제는 여전히 해결되지 않았다. 에러 로그를 다시 읽다가 드디어 "unable to find valid certification path to requested target"이 눈에 들어왔다. 인증과 관련된 문제임을 인지하고 구글링을 시작하였다.

위에서 이야기했듯이, 엘라스틱서치의 https 통신에는 ssl이 필요하다. 따라서 엘라스틱서치를 처음 다운받으면 SHA-256 해시로 구성된 ssl 인증서 지문(fingerprint)을 제공한다. 이 지문은 서버 인증서의 유효성을 검증하는 데 사용되며, 클라이언트가 서버의 인증서가 신뢰할 수 있는지 확인한다. 

 

또한, Elasticsearch는 자체적으로 http_ca.crt 파일을 발급할 수 있으며, 이 파일에서도 인증서 지문을 추출할 수 있다. fingerprint에서 sslContext를 추출하여 ssl 연결을 구성하도록 로직을 최종 수정하였다. 문제 해결 성공!

@Configuration
@EnableElasticsearchRepositories(basePackageClasses = com.sparta.elasticsearce_practice.BoardDocumentRepository.class)
public class ElasticSearchConfig extends ElasticsearchConfiguration {

    String fingerprint = "${fingerprint}";

    SSLContext sslContext = TransportUtils.sslContextFromCaFingerprint(fingerprint);

    @Override
    public ClientConfiguration clientConfiguration() {
        return ClientConfiguration.builder()
                .connectedTo("localhost:9200")
                .usingSsl(sslContext) // SSL 사용 설정
                .withBasicAuth("elastic", "FJW3vh6q6QyUpvYC8j2B") // 기본 인증을 사용하는 경우
                .build();
    }
}