TIL

[TIL - 20240819] Elasticsearch8.x + Kibana, Docker-compose로 구성, Credential 설정

Chef.Yeon 2024. 8. 19. 04:39

 

💻문제점

공식 사이트를 참고했다.

1. Docker-Compost 파일 작성

.env

ELASTIC_PASSWORD=elastic
KIBANA_PASSWORD=kibana_system
STACK_VERSION=8.7.1
CLUSTER_NAME=docker-cluster
LICENSE=basic
ES_PORT=9200
KIBANA_PORT=5601

 

docker-compose.yml

version: "3.8"

volumes:
  certs:
    driver: local
  esdata01:
    driver: local
  kibanadata:
    driver: local

networks:
  default:
    name: elastic
    external: false

services:
  setup:
    image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
    volumes:
      - certs:/usr/share/elasticsearch/config/certs
    user: "0"
    command: >
      bash -c '
        if [ x${ELASTIC_PASSWORD} == x ]; then
          echo "Set the ELASTIC_PASSWORD environment variable in the .env file";
          exit 1;
        elif [ x${KIBANA_PASSWORD} == x ]; then
          echo "Set the KIBANA_PASSWORD environment variable in the .env file";
          exit 1;
        fi;
        if [ ! -f config/certs/ca.zip ]; then
          echo "Creating CA";
          bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip;
          unzip config/certs/ca.zip -d config/certs;
        fi;
        if [ ! -f config/certs/certs.zip ]; then
          echo "Creating certs";
          echo -ne \
          "instances:\n"\
          "  - name: es01\n"\
          "    dns:\n"\
          "      - es01\n"\
          "      - localhost\n"\
          "    ip:\n"\
          "      - 127.0.0.1\n"\
          "  - name: kibana\n"\
          "    dns:\n"\
          "      - kibana\n"\
          "      - localhost\n"\
          "    ip:\n"\
          "      - 127.0.0.1\n"\
          > config/certs/instances.yml;
          bin/elasticsearch-certutil cert --silent --pem -out config/certs/certs.zip --in config/certs/instances.yml --ca-cert config/certs/ca/ca.crt --ca-key config/certs/ca/ca.key;
          unzip config/certs/certs.zip -d config/certs;
        fi;
        echo "Setting file permissions"
        chown -R root:root config/certs;
        find . -type d -exec chmod 750 \{\} \;;
        find . -type f -exec chmod 640 \{\} \;;
        echo "Waiting for Elasticsearch availability";
        until curl -s --cacert config/certs/ca/ca.crt https://es01:9200 | grep -q "missing authentication credentials"; do sleep 30; done;
        echo "Setting kibana_system password";
        until curl -s -X POST --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_security/user/kibana_system/_password -d "{\"password\":\"${KIBANA_PASSWORD}\"}" | grep -q "^{}"; do sleep 10; done;
        echo "All done!";
      '
    healthcheck:
      test: ["CMD-SHELL", "[ -f config/certs/es01/es01.crt ]"]
      interval: 1s
      timeout: 5s
      retries: 120

  es01:
    depends_on:
      setup:
        condition: service_healthy
    image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
    labels:
      co.elastic.logs/module: elasticsearch
    volumes:
      - certs:/usr/share/elasticsearch/config/certs
      - esdata01:/usr/share/elasticsearch/data
    ports:
      - ${ES_PORT}:9200
    environment:
      - node.name=es01
      - cluster.name=${CLUSTER_NAME}
      - discovery.type=single-node
      - ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
      - bootstrap.memory_lock=true
      - xpack.security.enabled=true
      - ES_JAVA_OPTS=-Xms1g -Xmx1g
      - xpack.security.http.ssl.enabled=true
      - xpack.security.http.ssl.key=certs/es01/es01.key
      - xpack.security.http.ssl.certificate=certs/es01/es01.crt
      - xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt
      - xpack.security.transport.ssl.enabled=true
      - xpack.security.transport.ssl.key=certs/es01/es01.key
      - xpack.security.transport.ssl.certificate=certs/es01/es01.crt
      - xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt
      - xpack.security.transport.ssl.verification_mode=certificate
      - xpack.license.self_generated.type=${LICENSE}
    ulimits:
      memlock:
        soft: -1
        hard: -1
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'",
        ]
      interval: 10s
      timeout: 10s
      retries: 120

  kibana:
    depends_on:
      es01:
        condition: service_healthy
    image: docker.elastic.co/kibana/kibana:${STACK_VERSION}
    labels:
      co.elastic.logs/module: kibana
    volumes:
      - certs:/usr/share/kibana/config/certs
      - kibanadata:/usr/share/kibana/data
    ports:
      - ${KIBANA_PORT}:5601
    environment:
      - SERVERNAME=kibana
      - ELASTICSEARCH_HOSTS=https://es01:9200
      - ELASTICSEARCH_USERNAME=kibana_system
      - ELASTICSEARCH_PASSWORD=${KIBANA_PASSWORD}
      - ELASTICSEARCH_SSL_CERTIFICATEAUTHORITIES=config/certs/ca/ca.crt
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "curl -s -I http://localhost:5601 | grep -q 'HTTP/1.1 302 Found'",
        ]
      interval: 10s
      timeout: 10s
      retries: 120

 

