programming language/Kakfa

[Kafka 활용] Kafka 심화 3 - 보안 적용 : Kafka SSL, SASL, ACL

공대키메라 2026. 7. 17. 13:51

지난 시간에는 Kafak2 심화 과정으로 공식문서에서 권장하는 운영을 위한 성능과 prometheus 랑 Grafana를 세팅해서 실제로 모니터링이 가능한지 알아보았다.

 

[Kafka 활용] Kafka 심화 2 - 안정적인 운영을 위한 지식 + 모니터링 도구 설정

 

이번 시간에는 Kafka 에서 소개하는 보안 설정들을 알아보고 이것을 적용해 볼 것이다.

 


목표

1. Kafka Security에 대해 알아본다.

3. Kakfa 보안 실습 환경을 구축 및 SSL을 적용한다.

3. Kafka SASL, ACL이 뭔지 이해한다.

 


0. 들어가기 전에

해당 내용을 이해하기 전에 SSL에 대해서 정리를 하고자 한다.

 

필자는 생활코딩의HTTPS와 SSL 인증서를 참고했다.

 

정리한 내용은 다음에서 읽어보길 바란다.([Security 기초] HTTPS와 SSL/TLS이해와 실습 (feat. 암호화 기초))

 

해당 글을 읽기 전에 kafka 구조를 다시 보고 가자.

 

kraft 를 도입한 kafka 구성도. producer -> kafka cluster -> consumer 전반 흐름 도식화

 

 

여기서 과연 어디어디에 보안을 적용해야 하는가?

 

만약 AWS 를 사용하는 입장이라면? 

 

이미 private subnet내부에서 kafka broker들 끼리 응답하는데 보안 적용이 필요할지는 의문이다.

 

내부망이라면 Security Group 기반 네트워크 접근 제어로 끝내면 되는 것이고,

외부망을 타야만 한다명 카프카 자체 설정으로 SSL/TLS(암호화)와 SASL(인증)을 걸어준다.

 

필자의 경우에는 학습을 목적으로 하니 다 적용할 예정이다.

 

1. Kafka Security에 대한 공식문서 정보

필자가 Kafka Security라고 한 이유는 공식문서에서 그렇게 소개가 되어있기 때문이다.

 

https://kafka.apache.org/43/security/security-overview/

 

물론 overview라고 하긴 하는데 공식문서는 너무 딱딱하고 눈에 들어오지 않는다. 

 

지원하는 기능에만 치우쳐서 설명하는 느낌?

 

Kafka에서 제공하는 Security기능은 세 가지로 분류가 가능한데 암호화, 인증 그리고 인가 처리다. 

 

(1) 암호화는 SSL(Secure Socket Layer)을,

 

(2) 인증은 SASL(Simple Authentication and Security Layer)

 

(3) 인가 혹은 권한은 ACL(Acces Control List)을 이용한다. 

 

 

암호화가 필요한 경우는 외부에서 내부로 접근하는 경우를 들 수 있다. 

 

즉, 카프카와 카프카 클라이언트 사이의 통신 내용을 중간에 가로채는 경우 필요하다.

 

 

인증의 경우는 신원을 확인하기 위해 Kafka에 적용할 수 있는 기능으로 SASL을 제공하는데 위의 공식문서에 따르면

 

SASL/GSSAPI, SASL/PLAIN 등 여러개를 지원한다. 

 

 

인가/권한은 접근 제어 리스트(ACL - Access Control List)를 쓰면 되는데 접속한 사람이 특정 토픽에서 어떤 메시지를 가져갈 수 있는지 권한을 할당해서 문제점을 사전에 방지한다. 

 

2.  SSL을 이용한 카프카 암호화 적용

 

실습을 위해 필자가 사용하고 있는 docker-kraft-compose.yml 파일을 보자.

 

docker-kraft-compose.yml 

