America/Sao_Paulo
Posts

Kafka com SSL: Protegendo a comunicação com o broker

February 21, 2026
Cover foto.
No post anterior exploramos os conceitos fundamentais do Apache Kafka e colocamos a mão na massa subindo um cluster local com Docker. Se você ainda não leu, recomendo conferir antes de continuar — vamos partir de onde paramos! Naquele setup, o broker expunha um listener completamente sem criptografia, o que é prático e simples para aprender em um ambiente local, mas nada profissional quando pensamos em produção. Em cenários reais, mensagens Kafka frequentemente carregam dados sensíveis como pedidos, transações financeiras e informações de usuários. Enviá-los pela rede sem nenhuma proteção é como enviar uma carta sem envelope — qualquer pessoa com acesso àquela rede pode ler o que está dentro. Neste post vamos entender por que isso é perigoso fazendo uma demonstração prática de como um atacante poderia visualizar o conteúdo dessas mensagens e então configurar o Kafka para usar TLS, garantindo que a comunicação entre os clientes e o broker seja criptografada em trânsito. Todo o código está disponível no repositório de exemplo criado para acompanhar este post.
Quando o Kafka opera sem criptografia, as mensagens trafegam pela rede exatamente como foram enviadas pelo produtor — em texto puro. Isso significa que qualquer processo com acesso ao tráfego de rede entre o cliente e o broker consegue interceptar e ler essas mensagens sem nenhuma dificuldade. Você pode estar pensando: "mas o meu cluster está dentro de uma VPC privada, quem vai conseguir acessar o tráfego interno?". Essa é uma pergunta válida — um atacante raramente chega diretamente ao tráfego de rede. O perigo está nas cadeias de ataque: um serviço com permissões excessivas comprometido pode ser usado como ponto de apoio para escalar o acesso; um container com vulnerabilidade explorada pode se tornar a ponte para inspecionar o tráfego dos vizinhos na mesma rede Docker; uma credencial vazada pode abrir caminho para um movimento lateral dentro da VPC. A comunicação em texto puro não é a porta de entrada — ela é o que torna o estrago muito maior quando outra porta é arrombada. Ferramentas de análise de pacotes como o Wireshark permitem inspecionar o tráfego de rede em tempo real. Vamos ver na prática como ficam as suas mensagens Kafka sem criptografia.
A imagem abaixo mostra uma captura de tráfego feita com o Wireshark enquanto a aplicação de exemplo enviava mensagens para o broker sem criptografia ativada. Repare que é possível visualizar o conteúdo das mensagens diretamente nos bytes capturados:
Captura do Wireshark mostrando mensagem Kafka em texto puro.
Informações como chave, valor e metadados das mensagens ficam completamente expostos para qualquer um que capture o tráfego naquele segmento de rede. Quem quiser reproduzir essa situação pode clonar o repositório de exemplo, seguir as instruções de configuração, subir o broker com docker compose up -d e executar o produtor com a flag --no-ssl, que conecta ao listener sem criptografia na porta 9094. Agora vamos aprender como resolver isso!
O TLS (Transport Layer Security) é o protocolo que o Kafka usa para proteger os dados em trânsito. Antes de colocarmos a mão na massa, vale entender alguns conceitos-chave: Autoridade Certificadora (CA)
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.
Todas as credenciais são geradas por quatro scripts bash que devem ser executados em ordem a partir da raiz do repositório. Cada um depende da saída do anterior.
O primeiro script cria o par de chaves da CA: ca-key é a chave privada (deve ser mantida em segredo) e ca-cert é o certificado público, que será distribuído para os truststores do broker e dos clientes.
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"
O keystore é o documento de identidade do broker. O processo tem quatro etapas porque o keytool não consegue importar diretamente um certificado assinado externamente: primeiro geramos um par de chaves dentro do keystore, depois exportamos uma Requisição de Assinatura de Certificado (CSR), a CA assina o CSR e, por fim, importamos o certificado da CA e o certificado assinado de volta ao keystore. O certificado da CA precisa ser importado primeiro para que o keytool consiga verificar a cadeia.
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"
Embora neste setup o broker não precise verificar os clientes, a imagem Docker apache/kafka exige que KAFKA_SSL_TRUSTSTORE_LOCATION aponte para um arquivo válido — caso contrário recusará a iniciar. O truststore do broker precisa apenas do certificado da CA.
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"
O truststore do cliente é usado pelas aplicações Kotlin para verificar a identidade do broker. Quando o broker apresenta seu certificado durante o handshake TLS, o cliente verifica se ele foi assinado pela CA presente neste truststore. Se sim, a conexão é estabelecida — caso contrário, é rejeitada.
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.
Com as credenciais geradas em kafka-creds/, podemos configurar o broker. O docker-compose.yaml abaixo monta esse diretório no container e define as variáveis de ambiente necessárias para o SSL:
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'
O ponto central está na variável KAFKA_LISTENER_SECURITY_PROTOCOL_MAP. Ela define o protocolo de segurança de cada listener:
  • 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)
As variáveis SSL apontam para as credenciais montadas em /etc/kafka/secrets: KAFKA_SSL_KEYSTORE_LOCATION
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 -d
Para acompanhar este post criamos um projeto de exemplo em Kotlin com um produtor e um consumidor que se comunicam com o broker via SSL. Se você quiser entender como criar um produtor e consumidor do zero, o post anterior cobre esses conceitos em detalhes. A boa notícia é que habilitar SSL em clientes Kafka exige apenas três propriedades adicionais. Tanto o OrderProducer quanto o OrderConsumer do projeto de exemplo recebem essas propriedades no seu objeto Properties:
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")
security.protocol
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.
Com o broker no ar e as credenciais geradas, podemos compilar o projeto e executar os clientes. Ambos os JARs devem ser executados a partir da raiz do repositório, pois o caminho do truststore é relativo ao diretório de trabalho:
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.jar
O produtor também aceita a flag --no-ssl para conectar ao listener sem criptografia na porta 9094, que foi usada para gerar a captura do Wireshark que vimos no início do post:
java -jar build/libs/producer.jar --no-ssl
Uma interface web do Kafka também está disponível em http://localhost:8080 para inspecionar tópicos e mensagens.
Veja o que acontece quando repetimos a captura com o Wireshark após configurar o SSL — o mesmo tráfego que antes aparecia em texto puro agora está completamente cifrado:
Captura do Wireshark mostrando tráfego Kafka criptografado com TLS.
Neste post vimos como um atacante com acesso à rede pode interceptar mensagens Kafka em texto puro e aprendemos a proteger esse tráfego configurando TLS no broker e nos clientes. O setup que fizemos usa TLS unidirecional: apenas o broker apresenta um certificado e os clientes o verificam, mas os clientes não precisam provar a própria identidade. Existe uma forma de ir além: o mTLS (TLS mútuo). Nessa abordagem, os clientes também precisam apresentar um certificado assinado pela CA, garantindo que somente aplicações autorizadas consigam se conectar ao broker — mesmo que alguém consiga acesso à rede. Isso adiciona uma camada extra de autenticação importante em ambientes multi-tenant ou com requisitos de segurança mais rigorosos. Esse será o tema de um próximo post!