Introdução
O risco de comunicar em texto puro
Demonstração com Wireshark
Como o TLS funciona no Kafka
A CA é a raiz de confiança do nosso setup. É um par de chaves cujo único propósito é assinar certificados. Quando o cliente recebe o certificado do broker, ele verifica se aquele certificado foi assinado por uma CA em quem ele confia. Em produção você poderia usar uma CA reconhecida (como a Let's Encrypt), mas para ambientes controlados é comum criar uma CA própria — é exatamente o que faremos aqui. Keystore
O keystore é o "documento de identidade" do broker. Ele armazena a chave privada do broker e o seu certificado assinado pela CA. O broker usa esses dados para provar sua identidade durante o handshake TLS. Truststore
O truststore é a "lista de CAs confiáveis". Os clientes Kotlin usam um truststore que contém o certificado da nossa CA. Quando o broker apresenta seu certificado durante a conexão, o cliente verifica se ele foi assinado por alguma CA presente no truststore — se sim, a conexão é estabelecida; caso contrário, é rejeitada. Neste setup vamos configurar TLS unidirecional: apenas o broker apresenta um certificado e os clientes o verificam. Os clientes não precisam apresentar certificado próprio — voltaremos a esse ponto na conclusão.
Gerando as credenciais
1. Criando a CA
1-create-ca.sh
#!/usr/bin/env bash
# Cria: kafka-creds/ca-key, kafka-creds/ca-cert
set -euo pipefail
PASSWORD=123456
VALIDITY=3650
CREDS=kafka-creds
mkdir -p "$CREDS"
MSYS_NO_PATHCONV=1 openssl req -new -x509 \
-keyout "$CREDS/ca-key" \
-out "$CREDS/ca-cert" \
-days "$VALIDITY" \
-passout pass:"$PASSWORD" \
-subj "/CN=local-kafka-ca/OU=kafka/O=example/L=Local/ST=SP/C=BR"2. Criando o keystore do broker
2-create-server-keystore.sh
#!/usr/bin/env bash
# Cria: kafka-creds/server.keystore.jks
# Requer: kafka-creds/ca-key, kafka-creds/ca-cert
set -euo pipefail
PASSWORD=123456
VALIDITY=3650
CREDS=kafka-creds
# Gera um par de chaves dentro do keystore
keytool -genkey -noprompt \
-keystore "$CREDS/server.keystore.jks" \
-alias localhost \
-dname "CN=localhost, OU=kafka, O=example, L=Local, ST=SP, C=BR" \
-keyalg RSA \
-validity "$VALIDITY" \
-storepass "$PASSWORD" \
-keypass "$PASSWORD"
# Exporta uma Requisição de Assinatura de Certificado do keystore
keytool -certreq -noprompt \
-keystore "$CREDS/server.keystore.jks" \
-alias localhost \
-file "$CREDS/cert-file" \
-storepass "$PASSWORD"
# Assina o CSR com a CA, gerando o certificado do broker
MSYS_NO_PATHCONV=1 openssl x509 -req \
-CA "$CREDS/ca-cert" \
-CAkey "$CREDS/ca-key" \
-in "$CREDS/cert-file" \
-out "$CREDS/cert-signed" \
-days "$VALIDITY" \
-CAcreateserial \
-passin pass:"$PASSWORD"
# Importa o certificado da CA para que o keytool possa verificar a cadeia
keytool -import -noprompt \
-keystore "$CREDS/server.keystore.jks" \
-alias CARoot \
-file "$CREDS/ca-cert" \
-storepass "$PASSWORD"
# Importa o certificado assinado, completando o keystore
keytool -import -noprompt \
-keystore "$CREDS/server.keystore.jks" \
-alias localhost \
-file "$CREDS/cert-signed" \
-storepass "$PASSWORD"3. Criando o truststore do broker
3-create-server-truststore.sh
#!/usr/bin/env bash
# Cria: kafka-creds/server.truststore.jks
# Requer: kafka-creds/ca-cert
set -euo pipefail
PASSWORD=123456
CREDS=kafka-creds
keytool -import -noprompt \
-keystore "$CREDS/server.truststore.jks" \
-alias CARoot \
-file "$CREDS/ca-cert" \
-storepass "$PASSWORD"4. Criando o truststore do cliente
4-create-client-truststore.sh
#!/usr/bin/env bash
# Cria: kafka-creds/client.truststore.jks
# Requer: kafka-creds/ca-cert
set -euo pipefail
PASSWORD=123456
CREDS=kafka-creds
keytool -import -noprompt \
-keystore "$CREDS/client.truststore.jks" \
-alias CARoot \
-file "$CREDS/ca-cert" \
-storepass "$PASSWORD"Nota para Windows / Git Bash: o Git Bash converte automaticamente strings que parecem caminhos Unix em caminhos Windows, o que quebra o argumento -subj passado ao openssl. Os scripts contornam isso prefixando as chamadas openssl com MSYS_NO_PATHCONV=1, desabilitando a conversão de caminhos para aquele comando específico.
Configurando o broker
docker-compose.yaml
version: '3.5'
services:
broker:
image: apache/kafka:3.9.0
volumes:
- ./kafka-creds:/etc/kafka/secrets
environment:
KAFKA_NODE_ID: 1
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:SSL,PLAINTEXT_HOST:PLAINTEXT'
KAFKA_ADVERTISED_LISTENERS: 'INTERNAL://broker:9093,EXTERNAL://localhost:9092,PLAINTEXT_HOST://localhost:9094'
KAFKA_LISTENERS: 'CONTROLLER://:29093,INTERNAL://:9093,EXTERNAL://:9092,PLAINTEXT_HOST://:9094'
KAFKA_PROCESS_ROLES: 'broker,controller'
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@broker:29093'
KAFKA_INTER_BROKER_LISTENER_NAME: 'INTERNAL'
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
KAFKA_SSL_KEYSTORE_LOCATION: '/etc/kafka/secrets/server.keystore.jks'
KAFKA_SSL_KEYSTORE_PASSWORD: '123456'
KAFKA_SSL_KEY_PASSWORD: '123456'
KAFKA_SSL_TRUSTSTORE_LOCATION: '/etc/kafka/secrets/server.truststore.jks'
KAFKA_SSL_TRUSTSTORE_PASSWORD: '123456'
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_NUM_PARTITIONS: 9
ports:
- "9092:9092"
- "9094:9094"
kafka-ui:
image: provectuslabs/kafka-ui:latest
ports:
- "8080:8080"
environment:
DYNAMIC_CONFIG_ENABLED: 'true'- CONTROLLER:PLAINTEXT e INTERNAL:PLAINTEXT — tráfego interno do cluster, que permanece sem criptografia dentro da rede Docker
- EXTERNAL:SSL — listener ao qual os clientes externos se conectam, agora protegido por TLS na porta 9092
- PLAINTEXT_HOST:PLAINTEXT — listener sem criptografia na porta 9094, mantido para fins de demonstração (como a captura do Wireshark que fizemos)
Caminho para o keystore do broker dentro do container, que contém a chave privada e o certificado assinado. KAFKA_SSL_KEYSTORE_PASSWORD
Senha para abrir o arquivo keystore. KAFKA_SSL_KEY_PASSWORD
Senha para a entrada de chave privada dentro do keystore. KAFKA_SSL_TRUSTSTORE_LOCATION
Caminho para o truststore do broker, exigido pela imagem Docker mesmo com mTLS desabilitado. KAFKA_SSL_TRUSTSTORE_PASSWORD
Senha para abrir o arquivo truststore. Agora podemos subir o broker:
docker compose up -dConfigurando os clientes Kotlin
Kotlin
// Produtor (OrderProducer.kt)
props["bootstrap.servers"] = "localhost:9092"
props["security.protocol"] = "SSL"
props["ssl.truststore.location"] = "./kafka-creds/client.truststore.jks"
props["ssl.truststore.password"] = "123456"
props["key.serializer"] = "org.apache.kafka.common.serialization.StringSerializer"
props["value.serializer"] = "dev.danielsantiago.kafka.ssl.example.config.serializer.OrderSerializer"
// Consumidor (OrderConsumer.kt)
props.setProperty("bootstrap.servers", "localhost:9092")
props["security.protocol"] = "SSL"
props["ssl.truststore.location"] = "./kafka-creds/client.truststore.jks"
props["ssl.truststore.password"] = "123456"
props.setProperty("group.id", "test")
props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
props.setProperty("value.deserializer", "dev.danielsantiago.kafka.ssl.example.config.deserializer.OrderDeserializer")Instrui o cliente Kafka a usar TLS na conexão com o broker. Sem essa propriedade, o cliente tentaria conectar em plaintext mesmo que o broker exija SSL. ssl.truststore.location
Caminho para o client.truststore.jks gerado pelo script 4. O cliente usa esse arquivo para verificar se o certificado apresentado pelo broker foi assinado pela nossa CA. O caminho é relativo ao diretório de trabalho, portanto execute os JARs sempre a partir da raiz do repositório. ssl.truststore.password
Senha para abrir o truststore do cliente.
Compilando e executando
terminal
# Compila os dois fat JARs
./gradlew build
# Em um terminal — inicia o consumidor
java -jar build/libs/consumer.jar
# Em outro terminal — executa o produtor (envia 5 pedidos)
java -jar build/libs/producer.jarjava -jar build/libs/producer.jar --no-ssl