networks:
  kafka-network:
    name: kafka-kraft-net
    driver: bridge

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    networks:
      - kafka-network

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_USERS_ALLOW_SIGN_UP=false
    volumes:
      - grafana-data:/var/lib/grafana
    networks:
      - kafka-network
    depends_on:
      - prometheus

  kafka-kraft-1:
    image: apache/kafka:latest
    container_name: kafka-kraft-1
    user: root
    ports:
      - "19092:19092"
      - "9404:9404"
    environment:
      CLUSTER_ID: 'O1N_F_D-S2K6V8-A_G-p-A'
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: 'controller,broker'
      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka-kraft-1:9093,2@kafka-kraft-2:9093,3@kafka-kraft-3:9093'
      
      # [보안 변경] 모든 네트워크망 리스너 프로토콜을 SSL로 변경
      KAFKA_LISTENERS: 'INTERNAL://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093,EXTERNAL://0.0.0.0:19092'
      KAFKA_ADVERTISED_LISTENERS: 'INTERNAL://kafka-kraft-1:9092,EXTERNAL://localhost:19092'
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:SSL,INTERNAL:SSL,EXTERNAL:SSL'
      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
      KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'
      
      # [SSL 추가] 1번 브로커 전용 Keystore 및 공통 Truststore 경로와 비밀번호 주입
      KAFKA_SSL_TRUSTSTORE_LOCATION: '/etc/kafka/secrets/kafka.truststore.jks'
      KAFKA_SSL_TRUSTSTORE_PASSWORD: 'kafkapassword'
      KAFKA_SSL_KEYSTORE_LOCATION: '/etc/kafka/secrets/kafka-1.keystore.jks'
      KAFKA_SSL_KEYSTORE_PASSWORD: 'kafkapassword'
      KAFKA_SSL_KEY_PASSWORD: 'kafkapassword'

      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
      KAFKA_JMX_OPTS: "-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=0.0.0.0 -Dcom.sun.management.jmxremote.rmi.port=9991 -Dcom.sun.management.jmxremote.port=9991"
      KAFKA_OPTS: "-javaagent:/opt/jmx_exporter/jmx_prometheus_javaagent.jar=9404:/opt/jmx_exporter/kafka-kraft.yml"
    volumes:
      - kafka-kraft-1-data:/tmp/kraft-combined-logs
      - ./jmx_prometheus_javaagent.jar:/opt/jmx_exporter/jmx_prometheus_javaagent.jar
      - ./kafka-kraft.yml:/opt/jmx_exporter/kafka-kraft.yml
      # [볼륨 추가] 로컬에서 만든 1번 노드 전용 인증서 파일을 컨테이너 내부로 읽기전용(ro) 마운트
      - ./ssl/kafka.truststore.jks:/etc/kafka/secrets/kafka.truststore.jks:ro
      - ./ssl/kafka-1.keystore.jks:/etc/kafka/secrets/kafka-1.keystore.jks:ro
    networks:
      - kafka-network

  kafka-kraft-2:
    image: apache/kafka:latest
    container_name: kafka-kraft-2
    user: root
    ports:
      - "29092:29092"
      - "9405:9405"
    environment:
      CLUSTER_ID: 'O1N_F_D-S2K6V8-A_G-p-A'
      KAFKA_NODE_ID: 2
      KAFKA_PROCESS_ROLES: 'controller,broker'
      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka-kraft-1:9093,2@kafka-kraft-2:9093,3@kafka-kraft-3:9093'
      
      # [보안 변경] 모든 네트워크망 리스너 프로토콜을 SSL로 변경
      KAFKA_LISTENERS: 'INTERNAL://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093,EXTERNAL://0.0.0.0:29092'
      KAFKA_ADVERTISED_LISTENERS: 'INTERNAL://kafka-kraft-2:9092,EXTERNAL://localhost:29092'
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:SSL,INTERNAL:SSL,EXTERNAL:SSL'
      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
      KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'
      
      # [SSL 추가] 2번 브로커 전용 Keystore 및 공통 Truststore 경로와 비밀번호 주입
      KAFKA_SSL_TRUSTSTORE_LOCATION: '/etc/kafka/secrets/kafka.truststore.jks'
      KAFKA_SSL_TRUSTSTORE_PASSWORD: 'kafkapassword'
      KAFKA_SSL_KEYSTORE_LOCATION: '/etc/kafka/secrets/kafka-2.keystore.jks'
      KAFKA_SSL_KEYSTORE_PASSWORD: 'kafkapassword'
      KAFKA_SSL_KEY_PASSWORD: 'kafkapassword'

      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
      KAFKA_JMX_OPTS: "-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=0.0.0.0 -Dcom.sun.management.jmxremote.rmi.port=9992 -Dcom.sun.management.jmxremote.port=9992"
      KAFKA_OPTS: "-javaagent:/opt/jmx_exporter/jmx_prometheus_javaagent.jar=9405:/opt/jmx_exporter/kafka-kraft.yml"
    volumes:
      - kafka-kraft-2-data:/tmp/kraft-combined-logs
      - ./jmx_prometheus_javaagent.jar:/opt/jmx_exporter/jmx_prometheus_javaagent.jar
      - ./kafka-kraft.yml:/opt/jmx_exporter/kafka-kraft.yml
      # [볼륨 추가] 로컬에서 만든 2번 노드 전용 인증서 파일을 컨테이너 내부로 읽기전용(ro) 마운트
      - ./ssl/kafka.truststore.jks:/etc/kafka/secrets/kafka.truststore.jks:ro
      - ./ssl/kafka-2.keystore.jks:/etc/kafka/secrets/kafka-2.keystore.jks:ro
    networks:
      - kafka-network

  kafka-kraft-3:
    image: apache/kafka:latest
    container_name: kafka-kraft-3
    user: root
    ports:
      - "39092:39092"
      - "9406:9406"
    environment:
      CLUSTER_ID: 'O1N_F_D-S2K6V8-A_G-p-A'
      KAFKA_NODE_ID: 3
      KAFKA_PROCESS_ROLES: 'controller,broker'
      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka-kraft-1:9093,2@kafka-kraft-2:9093,3@kafka-kraft-3:9093'
      
      # [보안 변경] 모든 네트워크망 리스너 프로토콜을 SSL로 변경
      KAFKA_LISTENERS: 'INTERNAL://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093,EXTERNAL://0.0.0.0:39092'
      KAFKA_ADVERTISED_LISTENERS: 'INTERNAL://kafka-kraft-3:9092,EXTERNAL://localhost:39092'
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:SSL,INTERNAL:SSL,EXTERNAL:SSL'
      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
      KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'
      
      # [SSL 추가] 3번 브로커 전용 Keystore 및 공통 Truststore 경로와 비밀번호 주입
      KAFKA_SSL_TRUSTSTORE_LOCATION: '/etc/kafka/secrets/kafka.truststore.jks'
      KAFKA_SSL_TRUSTSTORE_PASSWORD: 'kafkapassword'
      KAFKA_SSL_KEYSTORE_LOCATION: '/etc/kafka/secrets/kafka-3.keystore.jks'
      KAFKA_SSL_KEYSTORE_PASSWORD: 'kafkapassword'
      KAFKA_SSL_KEY_PASSWORD: 'kafkapassword'

      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
      KAFKA_JMX_OPTS: "-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=0.0.0.0 -Dcom.sun.management.jmxremote.rmi.port=9993 -Dcom.sun.management.jmxremote.port=9993"
      KAFKA_OPTS: "-javaagent:/opt/jmx_exporter/jmx_prometheus_javaagent.jar=9406:/opt/jmx_exporter/kafka-kraft.yml"
    volumes:
      - kafka-kraft-3-data:/tmp/kraft-combined-logs
      - ./jmx_prometheus_javaagent.jar:/opt/jmx_exporter/jmx_prometheus_javaagent.jar
      - ./kafka-kraft.yml:/opt/jmx_exporter/kafka-kraft.yml
      # [볼륨 추가] 로컬에서 만든 3번 노드 전용 인증서 파일을 컨테이너 내부로 읽기전용(ro) 마운트
      - ./ssl/kafka.truststore.jks:/etc/kafka/secrets/kafka.truststore.jks:ro
      - ./ssl/kafka-3.keystore.jks:/etc/kafka/secrets/kafka-3.keystore.jks:ro
    networks:
      - kafka-network

