America/Sao_Paulo
Posts

Kafka: Autenticação com OAuth e Keycloak

February 28, 2026
Kafka logo.
No post anterior configuramos o Kafka com TLS, garantindo que os dados trafegam criptografados entre clientes e broker. Agora, qualquer tentativa de interceptar o tráfego encontra apenas ruído cifrado. Mas isso resolve apenas metade do problema. TLS protege o que é transmitido, mas precisamos agora controlar quem pode se conectar. Com o setup atual, qualquer serviço que tenha acesso de rede ao broker consegue produzir mensagens, consumir dados e potencialmente injetar eventos maliciosos, mesmo sem conseguir ler o conteúdo de outras conexões. Neste post vamos configurar autenticação no Kafka usando SASL/OAUTHBEARER com o Keycloak como servidor de autorização OAuth 2.0. Todo o código está disponível no repositório de exemplo criado para acompanhar este post.
O Kafka funciona como um hub central de eventos em muitas arquiteturas de microsserviços. Mensagens de pedidos, transações financeiras, dados de usuários e logs de auditoria passam por ele o tempo todo. Sem autenticação, qualquer serviço com acesso à rede do broker pode:
  • Produzir eventos falsos em qualquer tópico: um serviço comprometido pode injetar pedidos fraudulentos, acionar workflows indevidos ou corromper o estado de outros sistemas
  • Consumir dados sensíveis de tópicos que não deveria acessar: um container com vulnerabilidade pode virar um ponto de exfiltração de dados
  • Fazer spoofing de producers: enviar mensagens se passando por outro serviço, quebrando rastreabilidade e trilhas de auditoria
A defesa em profundidade que estamos construindo ao longo desta série tem três camadas: criptografia (TLS, que já configuramos), autenticação (quem você é, abordada neste post) e autorização (o que você pode fazer, no próximo post com ACLs).
OAuth 2.0 é um protocolo de autorização que permite que aplicações obtenham tokens de acesso de forma segura. No contexto do Kafka, usamos o Client Credentials Flow, um fluxo machine-to-machine sem usuário humano envolvido. O fluxo funciona assim:
  1. A aplicação (producer ou consumer) se autentica no servidor OAuth usando client_id e client_secret
  2. O servidor OAuth retorna um JWT (JSON Web Token) assinado com sua chave privada
  3. A aplicação apresenta esse JWT ao Kafka como prova de identidade
JWT (JSON Web Token) Um JWT tem três partes separadas por ponto: header.payload.signature. O header indica o algoritmo de assinatura. O payload carrega as claims, que são informações como iss (quem emitiu), aud (para quem é destinado), exp (quando expira) e sub (o sujeito, a identidade da aplicação). A signature é calculada pelo servidor OAuth com sua chave privada, garantindo que ninguém adulterou o token. JWKS (JSON Web Key Set) Para validar a assinatura do JWT sem precisar consultar o servidor OAuth a cada mensagem, o broker Kafka busca as chaves públicas do servidor via um endpoint JWKS. Essas chaves ficam em cache: o broker valida cada token localmente, de forma offline. Isso é importante para a performance: a autenticação não adiciona uma chamada de rede extra por mensagem produzida ou consumida. Por que OAuth para o Kafka? Comparado a credenciais estáticas (usuário/senha com SASL/PLAIN ou SCRAM), o OAuth traz vantagens importantes:
  • Identidades centralizadas: um único servidor de identidade para todos os serviços, e rotacionar credenciais de um serviço não exige reconfigurar o broker
  • Tokens com expiração curta: se um token for capturado, ele expira automaticamente
  • Sem reinicialização do broker: novas credenciais de clientes não exigem reconfiguração no broker
  • Integração com IAM corporativo: Keycloak, Auth0, Okta. O Kafka vira mais um cliente do seu provedor de identidade existente
SASL (Simple Authentication and Security Layer) é um framework de autenticação usado pelo Kafka. OAUTHBEARER é o mecanismo SASL que usa JWTs como tokens bearer. O fluxo completo tem três fases:
Diagrama de sequência mostrando o fluxo de autenticação SASL/OAUTHBEARER entre Produtor/Consumidor, Keycloak e Broker Kafka.
Inicialização do broker: ao subir, o broker busca as chaves públicas do Keycloak via endpoint JWKS e as mantém em cache. Toda validação de token que acontecer depois usará essas chaves localmente. Obtenção do token (Client Credentials Flow): antes de conectar ao broker, o producer ou consumer chama o endpoint /token do Keycloak com client_id e client_secret. O Keycloak retorna um JWT assinado com aud=kafka-broker no payload, indicando que o token foi emitido especificamente para ser apresentado ao broker. Handshake SASL/OAUTHBEARER: o cliente anuncia que quer usar OAUTHBEARER, o broker confirma, e então o cliente envia o JWT. O broker valida:
  • Assinatura: usando a chave pública JWKS em cache, o que garante que o token não foi adulterado
  • aud: deve conter kafka-broker, o que impede que tokens emitidos para outros serviços funcionem aqui
  • iss: verifica que o token veio do issuer esperado e rejeita tokens de outros realms ou servidores
  • exp: rejeita tokens expirados
