Neuigkeiten von trion.
Immer gut informiert.

Kommunikation zwischen Kafka und ZooKeeper mit TLS verschlüsseln

Sichere Kommunikation ist gerade dann wichtig, wenn Infrastruktur zum Einsatz kommt, die nicht voll vertrauenswürdig ist. Dazu zählt zum Beispiel ein Kafka / Zookeeper Deployment in einer Cloud Umgebung.
Zwischen Kafka und Zookeeper ist es möglich, die Kommunikation mit TLS zu verschlüsseln und damit zu sichern.

Bisher ( Version 2.3.0 von Apache Kafka) ist es nicht möglich, die Kommunikation zwischen den Kafka-Brokern und dem ZooKeeper-Ensemble zu verschlüsseln. Dies ist nicht möglich, weil die mit Apache Kafka 2.3.0 ausgelieferte Version 3.4.13 von ZooKeeper TLS-Verschlüsselung nicht unterstützt.

Die Dokumentation spielt dies mit der Aussage herunter, dass für gewöhnlich keine sensiblen Daten (lediglich Konfigurations-Einstellungen und Status-Informationen) in ZooKeeper gespeichert werden. Solange sichergestellt ist, dass diese Daten nicht manipuliert werden können, was durch den Einsatz von ACLs für zNodes möglich ist, ist es demnach egal, wenn die Daten mitgelesen werden können:

The rationale behind this decision is that the data stored in ZooKeeper is not sensitive, but inappropriate manipulation of znodes can cause cluster disruption. Read the documentation about how to secure ZooKeeper.

— https://docs.confluent.io/2.0.0/kafka/zookeeper-authentication.html

Diese Ausage verschleiert die an anderer Stelle erwähnte Tatsache, dass durchaus Anwendungsfälle existieren, bei denen sensible Daten in ZooKeeper abgelegt werden. (Die Sicherheits-Hinweise für SASL/SCRAM weisen ausdrücklich darauf hin, dass ZooKeeper von anderen Netzen abgeschirmt werden muss, da dort bei diesem Setup die Authentifizierungs-Daten abgelegt werden) Z.B., wenn die Authentifizierung über SASL/SCRAM oder Delegation Tokens abgewickelt wird.

Entsprechend oft unterstreicht die Dokumentation, dass es für gewöhnlich nicht nötig ist, normalen Clients den Zugriff auf ZooKeeper zu ermöglichen: Heutzutage benötigen lediglich die Admin-Tools einen direkten Zugriff auf das ZooKeeper-Ensemble. Daher wird als Best Practice empfohlen, Zugriff auf ZooKeeper nur aus dem lokalen Netz zu erlauben und den Zugang von außen abzuschirmen (z.B. über eine Firewall).

Im Klartext heißt das: Ein Kafka-Cluster sollte niemals über mehrere Rechenzentren verteilt werden, es sei denn, es wurde sichergestellt, dass aller Datenverkehr zwischen den Rechenzentren über sichere Verbindungen getunnelt wird.

Abhilfe dank ZooKeeper 3.5.5

Am 20. Mai 2019 wurde Version 3.5.5 von ZooKeeper veröffentlicht. Version 3.5.5 ist das erste stabile Release des 3.5er-Zweiges von ZooKeeper. In dem 3.5er-Zweig wurde endlich die lange ersehnte Unterstützung für TLS-Verschlüsselung in ZooKeeper integriert. Die Version unterstützt die Verschlüsselung der Kommunikation zwischen den Knoten eines ZooKeeper-Ensembles und die Verschlüsselung der Kommunikation zwischen ZooKeeper-Servern und -Clients.

Teil der ZooKeeper-Distribution ist eine mächtige Client-API, die eine bequeme Abstraktion der Kommunikation zwischen Clients und Servern über das Atomic Broadcast Protocol bereitstellt. Die TLS-Verschlüsselung wird transparent von dieser API durchgeführt. Daher können Client-Implementierungen durch ein simples Upgrade von Version 3.4.13 auf 3.5.5 ohne weitere Anpassungen von diesem neuen Feature profitieren.