volumes:
  kafka-kraft-1-data:
  kafka-kraft-2-data:
  kafka-kraft-3-data:
  grafana-data:

 

docker-compose 파일 구성하는게 솔직히 너무 복잡해서 금방 까먹는다.

 

다만 세상이 좋아져서 원하는 것에 대해서 AI로 작성하고 스스로 검증까지 시키니 수월하게 할 수 있다.

 

 

우선 각 브로커마다 프라이빗 키와 인증서를 만들어야 한다.

 

SSL 인증서는 '도메인(호스트네임)'과 1:1로 묶이기 때문인데, 카프카 클러스터는 논리적인 하나의 그룹일 뿐 실제로는 각 브로커가 독립된 IP나 도메인을 가지기 때문이다. 

 

또한 브로커들끼리도 서로 통신하고 검증해야하기 때문이다. 

 

 

여기서 키스토어와 트러스트 스토어가 나온다. 

 

키스토어?

 

나를 증명하는 신분증 지갑! 즉, 자신의 신분을 상대방에게 증명하기 위해 사용하는 파일이다.

 

서버 측면에서 프라이빗 키와 인증서를 저장, 자격 증명을 제공. 그 외에 프라이빗하고 민감한 정보를 저장. 

 

트러스트 스토어?

 

상대방이 제시한 신분증(인증서)이 위조되지 않은 진짜인지 검증하기 위해 사용하는 파일이다

 

일반적으로 클라이언트 측면에서 서버가 제공하는 인증서를 검증하기 위한 퍼블릭 키와 서버와 SSL연결에서 유효성을 검사하는 서명된 인증서를 저장하며, 민감한 정보는 저장하지 않는다.

 

간단히 정리하면 키스토어는 서버 쪽, 트러스트스토어는 클라이언트쪽 검증용이라고 생각하자.

 

 

어찌되었든 keystore를 broker내부에 일일히 만들어야 한다. broker가 누구인지 증명하기 위함이다.

 

또한 트러스트 스토어를 만들어야 한다. kafka cluster내부에 모든 broker들이 동일한 CA 인증서를 신뢰하도록 하기 위함이다.


순서는 (1) CA 인증서 생성 -> (2) truststore생성 -> (3)keystore를 각각 docker의 이름에 맞게 생성 한다.

 

그 후 docker-kafka-compose.yml를 이용해 docker를 올리면 된다.

 

그림은 어떻게 되냐? 다음을 보자!

초록, 노랑 상자와 빨간줄을 보세요!

 

docker내부에 keystore를 생성하게 되면 docker는 소모성인데 자꾸 날아가버리니 미리 만들어서 volumn으로 연결하는게 좋다고 한다. (Gemini 고마워!)

 

해당 부분은 전혀 생각하지 못햇던 부분인데 Gemini가 또 알아서 잘 구성해줬다. 

 

한 번 읽어보면 좋을 듯 하다.


 

상호 인증(Mutual TLS, mTLS) 구조를 가집니다. 브로커들은 통신 과정에서 동시에 서버이자 클라이언트의 역할을 수행합니다.

예를 들어, 리더 선출이나 데이터 복제를 위해 브로커 1이 브로커 2에게 연결을 시도하는 과정을 분해해보면 다음과 같습니다.

  1. 브로커 2 (서버 역할): 자신의 Keystore에서 인증서를 꺼내 브로커 1에게 보여줍니다.
  2. 브로커 1 (클라이언트 역할): 자신의 Truststore를 참조하여, 브로커 2의 인증서에 찍힌 CA 도장이 유효한지 검증합니다.
  3. 브로커 1 (클라이언트 역할): 서버 검증이 끝나면, 이번에는 역으로 자신의 Keystore에서 인증서를 꺼내 브로커 2에게 제시합니다.
  4. 브로커 2 (서버 역할): 자신의 Truststore를 참조하여, 접근을 요청한 브로커 1의 인증서에 찍힌 CA 도장이 유효한지 검증합니다.