Se todas as validações passarem, a conexão é estabelecida e o cliente pode produzir e consumir normalmente.
O Keycloak precisa estar configurado antes do broker, pois o broker vai buscar as chaves JWKS na inicialização. Suba-o primeiro:
docker compose up keycloak -d
O console de administração estará disponível em http://localhost:8082. Faça login com admin / admin.
No menu superior esquerdo, clique em Manage realms → Create realm.
Criar realm no Keycloak.
  • Realm name: kafka-example
  • Clique em Create
Configuração do nome do realm.
Este client representa o próprio broker Kafka. Ele precisa de credenciais para autenticar suas conexões internas com o Keycloak e buscar tokens para uso interno. No menu lateral, vá em Clients → Create client.
Criar client no Keycloak.
Página 1 (General Settings):
  • Client type: OpenID Connect
  • Client ID: kafka-broker
  • Clique em Next
Configurações gerais do kafka-broker.
Página 2 (Capability config):
  • Client authentication: ON
  • Desmarque Standard flow
  • Marque Service accounts roles
  • Clique em Next → Save
Configuração de capabilities do kafka-broker.
Após salvar, vá à aba Credentials e copie o Client secret, pois você vai precisar dele para configurar o docker-compose.yaml.
Client secret do kafka-broker.
O broker valida que todo JWT recebido contém kafka-broker no campo aud. Para isso, é necessário configurar um mapper que adicione essa claim aos tokens emitidos. Vá em Clients → kafka-broker → aba Client scopes → clique no scope kafka-broker-dedicated.
Client scope do kafka-broker.
Na aba Mappers → Configure a new mapper → selecione Audience.
Adicionar mapper — passo 1.
Adicionar mapper — passo 2.
  • Name: kafka-broker-audience
  • Included Custom Audience: kafka-broker
  • Clique em Save
Configuração do audience mapper.
Este client é usado pelas aplicações Kotlin, tanto o producer quanto o consumer. Repita os mesmos passos do 2.2, com uma diferença:
  • Client ID: kafka-client
Após salvar, copie o Client secret na aba Credentials.
Client secret do kafka-client.
Repita os mesmos passos do 2.3 para o client kafka-client: Clients → kafka-client → Client scopes → kafka-client-dedicated → Mappers → Configure a new mapper → Audience
  • Name: kafka-broker-audience
  • Included Custom Audience: kafka-broker
  • Clique em Save
Audience mapper do kafka-client.
Com o realm e os dois clients configurados, podemos confirmar que tudo está correto fazendo uma requisição de token diretamente ao Keycloak:
terminal
curl -s -X POST "http://localhost:8082/realms/kafka-example/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=kafka-client&client_secret=<secret>"
O Keycloak deve responder com um JSON contendo um access_token. Se a requisição falhar ou o token não contiver "aud": ["kafka-broker", ...] no payload, verifique se o audience mapper foi configurado corretamente nos passos 2.3 e 2.5.
access_token
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI2UzVFTVF0TE56ZlFCMEZYYWk4ckdpbHZWaXZQb2ltbDRQMkd5NWk4cWtrIn0.eyJleHAiOjE3NzIzMTU4MTQsImlhdCI6MTc3MjMxNTUxNCwianRpIjoidHJydGNjOmNlYzBhNWMwLTFkMWYtNmZmYS1kYTI5LTdmZDk4ZDE1YzQ3OCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4Mi9yZWFsbXMva2Fma2EtZXhhbXBsZSIsImF1ZCI6WyJrYWZrYS1icm9rZXIiLCJhY2NvdW50Il0sInN1YiI6IjFjN2FhZTMwLTAwMTMtNDY3Zi05ZmJkLWZhNTAwZjc5NGE2ZSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImthZmthLWNsaWVudCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiLyoiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMta2Fma2EtZXhhbXBsZSIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJjbGllbnRIb3N0IjoiMTcyLjIzLjAuMSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoic2VydmljZS1hY2NvdW50LWthZmthLWNsaWVudCIsImNsaWVudEFkZHJlc3MiOiIxNzIuMjMuMC4xIiwiY2xpZW50X2lkIjoia2Fma2EtY2xpZW50In0.Ttd5Vm28ETSCo0VIob4jHCAcvCW77LgtX0klg6DcODyX3XaCylxdhexZBpAc0QMk9zbBs0zScEQVb9zA0PzeY10RHtXFo-hlghBnARlpajC733IJ69vGL6M5fjQMV8tKXJ-DG-zQZkyLIh0Xo5decSwRzbJYQExQf_LD5QcqkKbhZxFAIk-TRnI9IYXilTEnavnK16REoDnMjrJeVKvE3m0p1Huh5rwEB0ZHdhHqNmZCA1JBAzPrzJvPTvUvmhNHwER2joJ1A-V5xvHYrj63mEEou0xYoPPJyzjdIDpl7k4XWKxpqyRsaxxqD7i2E2bIm3mfGGskw8S5h3uHoR3OgQ
À primeira vista parece ruído, mas é apenas base64url. Decodificando as duas primeiras partes (header e payload), o conteúdo fica legível. O jwt.io permite colar qualquer JWT e inspecionar suas claims instantaneamente:
JWT decodificado
// Header
{
"alg": "RS256",
"typ": "JWT",
"kid": "6S5EMQtLNzfQB0FXai8rGilvVivPoiml4P2Gy5i8qkk"
}