In diesem Artikel werden wir Schritt für Schritt durch ein Beispiel führen, das aufzeigt, wie ein solches Upgrade für Apache Kafka 2.3.0 durchgeführt und anschließend ein Cluster mit TLS-Verschlüsselung für die Kommunikation zwischen den Brokern und ZooKeeper aufgesetzt werden kann.

Warnung: Das Vorgestellte Setup ist lediglich für die Evaluation gedacht. Von einem Einsatz in Produktion wird derzeit abgeraten.

Es werden manuell weitgehend ungetestete Änderungen an den von Apache Kafka verwendeten Abhängigkeiten vorgenommen. Dadurch kann es zu schwer vorhersehbaren Problemen kommen. Außerdem ist für die Aktivierung der TLS-Verschlüsselung die Umstellung von der bewährten NIOServerCnxnFactory, die direkt auf die NIO-API aufsetzt, auf die neu eingeführte NettyServerCnxnFactory, die auf das Netty-Framework aufbaut, notwendig.

Anleitung zur Aktivierung der TLS-Verschlüsselung zwischen den Brokern und ZooKeeper

Im Folgenden werden die notwendigen Schritte einzeln vorgestellt. Für eine schnelle Evaluation des Setups können auch einfach die Bash-Skripte ausgeführt werden, die die vorgestellten Schritte automatisieren.

Alle Kommandos müssen in dem selben Verzeichnis ausgeführt werden. Es wird empfohlen, zu diesem Zweck ein neues Verzeichnis anzulegen.

Herunterladen von Kafka und ZooKeeper

Als erstes müssen Apache Kafka und Apache ZooKeeper in den Versionen 2.3.0 bzw. 3.5.5 heruntergeladen und entpackt werden:

$ curl -sc - http://ftp.fau.de/apache/zookeeper/zookeeper-3.5.5/apache-zookeeper-3.5.5-bin.tar.gz | tar -xzv
$ curl -sc - http://ftp.fau.de/apache/kafka/2.3.0/kafka_2.12-2.3.0.tgz | tar -xzv

Kafka 2.3.0 von ZooKeeper 3.4.13 auf ZooKeeper 3.5.5 umstellen

Die Version 3.4.13 von ZooKeeper muss aus dem libs-Verzeichnis von Apache Kafka gelöscht werden:

$ rm -v kafka_2.12-2.3.0/libs/zookeeper-3.4.14.jar

Danach können die JARs der neuen ZooKeeper-Version in dieses Verzeichnis kopiert werden. (Das letzte JAR wird nur für die Ausführung von CLI-Anwendungen wie z.B. zookeeper-shell.sh benötigt.)

$ cp -av apache-zookeeper-3.5.5-bin/lib/zookeeper-3.5.5.jar kafka_2.12-2.3.0/libs/
$ cp -av apache-zookeeper-3.5.5-bin/lib/zookeeper-jute-3.5.5.jar kafka_2.12-2.3.0/libs/
$ cp -av apache-zookeeper-3.5.5-bin/lib/netty-all-4.1.29.Final.jar kafka_2.12-2.3.0/libs/
$ cp -av apache-zookeeper-3.5.5-bin/lib/commons-cli-1.2.jar kafka_2.12-2.3.0/libs/

Diese Schritte genügen bereits, um Apache Kafka auf die neue ZooKeeper-Version umzustellen. Wenn eines der Kafka-Skripte ausgeführt wird, wird es von jetzt an automatisch ZooKeeper 3.5.5 verwenden.

Eine private CA und die benötigten Zertifikate erzeugen

Ein Root-Zertifikat für die CA erzeugen und in einem Java-Truststore speichern:

$ openssl req -new -x509 -days 365 -keyout ca-key -out ca-cert -subj "/C=DE/ST=NRW/L=MS/O=juplo/OU=kafka/CN=Root-CA" -passout pass:superconfidential
$ keytool -keystore truststore.jks -storepass confidential -import -alias ca-root -file ca-cert -noprompt

Die folgenden Kommandos erzeugen ein selbst-signiertes Zertifikat und speichern es in dem Keystore zookeeper.jks ab. Einzelschritte:

  • Ein neues Schlüssel-Paar und ein Zertifikat für zookeeper erzeugen

  • Einen Certificate-Signing-Request für dieses Zertifikat generieren

  • Diesen Request mit dem Schlüssel der privaten CA signieren und dabei außerdem eine SAN-Erweiterung in das Zertifikat einfügen, so dass das signierte Zertifikat auch für localhost gültig ist

  • Das Root-Zertifikat der privaten CA in den Keystore zookeeper.jks importieren

  • Das signierte Zertifikat für zookeeper in den Keystore zookeeper.jks importieren