이렇게 양방향으로 신원 검증이 완벽히 끝나야만 비로소 데이터를 주고받을 수 있습니다. 외부망에서의 접근(Client to Broker)뿐만 아니라, 브로커 간 내부망 통신에서도 제로 트러스트(Zero Trust) 관점의 높은 보안 수준을 유지하기 위해 이러한 구조가 채택됩니다.


 

sudo apt update
sudo apt install openjdk-17-jdk -y

 

중도에 실패하면 ssl 디렉토리와 미완성 찌꺼지를 싸그리 제거하고자 한다면? 다음 명령어를 치면 된다.

 

rm -rf ssl

 

generate-ssl.sh

#!/bin/bash
set -e

# JKS(Java KeyStore) 파일들을 암호화할 때 사용할 공통 비밀번호입니다.
PASSWORD="kafkapassword"
# 인증서 유효기간 (3650일 = 약 10년)
VALIDITY=3650

# 결과물을 모아둘 디렉토리 생성 후 이동
mkdir -p ssl
cd ssl

# ===================================================================================
# 1단계: 나만의 사설 인증기관(CA, Certificate Authority) 구축
# ===================================================================================
# 카프카 노드들이 서로를 신뢰하려면 누군가 "이 노드는 진짜 카프카 노드가 맞다"고 보증을 서야 합니다.
# 실제 운영 환경이라면 AWS ACM이나 Let's Encrypt 같은 공인 기관을 쓰겠지만, 
# 로컬 개발 환경에서는 OpenSSL을 이용해 직접 '사설 인증기관(CA)'을 만듭니다.
echo "[1/4] 최상위 사설 인증기관(CA) 생성 중..."
openssl req -new -x509 -keyout ca-key -out ca-cert -days $VALIDITY -passout pass:$PASSWORD \
    -subj "/C=KR/ST=Seoul/L=Gangnam/O=Kimera/OU=Dev/CN=Kafka-CA"
# 결과: 
# - ca-key: 인증기관의 절대 유출되면 안 되는 개인키 (이 키로 도장/서명을 찍습니다)
# - ca-cert: 인증기관의 공개키 인증서 (누구나 볼 수 있는 "인증기관의 신분증" 입니다)


# ===================================================================================
# 2단계: 공통 Truststore (신분증 감별기) 생성
# ===================================================================================
# Truststore는 "내가 신뢰하는 인증기관(CA)의 목록"을 담는 지갑입니다.
# 클러스터 내의 모든 카프카 브로커는 동일한 CA를 신뢰하면 되므로, 이 파일은 하나만 만들어서 공유합니다.
echo "[2/4] 공통 Truststore 생성 중..."
keytool -keystore kafka.truststore.jks -alias CARoot -import -file ca-cert \
    -storepass $PASSWORD -keypass $PASSWORD -noprompt
# 동작 원리: 아까 만든 사설 인증기관의 신분증(ca-cert)을 지갑(kafka.truststore.jks)에 넣습니다.


# ===================================================================================
# 3단계: 각 브로커(1, 2, 3)별 개인 신분증(Keystore) 발급 및 서명
# ===================================================================================
# for문을 돌면서 3대의 카프카 브로커 각각에 대해 인증서를 발급합니다.
for i in 1 2 3
do
    echo "[3/4] kafka-kraft-$i 노드의 Keystore 및 인증서 작업 중..."
    
    # 3-1. Keystore(개인 지갑) 생성 및 비대칭키(Public/Private Key) 쌍 생성
    # -genkey 옵션을 통해 해당 노드만의 고유한 개인키를 생성하고 지갑(keystore.jks)에 보관합니다.
    # ext SAN 옵션은 주소록과 같습니다. 도커 내부망(kafka-kraft-$i)과 호스트(localhost) 
    # 어디로 접속하든 이 인증서가 유효하다고 명시하는 네트워크 보안 설정입니다.
    keytool -keystore kafka-$i.keystore.jks -alias localhost -validity $VALIDITY -genkey -keyalg RSA \
        -dname "CN=kafka-kraft-$i, OU=Dev, O=Kimera, L=Gangnam, ST=Seoul, C=KR" \
        -ext SAN=DNS:localhost,DNS:kafka-kraft-$i \
        -storepass $PASSWORD -keypass $PASSWORD

    # 3-2. CSR(Certificate Signing Request, 인증서 서명 요청서) 생성
    # 방금 만든 공개키를 들고 인증기관(CA)에게 "제 신분증에 진짜라고 도장 좀 찍어주세요" 하는 요청서를 만듭니다.
    keytool -keystore kafka-$i.keystore.jks -alias localhost -certreq -file cert-file-$i \
        -storepass $PASSWORD -keypass $PASSWORD

    # 3-3. CA가 CSR에 서명 (도장 찍기)
    # 1단계에서 만든 인증기관의 권한(ca-key, ca-cert)을 이용해 요청서(cert-file-$i)를 심사하고 
    # 서명이 완료된 최종 인증서(cert-signed-$i)를 발급합니다.
    openssl x509 -req -CA ca-cert -CAkey ca-key -in cert-file-$i -out cert-signed-$i \
        -days $VALIDITY -CAcreateserial -passin pass:$PASSWORD \
        -extfile <(printf "subjectAltName=DNS:localhost,DNS:kafka-kraft-$i")

    # 3-4. 서명된 인증서를 다시 내 지갑(Keystore)에 보관
    # 지갑에 넣을 때는 "나를 서명해준 기관의 신분증(ca-cert)"을 먼저 넣고,
    keytool -keystore kafka-$i.keystore.jks -alias CARoot -import -file ca-cert \
        -storepass $PASSWORD -keypass $PASSWORD -noprompt
        
    # 그 다음 "최종 서명받은 내 신분증(cert-signed-$i)"을 넣어야 자바가 체인(Chain)을 정상적으로 인식합니다.
    keytool -keystore kafka-$i.keystore.jks -alias localhost -import -file cert-signed-$i \
        -storepass $PASSWORD -keypass $PASSWORD -noprompt