// Payload
{
"iss": "http://localhost:8082/realms/kafka-example",  // issuer — quem emitiu o token
"aud": ["kafka-broker", "account"],                   // audience — para quem o token é válido
"exp": 1772315814,                                    // expiration — quando o token expira
"iat": 1772315514,                                    // issued at — quando foi emitido (exp - iat = 300s)
"sub": "1c7aae30-0013-467f-9fbd-fa500f794a6e",        // subject — identidade única do client
"azp": "kafka-client",                                // authorized party — client que requisitou o token
"preferred_username": "service-account-kafka-client"
}
As claims que o broker vai verificar são exatamente iss, aud e exp. O sub e o azp identificam qual client se autenticou, informação que ficará mais relevante quando falarmos de autorização com ACLs.
Com o client secret do kafka-broker em mãos (copiado na etapa 2.2), preencha o valor no docker-compose.yaml. O broker é configurado com variáveis de ambiente que ativam o mecanismo SASL e apontam para os endpoints do Keycloak:
docker-compose.yaml
broker:
image: confluentinc/cp-kafka:latest
environment:
  # ... demais configurações ...
  KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: >-
    CONTROLLER:PLAINTEXT,EXTERNAL:SASL_PLAINTEXT,PLAINTEXT_EXTERNAL:PLAINTEXT
  KAFKA_LISTENER_NAME_EXTERNAL_SASL_ENABLED_MECHANISMS: "OAUTHBEARER"
  KAFKA_LISTENER_NAME_EXTERNAL_SASL_OAUTHBEARER_EXPECTED_AUDIENCE: "kafka-broker"
  KAFKA_LISTENER_NAME_EXTERNAL_SASL_OAUTHBEARER_EXPECTED_ISSUER: "http://localhost:8082/realms/kafka-example"
  KAFKA_LISTENER_NAME_EXTERNAL_SASL_OAUTHBEARER_JWKS_ENDPOINT_URL: "http://keycloak:8080/realms/kafka-example/protocol/openid-connect/certs"
  KAFKA_LISTENER_NAME_EXTERNAL_SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL: "http://keycloak:8080/realms/kafka-example/protocol/openid-connect/token"
  KAFKA_LISTENER_NAME_EXTERNAL_OAUTHBEARER_SASL_LOGIN_CALLBACK_HANDLER_CLASS: "org.apache.kafka.common.security.oauthbearer.secured.OAuthBearerLoginCallbackHandler"
  KAFKA_LISTENER_NAME_EXTERNAL_OAUTHBEARER_SASL_SERVER_CALLBACK_HANDLER_CLASS: "org.apache.kafka.common.security.oauthbearer.secured.OAuthBearerValidatorCallbackHandler"
  KAFKA_BROKER_CLIENT_SECRET: "<secret do kafka-broker>"
