top of page
pedrobusko

Autenticação OpenID Connect com Apache Kafka 3.1

Este é um artigo traduzido originalmente publicado no blog do Brice Leporini: "OpenID Connect authentication with Apache Kafka 3.1".


Caro leitor, isso não vai ser divertido porque hoje estamos falando de segurança. No entanto, para torná-lo menos chato, trata-se de aproveitar o suporte do OpenID Connect (OIDC) no Kafka 3.1, a base do Confluent Platform 7.1 .


OAuth OpenID

OpenID Connect


Vamos começar com algumas palavras sobre o OIDC . É um padrão aberto que completa o OAuth2.0. O objetivo deste documento não é obter uma introdução adequada ao OIDC, mas vamos enfatizar algumas diferenças importantes com o OAuth2.0.

Em primeiro lugar, no OAuth2.0, o token nada mais é do que uma string opaca que deve ser verificada no servidor de autorização para ser confiável. OIDC usa JSON Web Token . É um documento JSON assinado, codificado em base64. O legal é que, por ser assinado, os aplicativos podem confiar nele sem exigir nenhuma requisição ao servidor de autorização, então implica apenas processar recursos, que escalam bem melhor do que conexões ponto a ponto. O único elemento que o aplicativo (aqui o aplicativo é um corretor Kafka) precisa é a chave pública para validar o token, e é publicado pelo servidor de autorização, com outra especificação aberta, JWKS , e é facilmente armazenado em cache .

Obviamente, este é um resumo extremamente incompleto do OIDC. O que eu gosto é que ele libera o aplicativo da complexidade do método de autenticação. Com o OIDC, a organização pode optar por autenticação simples de usuário/senha, MFA, biometria, SSO, múltiplos fluxos de autenticação e outras opções: não impacta a aplicação desde que esteja

em conformidade com o padrão.


Combinando com Kafka


Aqui, estamos simplificando, pois o caso de uso é criar um aplicativo para autenticação de aplicativo. Portanto, estou usando apenas o ID do cliente e o segredo do cliente. Para torná-lo o mais leve possível, o servidor de autorização é o Auth0 , um serviço totalmente gerenciado com nível gratuito. Para configurá-lo, recomendo a leitura da seção Backend/API da documentação. Kafka faz parte do back-end listado, mas o parágrafo Configure Auth0 APIs de qualquer tipo de back-end se encaixa neste PoC. Você pode optar por qualquer outro provedor de autenticação, pois o padrão é aberto e há várias alternativas de implementação, autogerenciadas e como serviço.

O suporte do OIDC no Kafka 3.1 é uma extensão de um recurso existente e é definido no KIP-768 . O fluxo de autenticação é bem simples:

OIDC no Kafka 3.1

Durante a inicialização, o intermediário coleta o conjunto de chaves públicas do servidor de autorização. O cliente começa autenticando no servidor de autorização e, em seguida, o último emite um JWT. Esse token é então usado para a autenticação SASL/OAUTHBEARER. O corretor agora valida o token verificando a assinatura e as reivindicações para limpar o cliente.

Para torná-lo mais divertido, estou usando o Kafka no modo KRaft (portanto, sem o Zookeeper) com base neste exemplo em execução no Docker fornecido pelo Confluent .

A primeira etapa é validar a configuração do Auth0 e o Kafka vem com uma ferramenta de linha de comando útil:


$ docker run -ti --rm confluentinc/cp-kafka:7.1.0 kafka-run-class org.apache.kafka.tools.OAuthCompatibilityTool \--clientId XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \--clientSecret XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \--sasl.oauthbearer.jwks.endpoint.url https://xxxx-xxxxx.us.auth0.com/.well-known/jwks.json \--sasl.oauthbearer.token.endpoint.url https://xxxx-xxxxx.us.auth0.com/oauth/token \--sasl.oauthbearer.expected.audience https://kafka.auth
PASSED 1/5: client configuration
PASSED 2/5: client JWT retrieval
PASSED 3/5: client JWT validation
PASSED 4/5: broker configuration
PASSED 5/5: broker JWT validation
SUCCESS