done

# ===================================================================================
# 4단계: 마무리 및 정리
# ===================================================================================
echo "[4/4] 완료. 불필요한 중간 생성 파일(CSR, 임시 키 등)을 삭제합니다."
rm ca-cert ca-key ca-cert.srl cert-file-* cert-signed-*
echo "모든 JKS 파일이 성공적으로 생성되었습니다. 이제 docker-compose up -d 를 실행하세요."

 

아니 근데 난생 처음보는 것들이 많다. 필자가 모르는 것들을 정리하겠다.

 

JKS

Java KeyStore 의 약자.

Java 환경에서 암호화 키와 인증서를 안전하게 보관하기 위해 사용하는 전용 데이터베이스(파일) 포맷

 

keytool

JDK(Java Development Kit) 에 기본적으로 내장된 암호화 키 및 인증서 관리 커맨드라인 도구

 

명령어들이 매우 복잡하다. 허나 이제는 대 AI시대! AI에게 필요한 기능을 말하면 알아서 명령어는 잘 작성해주니 필요하면 찾아서 물어보자!

 

 

로컬 환경에서 스크립트를 실행하여 인증서 파일(.jks)들을 미리 만들어두면, Docker Compose가 실행될 때 volumes 설정을 통해 이 파일들을 컨테이너 내부로 주입(Mount)하게 된다.

 

실행 결과

 

ssl 폴더 내부 keystore 확인

 

정리

  • kafka.truststore.jks (3대의 브로커가 공통으로 읽을 파일)
  • kafka-1.keystore.jks (1번 브로커 전용)
  • kafka-2.keystore.jks (2번 브로커 전용)
  • kafka-3.keystore.jks (3번 브로커 전용)

 

그 다음에 docker-kraft-compose.yml 을 실행했다.

drwxrwxr-x  3 thelovemsg thelovemsg    4096 Jul 14 15:13 .
drwxr-x--- 17 thelovemsg thelovemsg    4096 Jul 10 04:55 ..
-rw-rw-r--  1 thelovemsg thelovemsg    7493 Jul 14 14:59 docker-kraft-compose.yml
-rwxrwxr-x  1 thelovemsg thelovemsg    5212 Jul 14 15:00 generate-ssl.sh
-rw-rw-r--  1 thelovemsg thelovemsg 2886599 May 31  2024 jmx_prometheus_javaagent.jar
-rw-rw-r--  1 thelovemsg thelovemsg    3445 Jul  6 13:41 kafka-kraft.yml
-rw-rw-r--  1 thelovemsg thelovemsg     320 Jul  6 08:50 prometheus.yml
drwxrwxr-x  2 thelovemsg thelovemsg    4096 Jul 14 15:13 ssl
$ docker compose -f docker-kraft-compose.yml up -d
[+] up 5/5
 ✔ Container kafka-kraft-3 Started                                                                                                 0.8s
 ✔ Container kafka-kraft-2 Started                                                                                                 0.7s
 ✔ Container kafka-kraft-1 Started                                                                                                 0.8s
 ✔ Container prometheus    Started                                                                                                 0.3s
 ✔ Container grafana       Started                                                                                                 0.2s
$ docker ps
CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS          PORTS                                                                                                    NAMES
eaf89b914d45   apache/kafka:latest      "/__cacert_entrypoin…"   20 seconds ago   Up 19 seconds   0.0.0.0:9405->9405/tcp, [::]:9405->9405/tcp, 9092/tcp, 0.0.0.0:29092->29092/tcp, [::]:29092->29092/tcp   kafka-kraft-2
15bac5c8c2e0   apache/kafka:latest      "/__cacert_entrypoin…"   20 seconds ago   Up 19 seconds   0.0.0.0:9404->9404/tcp, [::]:9404->9404/tcp, 9092/tcp, 0.0.0.0:19092->19092/tcp, [::]:19092->19092/tcp   kafka-kraft-1
b3a20aba5739   apache/kafka:latest      "/__cacert_entrypoin…"   20 seconds ago   Up 19 seconds   0.0.0.0:9406->9406/tcp, [::]:9406->9406/tcp, 9092/tcp, 0.0.0.0:39092->39092/tcp, [::]:39092->39092/tcp   kafka-kraft-3
365633b3f3d5   grafana/grafana:latest   "/run.sh"                8 days ago       Up 18 seconds   0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp                                                              grafana
6e0fe4350dbb   prom/prometheus:latest   "/bin/prometheus --c…"   8 days ago       Up 19 seconds   0.0.0.0:9090->9090/tcp, [::]:9090->9090/tcp                                                              prometheus
$ docker exec -it kafka-kraft-1 bash

 

 