docker-compose up을 통해 컨테이너를 실행한다.

 

2. Nori 분석기 설치

# es container 확인
$ docker ps 

# es컨테이너 접속
$ docker exec -it {containerId} /bin/bash

$ cd /usr/share/elasticsearch/bin/

$ ./elasticsearch-plugin install analysis-nori

 

docker-compose stop > docker-compose up

 

3. Elasticsearch Config 설정

ElasticsearchConfig

@Configuration
@EnableElasticsearchRepositories
public class ElasticsearchConfig extends ElasticsearchConfiguration {
    @Value("${spring.elasticsearch.username}")
    private String username;

    @Value("${spring.elasticsearch.password}")
    private String password;

    @Value("${spring.elasticsearch.host}")
    private String host;

    @Override
    public ClientConfiguration clientConfiguration() {
        return ClientConfiguration.builder()
                .connectedTo(host)
                .usingSsl()
                .withBasicAuth(username, password)
                .build();
    }

    @Bean
    public ElasticsearchClient elasticsearchClient(RestClient restClient) {
        ObjectMapper objectMapper =
                new ObjectMapper()
                        .registerModule(new JavaTimeModule())
                        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        ElasticsearchTransport transport =
                new RestClientTransport(restClient, new JacksonJsonpMapper(objectMapper));
        return new ElasticsearchClient(transport);
    }
}

 

애플리케이션 실행 시, SSL 인증서 등록 관련 에러가 발생했다.

 

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


📃시도

인증서 객체를 생성하여 키스토어에 등록

docker-compose.yml의 setup 서비스 command를 살펴보면, 컨테이너 내에서 SSL/TLS 인증서 및 보안 설정을 구성하여 Elasticsearch와 Kibana가 통신할 수 있도록 한다.

 

여기서 SSL/TLS 인증을 위한 CA를 생성하는 부분이 있다.

echo "Creating CA";
bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip;
unzip config/certs/ca.zip -d config/certs;

 

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

 

이 오류는 SSL 인증서 검증 과정에서 발생하는 문제로, 애플리케이션이 Elasticsearch 서버의 인증서에 대해 신뢰할 수 있는 인증서 경로( PKIX path building failed)를 찾지 못했을 때 발생할 수 있다.

 

이를 해결하기 위해 클라이언트 측에서 해당 인증서를 신뢰하도록 설정해줘야 한다.

 

Java Keystore에 해당 인증서를 추가하자.

Stack Overflow에서 ca.crt를 디코딩하고 인증서 객체를 생성해 KeyStore에 저장 하는 방법을 찾았다.

https://stackoverflow.com/questions/75081596/how-to-configure-security-in-elasticsearch-8-5-3-using-starter-data-elasticsearc

 

How to configure security in elasticsearch 8.5.3 using Starter Data Elasticsearch 3.0.1 in maven java springboot

I was able to create an elasticsearch 8.5.3 server as a docker image, but with security completely disabled, and in my springboot application I am using ElasticsearchRepository to perform insert,up...

stackoverflow.com

elasticsearch 컨테이너의 /usr/share/elasticsearch/config/certs/ca/ca.crt를 열면 다음과 같은 내용이 있다.

-----BEGIN CERTIFICATE-----
MIIDSTCCAjGgAwIBAgIUR1gPSYQEiqCeWaI7ir1hQf9BZ9EwDQYJKoZIhvcNAQEL
(생략)
OL01woHZE+jHFz4i/31pARjL6Fvq8vsrjXJc2Uo=
-----END CERTIFICATE-----