Veja o que cada variável faz: KAFKA_LISTENER_NAME_EXTERNAL_SASL_ENABLED_MECHANISMS
Define OAUTHBEARER como o mecanismo SASL habilitado no listener EXTERNAL.
KAFKA_LISTENER_NAME_EXTERNAL_SASL_OAUTHBEARER_EXPECTED_AUDIENCE
O broker rejeita tokens cujo campo aud não contenha kafka-broker. Isso garante que tokens emitidos para outros serviços não funcionem no Kafka.
KAFKA_LISTENER_NAME_EXTERNAL_SASL_OAUTHBEARER_EXPECTED_ISSUER
O issuer (iss) esperado nos tokens. Tokens emitidos por outros realms ou servidores OAuth são rejeitados.
KAFKA_LISTENER_NAME_EXTERNAL_SASL_OAUTHBEARER_JWKS_ENDPOINT_URL
Endpoint de onde o broker busca as chaves públicas para validar assinaturas JWT. Note que a URL usa keycloak:8080 (rede interna do Docker), pois o broker acessa o Keycloak de dentro do container.
KAFKA_LISTENER_NAME_EXTERNAL_SASL_OAUTHBEARER_TOKEN_ENDPOINT_URL
Endpoint de onde o broker busca seu próprio token para autenticação interna com o Keycloak.
KAFKA_LISTENER_NAME_EXTERNAL_OAUTHBEARER_SASL_LOGIN_CALLBACK_HANDLER_CLASS
Handler responsável por buscar o token JWT no servidor OAuth durante o login do cliente.
KAFKA_LISTENER_NAME_EXTERNAL_OAUTHBEARER_SASL_SERVER_CALLBACK_HANDLER_CLASS
Handler do broker responsável por validar o JWT recebido durante o handshake SASL.
Com as variáveis preenchidas, suba o broker:
docker compose up broker -d
Para acompanhar este post criamos um projeto de exemplo em Kotlin com um producer e um consumer que se autenticam via SASL/OAUTHBEARER. Se você quiser entender como criar producers e consumers do zero, o primeiro post da série cobre esses conceitos em detalhes. São necessárias cinco propriedades adicionais para habilitar a autenticação OAuth nos clientes Kafka:
Kotlin
// MessageProducer.kt / MessageConsumer.kt
properties["bootstrap.servers"] = "localhost:9092"
properties["security.protocol"] = "SASL_PLAINTEXT"
properties["sasl.mechanism"]    = "OAUTHBEARER"
properties["sasl.jaas.config"]  =
  "org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required" +
  " clientId="kafka-client"" +
  " clientSecret="$clientSecret";"
properties["sasl.login.callback.handler.class"]   =
  "org.apache.kafka.common.security.oauthbearer.secured.OAuthBearerLoginCallbackHandler"
properties["sasl.oauthbearer.token.endpoint.url"] =
  "http://localhost:8082/realms/kafka-example/protocol/openid-connect/token"
security.protocol
Instrui o cliente a usar SASL sem TLS. Em produção, o ideal é combinar os dois e usar SASL_SSL.
sasl.mechanism
Define OAUTHBEARER como o mecanismo SASL a ser usado na conexão.
sasl.jaas.config
Configuração JAAS com clientId e clientSecret do client kafka-client no Keycloak. Essas credenciais são usadas para buscar o JWT antes de cada conexão.
sasl.login.callback.handler.class
Implementação do handler que executa o Client Credentials Flow: chama o endpoint /token do Keycloak com as credenciais do sasl.jaas.config e obtém o JWT.
sasl.oauthbearer.token.endpoint.url
URL do endpoint de token do Keycloak. Os clientes usam localhost:8082 para acessar de fora do Docker, diferente do broker que usa a rede interna.
terminal
# Compila os dois fat JARs
./gradlew clean build

# Com autenticação OAuth — conecta ao listener SASL_PLAINTEXT na porta 9092
java -jar build/libs/producer-1.0-SNAPSHOT.jar --client-secret=<secret do kafka-client>
java -jar build/libs/consumer-1.0-SNAPSHOT.jar --client-secret=<secret do kafka-client>

# Sem autenticação — conecta ao listener PLAINTEXT na porta 9094
java -jar build/libs/producer-1.0-SNAPSHOT.jar --no-auth
java -jar build/libs/consumer-1.0-SNAPSHOT.jar --no-auth
O broker expõe dois listeners externos para fins de comparação:
ListenerPortaProtocoloUso
EXTERNAL9092SASL_PLAINTEXTConexão com autenticação OAuth
PLAINTEXT_EXTERNAL9094PLAINTEXTConexão sem autenticação
A flag --no-auth conecta ao listener sem autenticação, útil para confirmar que o listener autenticado rejeita conexões sem token válido.
Neste post adicionamos a segunda camada de segurança ao nosso cluster Kafka: autenticação com identidade verificável. O Keycloak atua como Authorization Server, emitindo JWTs assinados para as aplicações autorizadas. O broker valida cada token localmente usando as chaves JWKS em cache, sem chamadas de rede adicionais por mensagem. Clientes sem credenciais válidas simplesmente não conseguem se conectar. Mas saber quem está conectado não é suficiente: precisamos controlar o que cada identidade pode fazer. Um producer autenticado não deveria conseguir consumir tópicos de outros domínios. Um consumer de relatórios não deveria ter permissão para produzir eventos de pagamento. No próximo post vamos configurar autorização com ACLs no Kafka, definindo políticas granulares de acesso por tópico e operação.