Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
kafka-keycloak-oauth — Apache Kafka 4.1.0(KRaft)와 Strimzi를 사용한 Keycloak OAuth2 인증 - CVE-2025-27817 URL 허용 목록 제한 우회 | Kitploit
도구/GitHubGitHub/oriolrius/kafka-keycloak-oauth
Cloud Infrastructure SecurityVulnerability AnalysisConfiguration AuditingDevSecOpsAuthenticationLearning & Education
GitHuboriolrius/kafka-keycloak-oauth

kafka-keycloak-oauth

Apache Kafka 4.1.0(KRaft)와 Strimzi를 사용한 Keycloak OAuth2 인증 - CVE-2025-27817 URL 허용 목록 제한 우회

저장소 보기
531011개월 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

Apache Kafka 4.1.0 with Keycloak OAuth2 Authentication

Keycloak 26.1.1 OAuth2/OIDC 인증을 갖춘 프로덕션 준비형 Apache Kafka 4.1.0(KRaft 모드) — Strimzi Kafka 이미지 사용.

이 프로젝트가 kafka-oauth-keycloak-tls-demo보다 나은 이유

이 프로젝트는 이전 POC의 진화된 버전으로, 다음과 같은 중요한 개선 사항이 있습니다:

  • Strimzi OAuth 0.17.0 (기존 1.0.0 대비) — Strimzi Kafka 0.48.0 이미지에 번들된 안정적인 프로덕션 버전
  • 커스텀 Docker 빌드 불필요 — OAuth가 사전 설치된 공식 Strimzi 이미지를 사용하여 Dockerfile 복잡성 제거
  • CVE-2025-27817 인지 — URL 허용 목록 제한과 Strimzi OAuth가 이를 우회하는 이유 문서화
  • 단순화된 아키텍처 — 단일 KRaft 통합 모드(broker+controller), 분할 아키텍처 아님
  • librdkafka 클라이언트 중심 — confluent-kafka-python으로 테스트(URL 허용 목록 문제 없이 동작), Java 네이티브 클라이언트 아님
  • 포괄적인 기술 문서 — 프로덕션 체크리스트, 문제 해결, 성능 튜닝, principal 매핑 세부 사항
  • 더 깔끔한 인증서 관리 — 즉시 테스트 가능한 예제 인증서 포함
  • 자동화된 Keycloak 설정 — audience 구성을 포함한 realm/client/mapper 스크립트 생성
  • 동작하는 Python 테스트 스위트 — OAuth 엔드투엔드 메시지 전달 검증
  • 명시적 issuer URL 처리 — 토큰 엔드포인트와 issuer 검증의 내부/외부 URL 이중성 문서화

아키텍처

  • Kafka 배포판: Strimzi Kafka 이미지 0.48.0 (Apache Kafka 4.1.0 + Strimzi OAuth 0.17.0 사전 번들 포함)
  • Kafka 버전: Apache Kafka 4.1.0 (KRaft 통합 broker+controller)
  • OAuth 라이브러리: Strimzi Kafka OAuth 0.17.0 (이미지에 번들, CVE-2025-27817 URL 허용 목록 제한 우회)
  • OAuth 제공자: Keycloak 26.1.1
  • 보안: 외부 클라이언트용 SASL_SSL(OAuth), 브로커 간 PLAINTEXT, 자체 서명 CA 기반 SSL

CVE-2025-27817 컨텍스트

Apache Kafka 4.0.0+는 SSRF/임의 파일 읽기 취약점을 수정하기 위해 URL 허용 목록(org.apache.kafka.sasl.oauthbearer.allowed.urls)을 JVM 시스템 속성으로 도입했습니다. 이로 인해 네이티브 Apache Kafka 클라이언트에서 표준 OAuth 사용이 중단됩니다.

해결책: Strimzi Kafka OAuth 라이브러리는 이 제한을 구현하지 않으므로 Kafka 4.1.0에서 OAuth 기능을 사용할 수 있습니다.

사전 요구 사항

  • Docker Compose
  • Python 3.x + uv (테스트용)
  • OpenSSL (인증서 생성용)

빠른 시작