다음은 나눠서 설명하기 귀찮아서... 실제로 메시지가 가고 consumer가 읽는것을 테스트하기 위한 과정을 첨부한다.

 

$ cat prometheus.yml
global:
  scrape_interval: 15s        # 15초마다 메트릭 수집
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'kafka'
    static_configs:
      - targets:
          - 'kafka-kraft-1:9404'
          - 'kafka-kraft-2:9405'
          - 'kafka-kraft-3:9406'
        labels:
          cluster: 'local-kraft'
$ unset KAFKA_OPTS
unset KAFKA_JMX_OPTS
$ /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka-kraft-1:9092 \
  --command-config /tmp/client-ssl.properties \
  --create --topic ssl-test-topic --partitions 3 --replication-factor 3
-bash: /opt/kafka/bin/kafka-topics.sh: No such file or directory
$ /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka-kraft-1:9092 \
  --command-config /tmp/client-ssl.properties \
  --create --topic ssl-test-topic --partitions 3 --replication-factor 3
-bash: /opt/kafka/bin/kafka-topics.sh: No such file or directory
$ docker exec kafka-kraft-1 /bin/bash
$ docker ps
CONTAINER ID   IMAGE                    COMMAND                  CREATED         STATUS         PORTS                                                                                                    NAMES
eaf89b914d45   apache/kafka:latest      "/__cacert_entrypoin…"   5 minutes ago   Up 5 minutes   0.0.0.0:9405->9405/tcp, [::]:9405->9405/tcp, 9092/tcp, 0.0.0.0:29092->29092/tcp, [::]:29092->29092/tcp   kafka-kraft-2
15bac5c8c2e0   apache/kafka:latest      "/__cacert_entrypoin…"   5 minutes ago   Up 5 minutes   0.0.0.0:9404->9404/tcp, [::]:9404->9404/tcp, 9092/tcp, 0.0.0.0:19092->19092/tcp, [::]:19092->19092/tcp   kafka-kraft-1
b3a20aba5739   apache/kafka:latest      "/__cacert_entrypoin…"   5 minutes ago   Up 5 minutes   0.0.0.0:9406->9406/tcp, [::]:9406->9406/tcp, 9092/tcp, 0.0.0.0:39092->39092/tcp, [::]:39092->39092/tcp   kafka-kraft-3
365633b3f3d5   grafana/grafana:latest   "/run.sh"                8 days ago      Up 5 minutes   0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp                                                              grafana
6e0fe4350dbb   prom/prometheus:latest   "/bin/prometheus --c…"   8 days ago      Up 5 minutes   0.0.0.0:9090->9090/tcp, [::]:9090->9090/tcp                                                              prometheus
$ docker exec -it kafka-kraft-1 /bin/bash
15bac5c8c2e0:/# ls -al
total 84
drwxr-xr-x   1 root root 4096 Jul 14 15:24 .
drwxr-xr-x   1 root root 4096 Jul 14 15:24 ..
-rwxr-xr-x   1 root root    0 Jul 14 15:24 .dockerenv
-rwxr-xr-x   1 root root 5307 Feb  5 22:20 __cacert_entrypoint.sh
drwxr-xr-x   1 root root 4096 Feb 10 10:40 bin
drwxr-xr-x   5 root root  340 Jul 14 15:24 dev
drwxr-xr-x   1 root root 4096 Jul 14 15:24 etc
drwxr-xr-x   1 root root 4096 Feb 10 10:40 home
drwxr-xr-x   1 root root 4096 Feb 10 10:40 lib
drwxr-xr-x   2 root root 4096 Feb 10 10:40 lib64
drwxr-xr-x   5 root root 4096 Jan 27 21:19 media
drwxr-xr-x   1 root root 4096 Feb 10 10:40 mnt
drwxr-xr-x   1 root root 4096 Jul 14 15:24 opt
dr-xr-xr-x 251 root root    0 Jul 14 15:24 proc
drwx------   1 root root 4096 Jul 14 15:27 root
drwxr-xr-x   3 root root 4096 Jan 27 21:19 run
drwxr-xr-x   1 root root 4096 Feb 10 10:40 sbin
drwxr-xr-x   2 root root 4096 Jan 27 21:19 srv
dr-xr-xr-x  13 root root    0 Jul 14 15:25 sys
drwxrwxrwt   1 root root 4096 Jul 14 15:26 tmp
drwxr-xr-x   1 root root 4096 Feb 10 10:40 usr
drwxr-xr-x   1 root root 4096 Jan 27 21:19 var
15bac5c8c2e0:/# /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka-kraft-1:9092 \
  --command-config /tmp/client-ssl.properties \
  --create --topic ssl-test-topic --partitions 3 --replication-factor 3
Failed to start Prometheus JMX Exporter