Todas as configurações vêm do provedor de autenticação. Qualquer outro tipo de saída exigiria que você desse uma olhada na configuração Auth0.


Agora vamos ajustar a configuração do broker:


version: '2'services:broker:image: confluentinc/cp-kafka:7.1.0hostname: brokercontainer_name: brokerports:- "9092:9092"- "9101:9101"environment:KAFKA_BROKER_ID: 1KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,OIDC:SASL_PLAINTEXT'KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://broker:29092,OIDC://localhost:9092'KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1KAFKA_JMX_PORT: 9101KAFKA_JMX_HOSTNAME: localhostKAFKA_PROCESS_ROLES: 'broker,controller'KAFKA_NODE_ID: 1KAFKA_CONTROLLER_QUORUM_VOTERS: '1@broker:29093'KAFKA_LISTENERS: 'PLAINTEXT://broker:29092,CONTROLLER://broker:29093,OIDC://0.0.0.0:9092'KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'KAFKA_SASL_ENABLED_MECHANISMS: OAUTHBEARERKAFKA_SASL_OAUTHBEARER_JWKS_ENDPOINT_URL: $JWKS_ENDPOINT_URLKAFKA_OPTS: -Djava.security.auth.login.config=/tmp/kafka_server_jaas.confKAFKA_SASL_OAUTHBEARER_EXPECTED_AUDIENCE: $OIDC_AUDKAFKA_LISTENER_NAME_OIDC_OAUTHBEARER_SASL_SERVER_CALLBACK_HANDLER_CLASS: org.apache.kafka.common.security.oauthbearer.secured.OAuthBearerValidatorCallbackHandlervolumes:- ./update_run.sh:/tmp/update_run.sh- ./kafka_server_jaas.conf:/tmp/kafka_server_jaas.conf- ./client.properties:/tmp/client.propertiescommand: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'"

Aqui estão as diferenças com o exemplo original:


$ diff compose.yml compose.ori.yml
14,15c14,15
<       KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,OIDC:SASL_PLAINTEXT'
<       KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://broker:29092,OIDC://localhost:9092'
---
>       KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'
>       KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092'
25c25
<       KAFKA_LISTENERS: 'PLAINTEXT://broker:29092,CONTROLLER://broker:29093,OIDC://0.0.0.0:9092'
---
>       KAFKA_LISTENERS: 'PLAINTEXT://broker:29092,CONTROLLER://broker:29093,PLAINTEXT_HOST://0.0.0.0:9092'
29,33d28
<       KAFKA_SASL_ENABLED_MECHANISMS: OAUTHBEARER
<       KAFKA_SASL_OAUTHBEARER_JWKS_ENDPOINT_URL: $JWKS_ENDPOINT_URL
<       KAFKA_OPTS: -Djava.security.auth.login.config=/tmp/kafka_server_jaas.conf
<       KAFKA_SASL_OAUTHBEARER_EXPECTED_AUDIENCE: $OIDC_AUD
<       KAFKA_LISTENER_NAME_OIDC_OAUTHBEARER_SASL_SERVER_CALLBACK_HANDLER_CLASS: org.apache.kafka.common.security.oauthbearer.secured.OAuthBearerValidatorCallbackHandler
36,37d30
<       - ./kafka_server_jaas.conf:/tmp/kafka_server_jaas.conf
<       - ./client.properties:/tmp/client.properties

Para encurtar a história, o ouvinte externo foi renomeado e configurado para usar SASL_PLAINTEXT com o mecanismo OAUTHBEARER. Observe que as coordenadas do serviço de autorização são fornecidas com variáveis ​​de ambiente para mantê-lo genérico.

A configuração do JAAS é bem básica:


KafkaServer {
    org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required;
};