root@kitploit:~
# SSL 인증서 생성
cd kafka-security
./generate-certs.sh
cd ..

# 서비스 시작
docker compose up -d

# Keycloak 확인
curl http://localhost:8080/health/ready

# Keycloak realm 및 클라이언트 설정
./scripts/setup-keycloak.sh

# OAuth 프로듀서 테스트
source ~/.venv/bin/activate
uv pip install confluent-kafka
python tests/quick_test.py

네트워크 토폴로지

root@kitploit:~
keycloak:8080 (HTTP) ←→ kafka-broker:9093 (SASL_SSL/OAuth)
                      ↔ kafka-broker:19092 (PLAINTEXT/브로커 간)
                      ↔ kafka-broker:29093 (PLAINTEXT/KRaft 컨트롤러)

SSL 구성

CA 구조

  • 루트 CA: kafka-security/ca-cert + ca-key
  • 브로커 키스토어: kafka-security/broker/kafka.server.keystore.jks (서버 인증서 + 개인 키 포함)
  • 브로커 트러스트스토어: kafka-security/broker/kafka.server.truststore.jks (CA 인증서 포함)
  • 비밀번호: changeit (모든 키스토어/트러스트스토어)

인증서 세부 사항

root@kitploit:~
# 브로커 인증서
CN=kafka-broker
SAN=DNS:kafka-broker,DNS:localhost,IP:127.0.0.1

# 유효 기간: 3650일
# 키 알고리즘: RSA 2048비트
# 서명 알고리즘: SHA256withRSA

Keycloak OAuth 구성

Realm: kafka-realm

클라이언트

kafka-broker (기밀)

  • 클라이언트 ID: kafka-broker
  • 클라이언트 시크릿: setup-keycloak.sh가 자동 생성
  • 용도: 브로커 간 OAuth 인증
  • 매퍼:
    • Audience 매퍼: JWT aud 클레임에 kafka-broker 추가
    • 사용자 이름 매퍼: 토큰에 preferred_username 포함

kafka-producer (기밀)

  • 클라이언트 ID: kafka-producer
  • 클라이언트 시크릿: 자동 생성
  • 용도: 외부 프로듀서 클라이언트
  • Grant: client_credentials
  • 매퍼: kafka-broker와 동일

kafka-consumer (기밀)

  • 클라이언트 ID: kafka-consumer
  • 클라이언트 시크릿: 자동 생성
  • 용도: 외부 컨슈머 클라이언트
  • Grant: client_credentials
  • 매퍼: kafka-broker와 동일

토큰 엔드포인트

root@kitploit:~
POST http://localhost:8080/realms/kafka-realm/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=kafka-producer
&client_secret=<secret>
&scope=profile email

JWT 토큰 구조

root@kitploit:~
{
  "aud": ["kafka-broker", "account"],
  "iss": "http://localhost:8080/realms/kafka-realm",
  "azp": "kafka-producer",
  "preferred_username": "service-account-kafka-producer",
  "scope": "profile email"
}

Kafka 구성

KRaft 모드 (kraft-config.properties)

root@kitploit:~
# 노드 ID
node.id=1
process.roles=broker,controller
controller.quorum.voters=1@kafka-broker:29093

# 리스너
listeners=SASL_SSL://0.0.0.0:9093,PLAINTEXT://0.0.0.0:19092,CONTROLLER://0.0.0.0:29093
advertised.listeners=SASL_SSL://localhost:9093,PLAINTEXT://kafka-broker:19092
listener.security.protocol.map=SASL_SSL:SASL_SSL,PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
inter.broker.listener.name=PLAINTEXT
controller.listener.names=CONTROLLER

# SASL 메커니즘
sasl.enabled.mechanisms=OAUTHBEARER

# Strimzi OAuth 핸들러 (SASL_SSL 리스너별)
listener.name.sasl_ssl.oauthbearer.sasl.login.callback.handler.class=io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler
listener.name.sasl_ssl.oauthbearer.sasl.server.callback.handler.class=io.strimzi.kafka.oauth.server.JaasServerOauthValidatorCallbackHandler