java.net.BindException: Address in use
        at java.base/sun.nio.ch.Net.bind0(Native Method)
        at java.base/sun.nio.ch.Net.bind(Unknown Source)
        at java.base/sun.nio.ch.ServerSocketChannelImpl.netBind(Unknown Source)
        at java.base/sun.nio.ch.ServerSocketChannelImpl.bind(Unknown Source)
        at java.base/sun.nio.ch.ServerSocketAdaptor.bind(Unknown Source)
        at jdk.httpserver/sun.net.httpserver.ServerImpl.<init>(Unknown Source)
        at jdk.httpserver/sun.net.httpserver.HttpServerImpl.<init>(Unknown Source)
        at jdk.httpserver/sun.net.httpserver.DefaultHttpServerProvider.createHttpServer(Unknown Source)
        at jdk.httpserver/com.sun.net.httpserver.HttpServer.create(Unknown Source)
        at io.prometheus.metrics.exporter.httpserver.HTTPServer$Builder.buildAndStart(HTTPServer.java:207)
        at io.prometheus.jmx.common.http.HTTPServerFactory.createHTTPServer(HTTPServerFactory.java:123)
        at io.prometheus.jmx.JavaAgent.premain(JavaAgent.java:62)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
        at java.base/java.lang.reflect.Method.invoke(Unknown Source)
        at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndStartAgent(Unknown Source)
        at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndCallPremain(Unknown Source)

Prometheus JMX Exporter exiting
15bac5c8c2e0:/# /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka-kraft-1:9092 \
  --command-config /tmp/client-ssl.properties \
  --create --topic ssl-test-topic --partitions 3 --replication-factor 3
Failed to start Prometheus JMX Exporter

java.net.BindException: Address in use
        at java.base/sun.nio.ch.Net.bind0(Native Method)
        at java.base/sun.nio.ch.Net.bind(Unknown Source)
        at java.base/sun.nio.ch.ServerSocketChannelImpl.netBind(Unknown Source)
        at java.base/sun.nio.ch.ServerSocketChannelImpl.bind(Unknown Source)
        at java.base/sun.nio.ch.ServerSocketAdaptor.bind(Unknown Source)
        at jdk.httpserver/sun.net.httpserver.ServerImpl.<init>(Unknown Source)
        at jdk.httpserver/sun.net.httpserver.HttpServerImpl.<init>(Unknown Source)
        at jdk.httpserver/sun.net.httpserver.DefaultHttpServerProvider.createHttpServer(Unknown Source)
        at jdk.httpserver/com.sun.net.httpserver.HttpServer.create(Unknown Source)
        at io.prometheus.metrics.exporter.httpserver.HTTPServer$Builder.buildAndStart(HTTPServer.java:207)
        at io.prometheus.jmx.common.http.HTTPServerFactory.createHTTPServer(HTTPServerFactory.java:123)
        at io.prometheus.jmx.JavaAgent.premain(JavaAgent.java:62)
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
        at java.base/java.lang.reflect.Method.invoke(Unknown Source)
        at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndStartAgent(Unknown Source)
        at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndCallPremain(Unknown Source)

Prometheus JMX Exporter exiting
15bac5c8c2e0:/# unset KAFKA_OPTS
unset KAFKA_JMX_OPTS
15bac5c8c2e0:/# /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka-kraft-1:9092 \
  --command-config /tmp/client-ssl.properties \
  --create --topic ssl-test-topic --partitions 3 --replication-factor 3
Created topic ssl-test-topic.
15bac5c8c2e0:/# /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server kafka-kraft-1:9092 \
  --producer.config /tmp/client-ssl.properties \
  --topic ssl-test-topic
Warning: --producer.config is deprecated and will be removed in a future version. Use --command-config instead.
>hello ssl kafka, test message!
>cs^C15bac5c8c2e/opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server kafka-kraft-1:9092 \092 \
  --consumer.config /tmp/client-ssl.properties \
  --topic ssl-test-topic --from-beginning
Option --consumer.config is deprecated and will be removed in a future version. Use --command-config instead.
hello ssl kafka, test message!

 

위 작업의 흐름을 요약했다. 

 

1. 호스트 환경에서의 실행 실패

  • 초기에 호스트 머신(Ubuntu)에서 Kafka CLI 스크립트를 실행하려 했으나, 호스트에는 해당 경로(/opt/kafka/bin/...)에 Kafka가 설치되어 있지 않아 에러(No such file or directory)가 발생했습니다.

 

2. Kafka Docker 컨테이너 접속

  • docker ps로 구동 중인 컨테이너 목록(kafka-kraft-1, 2, 3 등)을 확인한 뒤, docker exec -it kafka-kraft-1 /bin/bash 명령어로 Kafka 브로커가 실제 동작 중인 컨테이너 내부 쉘로 진입했습니다.

 

3. JMX Exporter 포트 충돌 해결

  • 컨테이너 내부에서 토픽 생성 스크립트(kafka-topics.sh)를 실행했으나, 환경변수로 지정된 JMX Exporter가 동일한 포트를 점유하려 시도하면서 java.net.BindException이 발생했습니다.

 

  • unset KAFKA_OPTS 및 unset KAFKA_JMX_OPTS를 실행하여 CLI 프로세스가 JMX Exporter를 구동하지 않도록 환경변수를 해제하여 충돌을 우회했습니다.

 

사실... 그러면 안되지만 필자는 아 귀찮아! 하고 바로 JMX Exporter를 없애버렷다. (어짜피 연습이자나?)

 

 