$ NAME=zookeeper
$ keytool -keystore $NAME.jks -storepass confidential -alias $NAME \
 -validity 365 -genkey -keypass confidential -dname \
 "CN=$NAME,OU=kafka,O=trion,C=DE"
$ keytool -keystore $NAME.jks -storepass confidential -alias $NAME \
  -certreq -file cert-file
$ openssl x509 -req -CA ca-cert -CAkey ca-key -in cert-file -out $NAME.pem\
  -days 365 -CAcreateserial -passin pass:superconfidential \
  -extensions SAN -extfile <(printf "\n[SAN]\nsubjectAltName=DNS:$NAME,DNS:localhost")
$ keytool -keystore $NAME.jks -storepass confidential -import \
  -alias ca-root -file ca-cert -noprompt
$ keytool -keystore $NAME.jks -storepass confidential -import \
  -alias $NAME -file $NAME.pem

Diese Schritte müssen wiederholt werden mit: - NAME=kafka-1 - NAME=kafka-2 - NAME=client

Es wurden jetzt signierte Zertifikate für alle Entitäten in dem Beispiel-Setup erzeugt und in separaten Java-Keystores abgelegt. Jeder Keystore enthält eine Chain-of-Trust, deren Wurzel jeweils das Root-Zertifikat der erzeugten privaten CA bildet. Außerdem steht ein Truststore zur Verfügung, mit dem sich alle diese Zertifikate validieren lassen. Dies ist möglich, da in dem Truststore das Root-Zertifikat der erzeugten privaten CA abgelegt ist, mit dem die Zertifikate signiert wurden.

ZooKeeper konfigurieren und starten

Wir erläutern hier nur die Konfigurations-Optionen, die für die TLS-Verschlüsselung ergänzt werden müssen!

In unserem Beispiel-Setup werden im wesentlichen zwei angepasste Konfigurations-Dateien benötigt um die TLS-Verschlüsselung der Kommunikation des Standalone-ZooKeeper zu konfigurieren.

Datei java.env
SERVER_JVMFLAGS="-Xms512m -Xmx512m -Dzookeeper.serverCnxnFactory=org.apache.zookeeper.server.NettyServerCnxnFactory"
ZOO_LOG_DIR=.

Über die Java-Umgebungsvariable zookeeper.serverCnxnFactory wird auf die Erzeugung von Verbindungen über das Netty-Framework umgestellt. Ohne die Umstellung auf Netty ist die Verwendung von TLS in ZooKeeper nicht möglich!