# JAAS를 통한 OAuth 구성
listener.name.sasl_ssl.oauthbearer.sasl.jaas.config=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required \
  oauth.client.id="kafka-broker" \
  oauth.client.secret="<secret>" \
  oauth.token.endpoint.uri="http://keycloak:8080/realms/kafka-realm/protocol/openid-connect/token" \
  oauth.valid.issuer.uri="http://localhost:8080/realms/kafka-realm" \
  oauth.jwks.endpoint.uri="http://keycloak:8080/realms/kafka-realm/protocol/openid-connect/certs" \
  oauth.username.claim="preferred_username";

주요 Strimzi OAuth 매개변수

  • oauth.client.id: 토큰 획득용 클라이언트 식별자
  • oauth.client.secret: 토큰 획득용 클라이언트 시크릿
  • oauth.token.endpoint.uri: Keycloak 토큰 엔드포인트 (브로커는 내부 호스트명 keycloak:8080 사용)
  • oauth.valid.issuer.uri: 예상 JWT issuer (토큰 iss 클레임과 일치해야 하며, 외부 localhost:8080 사용)
  • oauth.jwks.endpoint.uri: JWT 서명 검증용 JWKS 엔드포인트
  • oauth.username.claim: principal 추출용 JWT 클레임

권한 부여

root@kitploit:~
authorizer.class.name=org.apache.kafka.metadata.authorizer.StandardAuthorizer
super.users=User:kafka-broker;User:ANONYMOUS
allow.everyone.if.no.acl.found=true

참고: 현재 테스트용으로 허용적 설정입니다. 프로덕션에서는 ACL을 사용해야 합니다.

클라이언트 구성

Python 프로듀서 (confluent-kafka)

root@kitploit:~
from confluent_kafka import Producer

conf = {
    'bootstrap.servers': 'localhost:9093',
    'security.protocol': 'SASL_SSL',
    'sasl.mechanisms': 'OAUTHBEARER',
    'sasl.oauthbearer.method': 'oidc',
    'sasl.oauthbearer.client.id': 'kafka-producer',
    'sasl.oauthbearer.client.secret': '<secret>',
    'sasl.oauthbearer.token.endpoint.url': 'http://localhost:8080/realms/kafka-realm/protocol/openid-connect/token',
    'ssl.ca.location': 'kafka-security/ca-cert',
    'ssl.endpoint.identification.algorithm': 'none',
}

producer = Producer(conf)
producer.produce('topic', b'message')
producer.flush()

Python 컨슈머 (confluent-kafka)

root@kitploit:~
from confluent_kafka import Consumer

conf = {
    'bootstrap.servers': 'localhost:9093',
    'group.id': 'test-group',
    'security.protocol': 'SASL_SSL',
    'sasl.mechanisms': 'OAUTHBEARER',
    'sasl.oauthbearer.method': 'oidc',
    'sasl.oauthbearer.client.id': 'kafka-consumer',
    'sasl.oauthbearer.client.secret': '<secret>',
    'sasl.oauthbearer.token.endpoint.url': 'http://localhost:8080/realms/kafka-realm/protocol/openid-connect/token',
    'ssl.ca.location': 'kafka-security/ca-cert',
    'ssl.endpoint.identification.algorithm': 'none',
    'auto.offset.reset': 'earliest',
}

consumer = Consumer(conf)
consumer.subscribe(['topic'])
while True:
    msg = consumer.poll(1.0)
    if msg: print(msg.value())

librdkafka가 동작하는 이유

confluent-kafka-python은 sasl.oauthbearer.method=oidc를 통해 OAuth를 구현하는 librdkafka(C 라이브러리)를 사용합니다. 이 구현은 네이티브 Apache Kafka Java 클라이언트를 차단하는 org.apache.kafka.sasl.oauthbearer.allowed.urls 시스템 속성을 확인하지 않습니다.

문제 해결

OAuth 토큰 확인

root@kitploit:~
TOKEN=$(curl -s -X POST http://localhost:8080/realms/kafka-realm/protocol/openid-connect/token \
  -d "grant_type=client_credentials" \
  -d "client_id=kafka-producer" \
  -d "client_secret=<secret>" | jq -r .access_token)

echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | jq .

예상 클레임:

root@kitploit:~
{
  "aud": ["kafka-broker", "account"],
  "iss": "http://localhost:8080/realms/kafka-realm",
  "azp": "kafka-producer",
  "preferred_username": "service-account-kafka-producer"
}

브로커 OAuth 로그 확인

root@kitploit:~
docker logs kafka-broker 2>&1 | grep -E "Strimzi|JWTSignatureValidator|OAUTHBEARER"

예상 출력:

root@kitploit:~
[io.strimzi.kafka.oauth.validator.JWTSignatureValidator] JWKS keys change detected

브로커 리스너 확인

root@kitploit:~
docker exec kafka-broker netstat -tlnp | grep java

예상 출력:

root@kitploit:~
tcp6  0.0.0.0:9093   LISTEN  (SASL_SSL)
tcp6  0.0.0.0:19092  LISTEN  (PLAINTEXT)
tcp6  0.0.0.0:29093  LISTEN  (CONTROLLER)

KRaft 메타데이터 확인

root@kitploit:~
docker exec kafka-broker cat /var/lib/kafka/data/meta.properties

예상 출력:

root@kitploit:~
version=1
cluster.id=kafka-cluster-01
node.id=1

일반적인 문제

문제: {"status":"invalid_token"}

  • 원인: JWT 서명 검증 실패
  • 해결책: oauth.jwks.endpoint.uri가 브로커 컨테이너에서 접근 가능한지 확인
  • 확인 방법: docker exec kafka-broker curl http://keycloak:8080/realms/kafka-realm/protocol/openid-connect/certs

문제: Token audience mismatch

  • 원인: JWT aud 클레임에 kafka-broker가 없음
  • 해결책: ./scripts/setup-keycloak.sh를 실행하여 audience 매퍼 추가
  • 확인 방법: 토큰을 디코딩하고 aud 클레임에 kafka-broker가 포함되어 있는지 확인

문제: Token issuer mismatch

  • 원인: JWT iss가 oauth.valid.issuer.uri와 일치하지 않음
  • 해결책: oauth.valid.issuer.uri=http://localhost:8080/realms/kafka-realm (외부 호스트명) 확인
  • 참고: 브로커는 토큰 엔드포인트에 http://keycloak:8080을 사용하지만 http://localhost:8080 issuer에 대해 검증

문제: 네이티브 Java Kafka 클라이언트가 URL 허용 목록 오류로 실패

  • 원인: Apache Kafka 4.1.0의 CVE-2025-27817 수정
  • 해결책: librdkafka 기반 클라이언트(confluent-kafka-python) 또는 브로커 측 Strimzi OAuth 사용(이미 구성됨)

성능 튜닝

토큰 갱신

Keycloak의 JWT 토큰은 5분 만료입니다. Strimzi OAuth가 자동으로 갱신을 처리합니다:

  • oauth.refresh.token: 사용 안 함 (client_credentials grant)
  • 토큰은 만료 30초 전에 캐시되고 갱신됨

JWKS 캐싱

root@kitploit:~
sasl.oauthbearer.jwks.endpoint.refresh.ms=3600000  # 1시간
sasl.oauthbearer.jwks.endpoint.retry.backoff.ms=100
sasl.oauthbearer.jwks.endpoint.retry.backoff.max.ms=10000

연결 설정

root@kitploit:~
connections.max.idle.ms=600000
connection.failed.authentication.delay.ms=1000

프로덕션 체크리스트

  • 자체 서명 인증서를 CA 서명 인증서로 교체
  • ssl.endpoint.identification.algorithm=https로 업데이트 (none 제거)
  • 적절한 ACL 구성 (allow.everyone.if.no.acl.found=true 제거)
  • ACL 설정:
    root@kitploit:~
    kafka-acls --bootstrap-server localhost:9093 \
      --command-config admin.properties \
      --add --allow-principal User:kafka-producer \
      --operation Write --topic '*'
    
  • Keycloak 클라이언트 시크릿 교체
  • Keycloak HTTPS 활성화
  • oauth.token.endpoint.uri 및 oauth.jwks.endpoint.uri를 HTTPS URL로 업데이트
  • Kafka 모니터링 구성 (JMX, Prometheus)