첫 번째 줄(BEGIN)과 마지막 줄(END)를 제외하고 복사하여 application.yml에 추가한다.

나는 환경 변수를 만들어 추가했다.

spring:
  elasticsearch:
    host: ${ELASTIC_HOST}
    username: ${ELASTIC_USER}
    password: ${ELASTIC_PASSWORD}
    client:
      certificate: ${ELASTIC_CA}

 

다음과 같은 방식으로 해도 된다. 개행이 없어야 하기 때문에 '|' 을 추가하여 여러 줄의 텍스트를 하나의 문자열로 취급하도록 한다.

    client:
      certificate: |
        MIIDSTCCAjGgAwIBAgIUR1gPSYQEiqCeWaI7ir1hQf9BZ9EwDQYJKoZIhvcNAQEL
		(생략)
        OL01woHZE+jHFz4i/31pARjL6Fvq8vsrjXJc2Uo=

 

ElasticsearchConfig

@Configuration
@EnableElasticsearchRepositories
public class ElasticsearchConfig extends ElasticsearchConfiguration {
    @Value("${spring.elasticsearch.client.certificate}")
    private String certificateBase64;
    
    @Value("${spring.elasticsearch.username}")
    private String username;

    @Value("${spring.elasticsearch.password}")
    private String password;

    @Value("${spring.elasticsearch.host}")
    private String host;

    @Override
    public ClientConfiguration clientConfiguration() {
        try {
            return ClientConfiguration.builder()
                    .connectedTo("localhost:9200")
                    .usingSsl(getSSLContext())
                    .withBasicAuth(username, password)
                    .build();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Bean
    public ElasticsearchClient elasticsearchClient(RestClient restClient) {
        ObjectMapper objectMapper =
                new ObjectMapper()
                        .registerModule(new JavaTimeModule())
                        .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        ElasticsearchTransport transport =
                new RestClientTransport(restClient, new JacksonJsonpMapper(objectMapper));
        return new ElasticsearchClient(transport);
    }

    private SSLContext getSSLContext() throws Exception {
        byte[] decodedCertificate = Base64.getMimeDecoder().decode(certificateBase64);

        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        X509Certificate ca;
        try (InputStream certificateInputStream = new ByteArrayInputStream(decodedCertificate)) {
            ca = (X509Certificate) certificateFactory.generateCertificate(certificateInputStream);
        }

        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null, null); 
        keyStore.setCertificateEntry("ca", ca);

        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(keyStore);

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, tmf.getTrustManagers(), new java.security.SecureRandom());
        return sslContext;
    }
}

🔍해결

cmd로 인증서 등록

1. ca.crt 로컬에 복사

C드라이브 하위에 elasticsearch 폴더를 생성하고, cmd를 관리자 권한으로 실행했다.

(폴더를 생성하지 않고 C드라이브에 복사해도 된다)

$ docker ps

$ docker cp {es container id}:/usr/share/elasticsearch/config/certs/ca/ca.crt /elasticsearch

elasticsearch 폴더에 ca.crt 파일이 복사된 것을 확인할 수 있다.

 

2. 인증서 등록

자바가 설치된 경로의 lib 폴더로 이동해서 자바 인증서 저장소에 ca.crt를 등록해주자

$ cd {JAVA_HOME}\lib

$ keytool.exe -import -alias {별칭} -keystore "{JAVA_HOME}\lib\security\cacerts" -storepass changeit -file "인증서 경로"

예시: keytool.exe -import -alias es -keystore "{JAVA_HOME}\lib\security\cacerts" -storepass changeit -file "C:\elasticsearch\ca.crt"

 

등록한 인증서를 삭제하고 싶다면 다음 명령을 사용한다.

$ keytool -delete -alias es -keystore "{JAVA_HOME}\lib\security\cacerts" -storepass changeit

 

참고

https://ssow93.tistory.com/73

 

[에러] PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certificati

🚫 SSL 인증서 등록 관련 에러 발생 고객사 쪽의 API를 호출을 하고 있었는데, 이 사이트에 https를 적용하면서 정상적으로 호출이 되지 않고 아래와 같은 에러가 발생하였다. 이에 관련해서 조치

ssow93.tistory.com

 

728x90