4. SSL 기반 Kafka 토픽 생성 및 통신 테스트

  • --command-config /tmp/client-ssl.properties 옵션을 부여하여 SSL 인증 설정이 적용된 상태로 ssl-test-topic 토픽(파티션 3, 레플리케이션 3)을 성공적으로 생성했습니다.

 

  • kafka-console-producer.sh를 통해 해당 토픽에 테스트 메시지("hello ssl kafka, test message!")를 발행했습니다.

 

  • kafka-console-consumer.sh를 통해 --from-beginning 옵션으로 처음부터 메시지를 읽어 들여 정상적으로 발행/구독이 이루어지는 것을 확인했습니다.

 

 

실제 CLI 테스트가 성공했다면, 향후 spring boot환경에서 applicaiton.yml 을 다음과 같이 설치해서 사용하면 된다고 한다. 

 

spring:
  kafka:
    # docker-compose에서 외부로 열어둔 포트를 사용
    bootstrap-servers: localhost:19092,localhost:29092,localhost:39092
    properties:
      security.protocol: SSL
      # 로컬 PC에 생성된 Truststore 파일의 절대 경로 지정
      ssl.truststore.location: file:/절대경로/ssl/kafka.truststore.jks
      ssl.truststore.password: kafkapassword

 

3.Kafka SASL과 커버로스(Kerberos)

 

공식문서에서 SASL을 사용해서 권한 설정을 하는 방법에 대해 설명한다. (공식문서 : Authentication using SASL)

 

이전에 Kafka SSL 을 학습하기 전에 다음과 같이 정리했다.

 

(1) 암호화는 SSL(Secure Socket Layer)을,

(2) 인증은 SASL(Simple Authentication and Security Layer)을

(3) 인가 혹은 권한은 ACL(Acces Control List)


암호화가 필요한 경우는 외부에서 내부로 접근하는 경우를 들 수 있다. 

즉, 카프카와 카프카 클라이언트 사이의 통신 내용을 중간에 가로채는 경우 필요하다.


인증의 경우는 신원을 확인하기 위해 Kafka에 적용할 수 있는 기능으로 SASL을 제공하는데 위의 공식문서에 따르면

SASL/GSSAPI, SASL/PLAIN 등 여러개를 지원한다. 


인가/권한은 접근 제어 리스트(ACL - Access Control List)를 쓰면 되는데

접속한 사람이 특정 토픽에서 어떤 메시지를 가져갈 수 있는지 권한을 할당해서 문제점을 사전에 방지한다. 

 

 

그러면 커버로스란 무엇인가?

 

이에 대해 정보를 찾다가 재미있게 글을 정리해주신 글을 medium 에서 찾아보았으니 읽어보길 추천한다.

(참고 : 커버로스(Kerberos)에 대하여)

 

왜 이름이 커버로스인가? 켈베로스가 자꾸 생각나는 이름이다.

 

그런데 정말 지옥의 삼두견 켈베로스(Cerberus)에서 이름을 따왔다고 한다.

 

출처 : https://namu.wiki/w/케르베로스

 

위 토기를 보면 케르베로스의 목줄을 쥔 헤라클레스. 왼쪽에 에우뤼스테우스 왕이 항아리에 숨고 있다.

 

왜 굳이 커여운 지옥의 삼두견을... (왈왈왈!)

 

사용자, 인증 서버, 접속하려는 서비스 라는 세 주체가 마치 머리 셋 달린 개처럼 서로를 확인하며 보안을 지킨다는 의미

 

사용자, 인증 서버, 접속하려는 서비스 이 세 주체! 가 서로 감시를 하기 때문이다.

 

 

그러면 이 짝퉁 삼두견이 해당 그림에서 어디를 커버해주나?

 

  • 사용자 (User) => 카프카 클라이언트
  • 인증 서버 (Authentication Server) => 인증/티켓 서버 (커버로스 서버)
  • 접속하려는 서비스 (Target Service) => 카프카 서버

 

카프카 클라이언트는 kafka 클러스터에게 publish 하고 subscribe하는 producer와 consumer를 말한다.

 

카프카 서버는 kafka cluster 에 존재하는 broker들을 말한다.

 

인증/티켓 서버는 커버로스 서버이다.

 


여기서 희안하게 티켓이라는 말이 나온다. 

 

카프카 클라이언트는 인증/티켓 서버로부터 인증 확인 절차를 거치고, 인증이 완료되면 티켓을 발급받는다.

 

클라이언트는 발급받는 티켓을 가지고 카프카 서버에 인증을 할 수 있다.

 

 

그림에서 보듯이 카프카에 커버로스를 적용하기 위해서는 별도의 커버로스 서버가 필요하다고 한다. 


 

아오... 글이 너무 길어지고 또 명령어도 많아지고 docker 파일 또한 스크롤이 계속 늘어났다.

 

고로 Kafka 심화 학습 시리즈의 3탄 kafka 보안에서는 SSL을 적용해서 통신을 해보았다.

 

그리고 SASL이 뭔지, Kerberos라는게 있다는 것도 알았다.

 

 

사실 SASL용으로 따로 글을 적으려고 햇고 시리즈를 여러개로 나누려고 했는데

 

아... 귀찮다 귀찮아! 뭔가 큰 흐름만 알면 되고 실제로 할 때가 되면... 적용하면 된다는 생각이 들었다.

 

ACL도 마찬가지다. 인가 테스트를 다 하기에는 필자의 흥미가 많이 떨어졌으니 패스하겠다.

 

 

다음 글에서는 스키마에 대해서 알아보도록 하겠다.