
Apache Kafka 4.1.0(KRaft)와 Strimzi를 사용한 Keycloak OAuth2 인증 - CVE-2025-27817 URL 허용 목록 제한 우회
Keycloak 26.1.1 OAuth2/OIDC 인증을 갖춘 프로덕션 준비형 Apache Kafka 4.1.0(KRaft 모드) — Strimzi Kafka 이미지 사용.
이 프로젝트는 이전 POC의 진화된 버전으로, 다음과 같은 중요한 개선 사항이 있습니다:
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 기능을 사용할 수 있습니다.
# 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
keycloak:8080 (HTTP) ←→ kafka-broker:9093 (SASL_SSL/OAuth)
↔ kafka-broker:19092 (PLAINTEXT/브로커 간)
↔ kafka-broker:29093 (PLAINTEXT/KRaft 컨트롤러)
kafka-security/ca-cert + ca-keykafka-security/broker/kafka.server.keystore.jks (서버 인증서 + 개인 키 포함)kafka-security/broker/kafka.server.truststore.jks (CA 인증서 포함)changeit (모든 키스토어/트러스트스토어)# 브로커 인증서
CN=kafka-broker
SAN=DNS:kafka-broker,DNS:localhost,IP:127.0.0.1
# 유효 기간: 3650일
# 키 알고리즘: RSA 2048비트
# 서명 알고리즘: SHA256withRSA
kafka-broker (기밀)
kafka-brokersetup-keycloak.sh가 자동 생성aud 클레임에 kafka-broker 추가preferred_username 포함kafka-producer (기밀)
kafka-producerclient_credentialskafka-consumer (기밀)
kafka-consumerclient_credentialsPOST 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
{
"aud": ["kafka-broker", "account"],
"iss": "http://localhost:8080/realms/kafka-realm",
"azp": "kafka-producer",
"preferred_username": "service-account-kafka-producer",
"scope": "profile email"
}
# 노드 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";
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 클레임authorizer.class.name=org.apache.kafka.metadata.authorizer.StandardAuthorizer
super.users=User:kafka-broker;User:ANONYMOUS
allow.everyone.if.no.acl.found=true
참고: 현재 테스트용으로 허용적 설정입니다. 프로덕션에서는 ACL을 사용해야 합니다.
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()
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())
confluent-kafka-python은 sasl.oauthbearer.method=oidc를 통해 OAuth를 구현하는 librdkafka(C 라이브러리)를 사용합니다. 이 구현은 네이티브 Apache Kafka Java 클라이언트를 차단하는 org.apache.kafka.sasl.oauthbearer.allowed.urls 시스템 속성을 확인하지 않습니다.
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 .
예상 클레임:
{
"aud": ["kafka-broker", "account"],
"iss": "http://localhost:8080/realms/kafka-realm",
"azp": "kafka-producer",
"preferred_username": "service-account-kafka-producer"
}
docker logs kafka-broker 2>&1 | grep -E "Strimzi|JWTSignatureValidator|OAUTHBEARER"
예상 출력:
[io.strimzi.kafka.oauth.validator.JWTSignatureValidator] JWKS keys change detected
docker exec kafka-broker netstat -tlnp | grep java
예상 출력:
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)
docker exec kafka-broker cat /var/lib/kafka/data/meta.properties
예상 출력:
version=1
cluster.id=kafka-cluster-01
node.id=1
문제: {"status":"invalid_token"}
oauth.jwks.endpoint.uri가 브로커 컨테이너에서 접근 가능한지 확인docker exec kafka-broker curl http://keycloak:8080/realms/kafka-realm/protocol/openid-connect/certs문제: Token audience mismatch
aud 클레임에 kafka-broker가 없음./scripts/setup-keycloak.sh를 실행하여 audience 매퍼 추가aud 클레임에 kafka-broker가 포함되어 있는지 확인문제: Token issuer mismatch
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 허용 목록 오류로 실패
Keycloak의 JWT 토큰은 5분 만료입니다. Strimzi OAuth가 자동으로 갱신을 처리합니다:
oauth.refresh.token: 사용 안 함 (client_credentials grant)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
connections.max.idle.ms=600000
connection.failed.authentication.delay.ms=1000
ssl.endpoint.identification.algorithm=https로 업데이트 (none 제거)allow.everyone.if.no.acl.found=true 제거)kafka-acls --bootstrap-server localhost:9093 \
--command-config admin.properties \
--add --allow-principal User:kafka-producer \
--operation Write --topic '*'
oauth.token.endpoint.uri 및 oauth.jwks.endpoint.uri를 HTTPS URL로 업데이트.
├── 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 이미지(quay.io/strimzi/kafka:0.48.0-kafka-4.1.0)를 사용하는 이유:
io.strimzi.kafka.oauth.*)이미지 구성:
브로커 구성에는 두 개의 URL이 있습니다:
oauth.token.endpoint.uri=http://keycloak:8080/... (내부 Docker 네트워크)oauth.valid.issuer.uri=http://localhost:8080/... (외부, JWT iss 클레임과 일치)이유:
브로커는 JWT preferred_username 클레임에서 principal을 추출합니다:
service-account-kafka-producer → User:service-account-kafka-producer
ACL은 이 principal을 참조하여 권한 부여를 수행합니다.
| 구성 요소 | 버전 | 참고 사항 |
|---|
| Apache Kafka | 4.1.0 | KRaft 모드 (ZooKeeper 없음) |
| Strimzi Kafka 이미지 | 0.48.0 | Docker 이미지: quay.io/strimzi/kafka:0.48.0-kafka-4.1.0 |
| Strimzi OAuth 라이브러리 | 0.17.0 | Strimzi Kafka 0.48.0 이미지에 사전 번들 |
| Keycloak | 26.1.1 | 최신 LTS |
| librdkafka | 2.12.0+ | OIDC OAuth 지원 |
| confluent-kafka-python | 2.12.0+ | librdkafka 버전과 일치 |