디렉터리 구조

root@kitploit:~
.
├── docker-compose.yml              # 오케스트레이션
├── .env                            # 시크릿 (gitignored)
├── kafka-config/
│   ├── kraft-config.properties     # Kafka 브로커 구성
│   ├── producer.properties         # 프로듀서 OAuth 구성 (CLI 도구용)
│   └── consumer.properties         # 컨슈머 OAuth 구성 (CLI 도구용)
├── kafka-security/
│   ├── generate-certs.sh           # SSL 인증서 생성기
│   ├── ca-cert                     # 루트 CA 인증서
│   ├── ca-key                      # 루트 CA 개인 키
│   └── broker/
│       ├── kafka.server.keystore.jks
│       └── kafka.server.truststore.jks
├── scripts/
│   └── setup-keycloak.sh           # Keycloak realm/클라이언트 설정
└── tests/
    └── quick_test.py               # OAuth 검증 테스트

기술 참고 사항

Apache Kafka 공식 이미지 대신 Strimzi Kafka 이미지를 사용하는 이유

공식 Apache Kafka 이미지 대신 Strimzi Kafka 이미지(quay.io/strimzi/kafka:0.48.0-kafka-4.1.0)를 사용하는 이유:

  1. 번들 OAuth 지원: Strimzi OAuth 0.17.0 라이브러리 사전 설치 포함 (클래스: io.strimzi.kafka.oauth.*)
  2. CVE-2025-27817 우회: Strimzi OAuth 라이브러리는 네이티브 Kafka OAuth를 중단시키는 URL 허용 목록 제한을 구현하지 않음
  3. 프로덕션 준비 완료: Strimzi Operator를 통해 Kubernetes 환경에서 검증됨
  4. 단일 이미지: OAuth JAR 파일을 수동으로 다운로드하고 마운트할 필요 없음

이미지 구성:

  • Strimzi Kafka 0.48.0 = Docker 이미지 버전/릴리스
  • Apache Kafka 4.1.0 = 내부에 번들된 Kafka 브로커 버전
  • Strimzi OAuth 0.17.0 = 내부에 번들된 OAuth 라이브러리 버전

Issuer URL 이중성

브로커 구성에는 두 개의 URL이 있습니다:

  • oauth.token.endpoint.uri=http://keycloak:8080/... (내부 Docker 네트워크)
  • oauth.valid.issuer.uri=http://localhost:8080/... (외부, JWT iss 클레임과 일치)

이유:

  • 브로커는 내부 DNS 이름을 사용하여 토큰을 가져옴
  • Keycloak은 realm 설정에 구성된 외부 issuer URL로 토큰을 발급
  • JWT 검증은 정확한 issuer 일치가 필요

Principal 매핑

브로커는 JWT preferred_username 클레임에서 principal을 추출합니다:

root@kitploit:~
service-account-kafka-producer → User:service-account-kafka-producer

ACL은 이 principal을 참조하여 권한 부여를 수행합니다.

버전 호환성

참고 자료

  • Strimzi Kafka OAuth
  • Apache Kafka Security
  • Keycloak OIDC
  • CVE-2025-27817
  • KRaft Mode
도구 다운로드
  • OAuth 감사 추적을 위한 로그 집계 설정
  • 장애 조치 시나리오 테스트
  • 시크릿 교체 절차 문서화
  • 필요한 경우 Keycloak 사용자 페더레이션(LDAP/AD) 활성화
  • 구성 요소버전참고 사항
    Apache Kafka4.1.0KRaft 모드 (ZooKeeper 없음)
    Strimzi Kafka 이미지0.48.0Docker 이미지: quay.io/strimzi/kafka:0.48.0-kafka-4.1.0
    Strimzi OAuth 라이브러리0.17.0Strimzi Kafka 0.48.0 이미지에 사전 번들
    Keycloak26.1.1최신 LTS
    librdkafka2.12.0+OIDC OAuth 지원
    confluent-kafka-python2.12.0+librdkafka 버전과 일치