Agora vamos iniciar o broker:


$ JWKS_ENDPOINT_URL=https://xxxx-xxxxx.us.auth0.com/.well-known/jwks.json OIDC_AUD=https://kafka.auth compose up
[+] Running 1/1
 ⠿ Container broker  Created                       0.1s
Attaching to broker
broker  | ===> User
broker  | uid=1000(appuser) gid=1000(appuser) groups=1000(appuser)
broker  | ===> Configuring ...
broker  | ===> Running preflight checks ...
broker  | ===> Check if /var/lib/kafka/data is writable ...
broker  | ===> Check if Zookeeper is healthy ...
broker  | ignore zk-ready  40
broker  | Formatting /tmp/kraft-combined-logs
broker  | ===> Launching ...
broker  | ===> Launching kafka ...
[...]
broker  | [2022-04-15 06:35:13,095] INFO KafkaConfig values:
broker  |   advertised.listeners = PLAINTEXT://broker:29092,OIDC://localhost:9092
[...]
broker  |   listener.security.protocol.map = CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,OIDC:SASL_PLAINTEXT
broker  |   listeners = PLAINTEXT://broker:29092,CONTROLLER://broker:29093,OIDC://0.0.0.0:9092
[...]
broker  |   sasl.enabled.mechanisms = [OAUTHBEARER]
[...]
broker  |   sasl.oauthbearer.expected.audience = [https://kafka.auth]
[...]
broker  |   sasl.oauthbearer.jwks.endpoint.url = https://xxxx-xxxxx.us.auth0.com/.well-known/jwks.json
[...]
broker  | [2022-04-15 06:35:13,159] INFO [BrokerLifecycleManager id=1] The broker has been unfenced. Transitioning from RECOVERY to RUNNING. (kafka.server.BrokerLifecycleManager)

Coisa boa! Em seguida, vamos configurar o cliente:


security.protocol=SASL_PLAINTEXTsasl.mechanism=OAUTHBEARERsasl.login.callback.handler.class=org.apache.kafka.common.security.oauthbearer.secured.OAuthBearerLoginCallbackHandlersasl.login.connect.timeout.ms=15000sasl.oauthbearer.token.endpoint.url=https://xxxx-xxxxx.us.auth0.com/oauth/tokensasl.jaas.config=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required \
clientId="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
clientSecret="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" ;

Agora podemos fazer alguns testes básicos de produção e consumo. Você deve ter notado que também montei o arquivo de configuração do cliente no container do broker, é pura comodidade rodar os clientes no mesmo container:


$ docker exec -ti broker kafka-console-producer --producer.config /tmp/client.properties --bootstrap-server localhost:9092 --topic test>Hello OIDC!
>%

E em outro terminal:


$ docker exec -ti broker kafka-console-consumer --consumer.config /tmp/client.properties --bootstrap-server localhost:9092 --topic test
Hello OIDC!
^CProcessed a total of 1 messages

É isso!

Executar o cliente sem a configuração adequada gera erros em ambos os lados, atestando que a corretora está rejeitando conforme o esperado:


$ docker exec -ti broker kafka-console-consumer --bootstrap-server localhost:9092 --topic test[2022-04-15 06:57:28,564] WARN [Consumer clientId=console-consumer, groupId=console-consumer-9357] Bootstrap broker localhost:9092 (id: -1 rack: null) disconnected (org.apache.kafka.clients.NetworkClient)
broker  | [2022-04-15 06:57:28,247] INFO [SocketServer listenerType=BROKER, nodeId=1] Failed authentication with /127.0.0.1 (Unexpected Kafka request of type METADATA during SASL handshake.) (org.apache.kafka.common.network.Selector)

Se você aumentar o nível do org.apache.kafka.common.securitylogger, poderá ver o token analisado.

Para referência, também recomendo a leitura da documentação do SASL/OAUTHBEARER .



39 visualizações0 comentário

Commentaires


bottom of page