Datei zoo.cfg
dataDir=/tmp/zookeeper
secureClientPort=2182  (1)
maxClientCnxns=0
authProvider.1=org.apache.zookeeper.server.auth.X509AuthenticationProvider (2)
ssl.keyStore.location=zookeeper.jks   (3)
ssl.keyStore.password=confidential
ssl.trustStore.location=truststore.jks  (4)
ssl.trustStore.password=confidential
  1. secureClientPort Wir erlauben nur verschlüsselte Verbindungen!
    (Wenn auch unverschlüsselte Verbindungen zugelassen werden sollen, muss zusätzlich der Parameter clientPort gesetzt werden.)

  2. authProvider.1 Aktiviert die Authentifizierung über Client-Zertifikate

  3. ssl.keyStore.* Gibt den Pfad zu und das Passwort des Keystores mit dem zookeeper-Zertifikat an

  4. `ssl.trustStore.* Gibt den Pfad zu und das Passwort des Truststores mit dem Root-Zertifikat der erzeugten privaten CA an

Um Logging für ZooKeeper zu aktivieren, muss die Datei log4j.properties in das aktuelle Arbeitsverzeichnis kopiert werden: (s. auch java.env):

$ cp -av apache-zookeeper-3.5.5-bin/conf/log4j.properties .

Starten des ZooKeeper-Servers:

$ apache-zookeeper-3.5.5-bin/bin/zkServer.sh --config . start

Das --config . sorgt dafür, daß das Skript im aktuellen Verzeichnis nach den Konfigurations-Daten und den Zertifikaten sucht.

Konfigurieren und starten der Broker

Wir erläutern hier nur die Konfigurations-Optionen und Start-Parameter, die benötigt werden umd die Kommunikation zwischen den Brokern und ZooKeeper zu verschlüsseln!

Alle weiteren (nicht explizit angesprochenen) Parameter, die SSL-Parameter spezifizieren werden nur für die Absicherung der Kommunikation zwischen den Brokern selbst und zwischen den Brokern und Kafka-Clients benötigt. Weitere Informationen zu diesen Parameter ist in der Kafka-Dokumentation zu finden. Orientierungs-Hilfe: Das Beispiel-Setup benutzt SSL für die Authentifizierung zwischen den Brokern und SASL/PLAIN für die Client-Authentifizierung - beide Kanäle werden dabei mit TLS verschlüsselt.

Die TLS-Verschlüsselung der Client-API von ZooKeeper wird über Java-Umgebungsvariablen konfiguriert. Daher müssen die meisten SSL-Parameter für die Konfiguration der Verbindung mit ZooKeeper beim Start der Broker übergeben werden. Nur die Adresse und der Port, unter der ZooKeeper angesprochen wird, werden in der Konfigurations-Datei eingestellt.

Konfigurations-Datei kafka-1.properties
broker.id=1
zookeeper.connect=zookeeper:2182
listeners=SSL://kafka-1:9193,SASL_SSL://kafka-1:9194
security.inter.broker.protocol=SSL
ssl.client.auth=required
ssl.keystore.location=kafka-1.jks
ssl.keystore.password=confidential
ssl.key.password=confidential
ssl.truststore.location=truststore.jks
ssl.truststore.password=confidential
listener.name.sasl_ssl.plain.sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required user_consumer="pw4consumer" user_producer="pw4producer";
sasl.enabled.mechanisms=PLAIN
log.dirs=/tmp/kafka-1-logs
offsets.topic.replication.factor=2
transaction.state.log.replication.factor=2
transaction.state.log.min.isr=2

zookeeper.connect: Wenn auch unverschlüsselte Kommunikation erlaubt wurde, muss darauf geachtete werden, dass hier der richtige Port verwendet wird! Alle weiteren Parameter sind nicht relevant für die Verschlüsselung der Kommunikation des Brokers mit ZooKeeper.

Der Broker sollte im Hintergrund gestartet werden. Der folgende Befehl speichert die PID des Prozesses in der Datei KAFKA-1 und leitet die Ausgabe in eine Log-Datei um:

$ (
  export KAFKA_OPTS="
    -Dzookeeper.clientCnxnSocket=org.apache.zookeeper.ClientCnxnSocketNetty (1)
    -Dzookeeper.client.secure=true                                          (2)
    -Dzookeeper.ssl.keyStore.location=kafka-1.jks       (3)
    -Dzookeeper.ssl.keyStore.password=confidential
    -Dzookeeper.ssl.trustStore.location=truststore.jks  (4)
    -Dzookeeper.ssl.trustStore.password=confidential
  "
  kafka_2.12-2.3.0/bin/kafka-server-start.sh kafka-1.properties & echo $! > KAFKA-1
) > kafka-1.log &
  1. Stellt von NIO auf das Netty-Framework um. So wie schon der ZooKeeper-Server kann auch die Client-API TLS nur dann verwenden, wenn für den Verbindungsaufbau das Netty-Framework verwendet wird.

  2. Aktivierung der TLS-Verschlüsselung für alle Verbindungen, die dieser ZooKeeper-Client aufbaut

  3. Gibt den Pfad zu und das Passwort von dem Keystore mit dem kafka-1-Zertifikat an

  4. Gibt den Pfad zu und das Passwort von dem Truststore mit dem Root-Zertifikat der erzeugten privaten CA an

Um sicher zu gehen, dass der Broker korrekt gestartet wurde, sollte die Ausgabe in der Log-Datei kafka-1.log untersucht werden!

Die Schritte müssen für kafka-2 wiederholt werden.

Dabei darf nicht vergessen werden, die Konfiguration-Datei entsprechend anzupassen.

CLI-Anwendungen konfigurieren und starten

Alle Skripte aus der Apache-Kafka-Distribution die Verbindungen zu ZooKeeper aufbauen werden auf die selbe Weise konfiguriert, wie am Fall von kafka-server-start.sh vorgeführt. Um ein Topic anzulegen wäre z.B. folgender Aufruf notwendig:

$ export KAFKA_OPTS="
  -Dzookeeper.clientCnxnSocket=org.apache.zookeeper.ClientCnxnSocketNetty
  -Dzookeeper.client.secure=true
  -Dzookeeper.ssl.keyStore.location=client.jks
  -Dzookeeper.ssl.keyStore.password=confidential
  -Dzookeeper.ssl.trustStore.location=truststore.jks
  -Dzookeeper.ssl.trustStore.password=confidential
"
$ kafka_2.12-2.3.0/bin/kafka-topics.sh \
  --zookeeper zookeeper:2182 \
  --create --topic test \
  --partitions 1 --replication-factor 2

Zu beachten: Es wird ein anderer Keystore (client.jks) verwendet.

CLI-Anwendungen, die nur Verbindungen zu den Brokern benötigen, können wie gewohnt verwendet werden.

In diesem Beispiel-Setup nutzen diese Skripte einen verschlüsselten Listener auf Port 9194 (in dem Fall von kafka-1) und authentifizieren sich mit SASL/PLAIN. Die Client-Konfiguration ist in den Dateien consumer.config bzw. producer.config abgelegt. Die Einstellungen dort sollten zum besseren Verständniss mit der oben vorgenommenen Broker-Konfiguration abgegelichen werden. Für weitergehende Informationen zur Absicherung der Kommunikation zwischen den Brokern und Clients empfehlen wir die Kafka-Dokumentation.

Weitere Schritte zur Absicherung von ZooKeeper…​

Diese Anleitung aktiviert lediglich die TLS-Verschlüsselung zwischen den Kafka-Brokern und einem Standalone-ZooKeeper. Sie zeigt nicht, wie die Kommunikation zwischen den ZooKeeper-Nodes eines Ensembles verschlüsselt werden kann, oder ob sich die Kafka-Broker gegenüber ZooKeeper mit TLS-Zertifikaten authentifizieren können.

Für dies Thema ist ein Folgeartikel geplant.

Vollständig automatisiertes Beispiel für das vorgestellte Setup

Zur schnellen und bequemen Evaluation des vorgestellten Beispiel-Setups kann zookeeper+tls.tgz heruntergeladen und ausgepackt werden.

$ curl -sc - https://juplo.de/wp-uploads/zookeeper+tls.tgz | tar -xzv

Das Archiv enthält Bash-Skripte, die die vorgestellten Schritte vollständig automatisch durchführen. Dazu muss lediglich die Datei README.sh in dem ausgepakten Verzeichnis ausgeführt werden.

Das Skript lädt die benötigte Software, führt das Upgrade der ZooKeeper-Version durch, erzeugt die benötigten Zertifikate und startet den Standalone-ZooKeeper und die beiden Broker mit TLS-Verschlüsselung. Der Standalone-ZooKeeper und die Broker-Instanzen laufen nach Beendigung des Skripts weiter. Dadurch ist es bequem möglich die Beispiele für CLI-Anwendungen aus dem Skript von Hand nachzuvollziehen und auszubauen.

Verwendung

  • README.sh starten, um das automatisierte Setup auszuführen

  • Nach der Ausführung von README.sh läuft der aufgesetzte Beispiel-Cluster weiterhin. Das ermöglicht das experimentieren mit eigenen Aufrufen der CLI-Anwendungen

  • README.sh kann mehrfach ausgeführt werden: bereits ausgeführte Setup-Schritte werden dabei automatisch übersprungen

  • README.sh stop fährt den Kafka-Cluster herunter (der Cluster kann durch Aufruf von README.sh erneut gestartet werden)

  • README.sh cleanup fährt den Cluster herunter und löscht anschließend alle erzeugten Dateien (nur die heruntergeladenen Software-Pakete bleiben erhalten)




Zu den Themen Kafka, Kubernetes und Cloud Architektur bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:

Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Los geht's!

Bitte teilen Sie uns mit, wie wir Sie am besten erreichen können.