Привет! Столкнулся с тем, что быстро не нашел простой инструкции, как с использованием SSL и SASL настроить безопасный кластер из нескольких Linux узлов Zookeeper, и решил это исправить.

В этой статье поговорим о том, как:

  • Настроить Zookeeper в кластере из трех узлов без шифрования (Plain);

  • Добавить шифрование во внутрикластерное взаимодействие (Quorum TLS);

  • Создать сертификаты для подключения к узлам Zookeeper клиентов (Server TLS);

  • Создать сертификаты для подключения клиентов к узлам (Client TLS);

  • Добавить авторизацию в шифрованный кластер (SASL with MD5);

  • Показать на примере, как работают ACL, посмотреть, чем отличается суперпользователь super от всех остальных (как работает ACL в действии).

Plain

Для понимания, какая настройка за что отвечает, начнем установку с простой кластерной конфигурации, а потом усложним ее.

Возьмем три узла с Linux, например AlmaLinux 9.2.

Важно, чтобы они знали свои имена, поэтому нужно либо настроить DNS, либо добавить информацию об их именах в /etc/hosts на всех трех узлах:

127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.1.11 zoo1.local zoo1
192.168.1.12 zoo2.local zoo2 
192.168.1.13 zoo3.local zoo3

Ниже мы будем использовать команду hostname -f, для которой будет важно, в каком порядке определены имена в /etc/hosts (сначала zoo1.local, а только потом zoo1, но не наоборот).

В документации Zookeeper(Zk) говорится, что он хорошо работает на стабильной JDK11 LTS (в списке JDK 8 LTS, JDK 11 LTS, JDK 12 ), установим её на все узлы:

sudo dnf install -y java-11-openjdk.x86_64

Скачаем стабильный дистрибутив Zk, сейчас это версия 3.8.3:

wget https://dlcdn.apache.org/zookeeper/stable/apache-zookeeper-3.8.3-bin.tar.gz

Стабильные версии Zk можно найти тут.

На всех трех узлах сразу создадим отдельного пользователя zookeeper:

sudo useradd -m -d /opt/zookeeper -s /bin/bash zookeeper

После того, как все настроим, можно будет поменять шелл пользователя на /bin/false. Команда создаст папку /opt/zookeeper.

Распакуем в нее скачанный файл, а также создадим папки для сертификатов, данных и настроек для подключения клиентов:

sudo tar -xf apache-zookeeper-3.8.3-bin.tar.gz --directory /opt/zookeeper
sudo chown -R zookeeper:zookeeper /opt/zookeeper
sudo su - zookeeper
mv apache-zookeeper-3.8.3-bin/{bin,conf,lib,docs} . 
rm -r apache-zookeeper-3.8.3-bin
mkdir -m 700 ssl client-settings data .ssh

На всех узлах сразу запишем простую стартовую конфигурацию в файл /opt/zookeeper/conf/zookeeper.properties:

dataDir=/opt/zookeeper/data

# zoo cluster node info
#server.id=hostname:port1:port2
	  # id - The ID of the Zookeeper cluster node.
	  # hostname - The hostname or IP address where the node listens for connections.
	  # port1 - The port number used for intra-cluster communication.
	  # port2 - The port number used for leader election.
server.1=zoo1.local:2888:3888
server.2=zoo2.local:2888:3888
server.3=zoo3.local:2888:3888

## Metrics Providers
# https://prometheus.io Metrics Exporter
metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
metricsProvider.httpHost=0.0.0.0
metricsProvider.httpPort=8006
metricsProvider.exportJvmInfo=true

# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial synchronization phase can take
initLimit=10
# The number of ticks that can pass between sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored. 

#max client connections number
maxClientCnxns=60
		
# the port at which the clients will connect in plain text (it should be commented and disabled)
clientPort=2181

И на всех узлах напишем unit file для systemd /etc/systemd/system/zookeeper.service. В этом файле мы установим важную переменную JAVA_HOME, а также через переменную ZK_SERVER_HEAP немного расширим HEAP память:

exit
sudo vim /etc/systemd/system/zookeeper.service
[Unit]
Description=Apache Zookeeper server
Documentation=http://zookeeper.apache.org
Requires=network.target remote-fs.target
After=network.target remote-fs.target
		 
[Service]
Type=forking
User=zookeeper
Group=zookeeper

Environment="JAVA_HOME=/usr/lib/jvm/jre-11"

#default heap size is 1000 Mb (set in zkEnv.sh)
Environment="ZK_SERVER_HEAP=2000"

ExecStart=/opt/zookeeper/bin/zkServer.sh start /opt/zookeeper/conf/zookeeper.properties
ExecStop=/opt/zookeeper/bin/zkServer.sh stop
Restart=always
		 
[Install]
WantedBy=multi-user.target

Перед первым запуском важно в папке data создать уникальные идентификаторы узлов. Это важно, иначе ничего не заработает:

sudo su - zookeeper
# Только на первом узле 
	echo 1 > /opt/zookeeper/data/myid 
# Только на втором узле
	echo 2 > /opt/zookeeper/data/myid 
# Только на третьем узле
	echo 3 > /opt/zookeeper/data/myid
exit

На всех узлах читаем unit файл systemd, запускаемся и пробуем подключиться к Zk консольным клиентом. Должно быть все в порядке, но на всякий случай следует заглянуть в лог файлы Zk:

sudo systemctl daemon-reload
sudo systemctl enable --now zookeeper
sudo systemctl status zookeeper --no-pager
sudo su - zookeeper
bin/zkCli.sh -server 127.0.0.1:2181 ls /zookeeper
      #должны увидеть строку
      #[config, quota]
tail -f logs/zookeeper-zookeeper-server-$(hostname -f).out

Если видите кучу ошибок и узлы не подключаются друг к другу — еще раз проверьте, что файлы /opt/zookeeper/data/myid на всех узлах содержат уникальный номер.

Quorum TLS

В предыдущей серии мы уже создали папку /opt/zookeeper/ssl, в ней будут лежать все хранилища ключей. Сейчас мы займемся сертификатами для шифрования данных, передаваемых между узлами кластера.

Перейдем в папку /opt/zookeeper/ssl, создадим на всех узлах еще одну вложенную — Quorum-TLS-CA. В ней будет размещен корневой сертификат, которым мы подпишем ключи для шифрования внутрикластерного трафика:

cd /opt/zookeeper/ssl
mkdir -m 700 Quorum-TLS-CA

На одном из узлов создаем CA сертификат для Quorum, со сроком действия в 10 лет:

openssl req -new -newkey rsa:4096 -days 3650 -x509 -subj "/CN=ZooKeeper-Security-CA" -keyout Quorum-TLS-CA/ca-key -out Quorum-TLS-CA/ca-cert -nodes

Для удобства дальнейшей настройки лучше скопировать полученные файлы Quorum-TLS-CA/ca-key и Quorum-TLS-CA/ca-cert на все другие узлы (не нужно генерировать свой CA на каждом узле, он нужен один):

# Только на первом узле
ssh-keygen -C zookeeperKey -t ed25519
cat /opt/zookeeper/.ssh/id_ed25519.pub >> /opt/zookeeper/.ssh/authorized_keys
chmod 600 /opt/zookeeper/.ssh/authorized_keys
cat /opt/zookeeper/.ssh/id_ed25519
    #секретный ключ
cat /opt/zookeeper/.ssh/id_ed25519.pub
    #публичный ключ

# На втором и третьем узле
mkdir .ssh
vim /opt/zookeeper/.ssh/id_ed25519
    #вставить секретный ключ из сессии первого хоста
vim /opt/zookeeper/.ssh/authorized_keys
    #вставить публичный ключ из сессии первого хоста
chmod 600 /opt/zookeeper/.ssh/authorized_keys /opt/zookeeper/.ssh/id_ed25519

# Только на первом узле
cd /opt/zookeeper/ssl
scp Quorum-TLS-CA/* zoo2.local:/opt/zookeeper/ssl/Quorum-TLS-CA
scp Quorum-TLS-CA/* zoo3.local:/opt/zookeeper/ssl/Quorum-TLS-CA

На всех узлах в переменную окружения задаем пароль для хранилищ сертификатов для кворума. Этот пароль будет одинаковым и для сертификата, и для хранилищ сертификатов key/trust:

export QPASS=ChangeMeSecretQpass

На всех узлах создаём хранилище доверенных сертификатов (truststore) для zookeeper-quorum, добавляем в него сертификат CA. Файл хранилища будет называться zookeeper-quorum.truststore.jks:

keytool -keystore zookeeper-quorum.truststore.jks -alias CARoot -import -file Quorum-TLS-CA/ca-cert -storepass $QPASS -keypass $QPASS -noprompt

На всех узлах создаём хранилище ключей (keystore) для zookeeper-quorum. Тут указывается имя узла, предподстановка шелла $(hostname -f) нам в этом поможет. Сгенерированный ключ надо будет подписать:

keytool -genkeypair -alias $(hostname -f) -keyalg RSA -keysize 2048 -dname "cn=$(hostname -f)" -keypass $QPASS -keystore zookeeper-quorum.keystore.jks -storepass $QPASS

На всех узлах создаем CSR (Certificate Signing Request) = запрос на выдачу (подпись) SSL сертификата:

keytool -keystore zookeeper-quorum.keystore.jks -alias $(hostname -f) -certreq -file ca-request-zookeeper-$(hostname -f) -storepass $QPASS -keypass $QPASS

Если вы заранее скопировали Quorum-TLS-CA/ca-key и Quorum-TLS-CA/ca-cert на все узлы, то дальше просто выполните команду по подписи этих CSR, в противном случае вам нужно подписать эти файлы там, где есть CA.

Подписываем все три сертификата. В нашем случае ca-key и ca-cert уже есть на всех узлах кластера:

openssl x509 -req -CA Quorum-TLS-CA/ca-cert -CAkey Quorum-TLS-CA/ca-key -in ca-request-zookeeper-$(hostname -f) -out cert-zookeeper-$(hostname -f)-signed -days 3650 -CAcreateserial -passin pass:$QPASS

На всех узлах добавляем сертификат CA в хранилище ключей Zookeeper (keystore):

keytool -keystore zookeeper-quorum.keystore.jks -import -file Quorum-TLS-CA/ca-cert -storepass $QPASS -keypass $QPASS -noprompt -alias CARoot

На всех узлах добавляем подписанный сертификат Zookeeper в хранилище ключей keystore:

keytool -keystore zookeeper-quorum.keystore.jks -import -file cert-zookeeper-$(hostname -f)-signed -storepass $QPASS -keypass $QPASS -alias $(hostname -f) -noprompt

На всех узлах проверяем, что внутри zookeeper-quorum.keystore.jks есть CA и сертификат ОДНОГО узла (т.е. всего два сертификата, не больше):

keytool -list -v -keystore zookeeper-quorum.keystore.jks -storepass $QPASS

На всех узлах удаляем теперь не нужный файл CSR, удаляем ненужный файл подписанного сертификата Zookeeper (он теперь в keystore):

rm cert-zookeeper-*-signed  ca-request-zookeeper-* Quorum-TLS-CA/ca-cert.srl

На всех узлах в /opt/zookeeper/conf/zookeeper.properties добавляем новые строчки конфигурации внутрикластерного взаимодействия. Особое внимание надо уделить паролю: раньше мы его указывали в переменной QPASS, теперь его надо указать дважды.

#Quorum TLS
sslQuorum=true 
serverCnxnFactory=org.apache.zookeeper.server.NettyServerCnxnFactory 
ssl.quorum.keyStore.location=/opt/zookeeper/ssl/zookeeper-quorum.keystore.jks
ssl.quorum.keyStore.password=ChangeMeSecretQpass
ssl.quorum.trustStore.location=/opt/zookeeper/ssl/zookeeper-quorum.truststore.jks
ssl.quorum.trustStore.password=ChangeMeSecretQpass

На всех узлах необходимо перезапустить Zookeeper, проверить логи, попробовать подключиться:

exit
sudo systemctl restart zookeeper
sudo su - zookeeper
tail -f logs/zookeeper-zookeeper-server-$(hostname -f).out

Сейчас кластер внутри работает по TLS, но снаружи пока PLAIN:

/opt/zookeeper/bin/zkCli.sh -server 127.0.0.1:2181 ls /zookeeper

Server TLS

В этой серии мы создадим CA для клиентских подключений, а также сертификаты для узлов кластера Zookeeper для внешних подключений. Затем аналогичным образом создадим сертификат для клиента.

На одном узле создаем CA для внешних подключений сроком действия 10 лет, разместим его в папке Client-TLS-CA:

cd /opt/zookeeper/ssl
mkdir -m 700 Client-TLS-CA
openssl req -new -newkey rsa:4096 -days 3650 -x509 -subj "/CN=ZooKeeper-Client-CA" -keyout Client-TLS-CA/ca-key -out Client-TLS-CA/ca-cert -nodes

Для удобства дальнейшей настройки лучше скопировать полученные файлы Client-TLS-CA/ca-key и Client-TLS-CA/ca-cert на все другие узлы (не нужно генерировать свой CA на каждом узле, он нужен один):

# Только на первом узле
cd /opt/zookeeper/ssl
for h in zoo2.local zoo3.local; do
  ssh ${h} mkdir -m 700 /opt/zookeeper/ssl/Client-TLS-CA
  scp Client-TLS-CA/* ${h}:/opt/zookeeper/ssl/Client-TLS-CA
done

На всех узлах в переменную окружения задаем пароль для серверных хранилищ сертификатов. Этот пароль будет одинаковым и для сертификата, и для хранилищ сертификатов key/trust:

export SPASS=ChangeMeSecretSpass

На всех узлах создаём хранилище доверенных сертификатов (truststore) для zookeeper-server, добавляем в него сертификат CA:

keytool -keystore zookeeper-server.truststore.jks -alias CARoot -import -file Client-TLS-CA/ca-cert -storepass $SPASS -keypass $SPASS -noprompt

На всех узлах создаём хранилище ключей (keystore) для zookeeper-server. Тут указывается имя узла, сгенерированный ключ надо будет подписать. Для удобства я использую предподстановку шелла $(hostname -f):

keytool -genkeypair -alias $(hostname -f) -keyalg RSA -keysize 2048 -dname "cn=$(hostname -f)" -keypass $SPASS -keystore zookeeper-server.keystore.jks -storepass $SPASS

На всех узлах создаём CSR (Certificate Signing Request) = запрос на выдачу (подпись) SSL сертификата:

keytool -keystore zookeeper-server.keystore.jks -alias $(hostname -f) -certreq -file ca-request-zookeeper-$(hostname -f) -storepass $SPASS -keypass $SPASS

Теперь подписываем сертификат. В нашем случае ca-key уже есть на всех узлах кластера, поэтому просто выполняем команду подписи:

openssl x509 -req -CA Client-TLS-CA/ca-cert -CAkey Client-TLS-CA/ca-key -in ca-request-zookeeper-$(hostname -f) -out cert-zookeeper-$(hostname -f)-signed -days 3650 -CAcreateserial -passin pass:$SPASS

На всех узлах добавляем сертификат CA в хранилище ключей Zookeeper (keystore):

keytool -keystore zookeeper-server.keystore.jks -import -file Client-TLS-CA/ca-cert -storepass $SPASS -keypass $SPASS -noprompt -alias CARoot

На всех узлах добавляем подписанный сертификат zookeeper в хранилище ключей Zookeeper (keystore):

keytool -keystore zookeeper-server.keystore.jks -import -file cert-zookeeper-$(hostname -f)-signed -storepass $SPASS -keypass $SPASS -alias $(hostname -f) -noprompt

На всех узлах проверяем, что внутри есть CA и сертификат ОДНОГО узла (т.е. всего два сертификата, не больше):

keytool -list -v -keystore zookeeper-server.keystore.jks -storepass $SPASS

На всех узлах удаляем теперь ненужный файл CSR, удаляем ненужный файл подписанного сертификата zookeeper-client (он теперь в keystore):

rm cert-zookeeper-*-signed  ca-request-zookeeper-* Client-TLS-CA/ca-cert.srl

На всех узлах необходимо добавить дополнительные настройки в /opt/zookeeper/conf/zookeeper.properties, уделяя внимание паролю ChangeMeSecretSpass:

#Server TLS
secureClientPort=2182
authProvider.x509=org.apache.zookeeper.server.auth.X509AuthenticationProvider
serverCnxnFactory=org.apache.zookeeper.server.NettyServerCnxnFactory
ssl.trustStore.location=/opt/zookeeper/ssl/zookeeper-server.truststore.jks
ssl.trustStore.password=ChangeMeSecretSpass
ssl.keyStore.location=/opt/zookeeper/ssl/zookeeper-server.keystore.jks
ssl.keyStore.password=ChangeMeSecretSpass
ssl.hostnameVerification=false                                                                                                                                          
ssl.clientAuth=need  

А также в этом файле следует закомментировать уже ненужную строку - поставить перед ней #:

#clientPort=2181

Ранее она отвечала за нешифрованные соединения, теперь они будут невозможны. Благодаря настройке ssl.hostnameVerification=false мы можем выдавать клиентам обезличенный файл с их сертификатом, и нам будет не важен их реальный hostname.

Client-TLS

На один из узлов добавляем обезличенные store файлы для клиентов. CA будет тот же, все команды аналогичны, просто у клиентов будет другой пароль и имя хоста по маске '*.local'. Альтернативно можно пускать всех с настройкой ssl.clientAuth=none (но так я не пробовал).

Для клиентов будет отдельный пароль, но тот же CA.

Для удобства все файлы для подключений клиентов будут отдельно в /opt/zookeeper/client-settings:

cd /opt/zookeeper/client-settings/
export CPASS=ChangeMeSecretCpass
#truststore
keytool -keystore zookeeper-client.truststore.jks -alias CARoot -import -file ../ssl/Client-TLS-CA/ca-cert -storepass $CPASS -keypass $CPASS -noprompt
#keystore
keytool -genkeypair -alias zkclient -keyalg RSA -keysize 2048 -dname "cn=*.local" -keypass $CPASS -keystore zookeeper-client.keystore.jks -storepass $CPASS
#gen CSR
keytool -keystore zookeeper-client.keystore.jks -alias zkclient -certreq -file ca-request-zookeeper-zkclient -storepass $CPASS -keypass $CPASS
#sign
openssl x509 -req -CA ../ssl/Client-TLS-CA/ca-cert -CAkey ../ssl/Client-TLS-CA/ca-key -in ca-request-zookeeper-zkclient -out cert-zookeeper-zkclient-signed -days 3650 -CAcreateserial -passin pass:$CPASS
#import signed
keytool -keystore zookeeper-client.keystore.jks -import -file ../ssl/Client-TLS-CA/ca-cert -storepass $CPASS -keypass $CPASS -noprompt -alias CARoot
keytool -keystore zookeeper-client.keystore.jks -import -file cert-zookeeper-zkclient-signed -storepass $CPASS -keypass $CPASS -alias zkclient -noprompt
#check
keytool -list -v -keystore zookeeper-client.keystore.jks -storepass $CPASS
#clean up
rm cert-zookeeper-*-signed  ca-request-zookeeper-* ..srl

Создадим файл с примером конфигурации zkCli.sh /opt/zookeeper/client-settings/zookeeper-client.properties, не забываем менять ChangeMeSecretCpass:

zookeeper.clientCnxnSocket=org.apache.zookeeper.ClientCnxnSocketNetty
zookeeper.client.secure=true
zookeeper.ssl.client.enable=true
zookeeper.ssl.protocol=TLSv1.2
zookeeper.ssl.trustStore.location=/opt/zookeeper/client-settings/zookeeper-client.truststore.jks
zookeeper.ssl.trustStore.password=ChangeMeSecretCpass
zookeeper.ssl.keyStore.location=/opt/zookeeper/client-settings/zookeeper-client.keystore.jks
zookeeper.ssl.keyStore.password=ChangeMeSecretCpass

##client will NOT ignore Zk server hostname
#zookeeper.ssl.hostnameVerification=false

Теперь содержимое папки /opt/zookeeper/client-settings можно синхронизировать между узлами кластера Zk, и в дальнейшем просто её выборочно предоставлять клиентам Zk:

scp /opt/zookeeper/client-settings/* zoo2.local:/opt/zookeeper/client-settings/
scp /opt/zookeeper/client-settings/* zoo3.local:/opt/zookeeper/client-settings/

Перезапустим сервис Zookeeper c новой конфигурацией. Мы его остановим, а уже настроенный systemd его запустит:

for h in zoo{1..3}.local ; do 
  echo Reloading Zk on $h
  ssh $h "pkill -u zookeeper java && sleep 5"
done

Попробуем проверить подключение по SSL:

/opt/zookeeper/bin/zkCli.sh -server zoo1.local:2182 -client-configuration /opt/zookeeper/client-settings/zookeeper-client.properties ls /zookeeper

SASL with MD5

Если вам не нужен ACL, то можно было бы остановиться и на этом, т.к. в кластер Zk смогут попасть только те клиенты, которые обладают подписанным сертификатом (аутентификацией), но мы добавим авторизацию.

Добавляем на всех узлах в /opt/zookeeper/conf/zookeeper.properties:

#SASL with Digest-MD5
requireClientAuthScheme=sasl
zookeeper.allowSaslFailedClients=false
zookeeper.sessionRequireClientSASLAuth=true
# You must add the authProvider.<ID> property for every server that is part of the Zookeeper cluster
authProvider.1=org.apache.zookeeper.server.auth.SASLAuthenticationProvider
authProvider.2=org.apache.zookeeper.server.auth.SASLAuthenticationProvider
authProvider.3=org.apache.zookeeper.server.auth.SASLAuthenticationProvider

authProvider тут три, по числу узлов в кластере 1,2,3.

К сожалению, в Zookeeper есть глюк, и он не считывает нужные переменные, позволяя подключаться к кластеру как с паролем, так и без него, поэтому позже часть настроек вынесем в unit файл systemd, там сработает.

На всех узлах продолжим составление файлов конфигураций.

Создадим новый файл /opt/zookeeper/client-settings/zookeeper-client-jaas.conf. Это клиентская версия конфига с паролем:

Client {
	   org.apache.zookeeper.server.auth.DigestLoginModule required
	   username="bercut"
	   password="ChangeMeSecretBUpass";
};

А /opt/zookeeper/conf/zookeeper-server-jaas.conf - серверные настройки с паролями. Особую роль играет пользователь super, т.к. на него не распространяются права ACL, ему можно все. Вместо пользователей bercut и test могут быть любые другие:

QuorumServer {
		   org.apache.zookeeper.server.auth.DigestLoginModule required
		   user_zookeeper="ChangeMeSecretZUpass";
};
QuorumLearner {
    	   org.apache.zookeeper.server.auth.DigestLoginModule required
		   username="zookeeper"
		   password="ChangeMeSecretZUpass";
};
Server {
		   org.apache.zookeeper.server.auth.DigestLoginModule required
		   user_super="ChangeMeSecretSUpass"
		   user_bercut="ChangeMeSecretBUpass"
		   user_test="ChangeMeSecretTUpass";
		};

На всех узлах добавим важные переменные окружения в unit systemd /etc/systemd/system/zookeeper.service:

Environment="SERVER_JVMFLAGS=-Djava.security.auth.login.config=/opt/zookeeper/conf/zookeeper-server-jaas.conf \
									 -Dzookeeper.authProvider.1=org.apache.zookeeper.server.auth.SASLAuthenticationProvider \
									 -Dzookeeper.authProvider.2=org.apache.zookeeper.server.auth.SASLAuthenticationProvider \
									 -Dzookeeper.authProvider.3=org.apache.zookeeper.server.auth.SASLAuthenticationProvider \
									 -Dzookeeper.allowSaslFailedClients=false \
									 -Dzookeeper.sessionRequireClientSASLAuth=true"
exit
sudo vim /etc/systemd/system/zookeeper.service
sudo systemctl daemon-reload
sudo systemctl restart zookeeper.service
sudo su - zookeeper

Если не добавить опции после jaas, то Zk будет пускать и с паролем, и без пароля по SSL. Эти настройки принудительно разрешают только SSL.

/opt/zookeeper/conf/zookeeper.properties (что получилось в итоге)
# zoo cluster node info
   #server.id=hostname:port1:port2
      # id - The ID of the Zookeeper cluster node.
      # hostname - The hostname or IP address where the node listens for connections.
      # port1 - The port number used for intra-cluster communication.
      # port2 - The port number used for leader election.
server.1=zoo1.local:2888:3888
server.2=zoo2.local:2888:3888
server.3=zoo3.local:2888:3888

# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial synchronization phase can take
initLimit=10
# The number of ticks that can pass between sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored. 
dataDir=/opt/zookeeper/data
# the port at which the clients will connect in plain text (it should be commented and disabled)
#clientPort=2181

# the maximum number of client connections. increase this if you need to handle more clients
maxClientCnxns=60
#
# Be sure to read the maintenance section of the 
# administrator guide before turning on autopurge.
#
# https://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
autopurge.snapRetainCount=3
# Purge task interval in hours
# Set to "0" to disable auto purge feature
autopurge.purgeInterval=1

## Metrics Providers
# https://prometheus.io Metrics Exporter
metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
metricsProvider.httpHost=0.0.0.0
metricsProvider.httpPort=8006
metricsProvider.exportJvmInfo=true

#Quorum TLS
sslQuorum=true 
serverCnxnFactory=org.apache.zookeeper.server.NettyServerCnxnFactory 
ssl.quorum.keyStore.location=/opt/zookeeper/ssl/zookeeper-quorum.keystore.jks
ssl.quorum.keyStore.password=ChangeMeSecretQpass
ssl.quorum.trustStore.location=/opt/zookeeper/ssl/zookeeper-quorum.truststore.jks
ssl.quorum.trustStore.password=ChangeMeSecretQpass
ssl.quorum.hostnameVerification=true

#Server TLS
secureClientPort=2182
authProvider.x509=org.apache.zookeeper.server.auth.X509AuthenticationProvider
serverCnxnFactory=org.apache.zookeeper.server.NettyServerCnxnFactory
ssl.trustStore.location=/opt/zookeeper/ssl/zookeeper-server.truststore.jks
ssl.trustStore.password=ChangeMeSecretCpass
ssl.keyStore.location=/opt/zookeeper/ssl/zookeeper-server.keystore.jks
ssl.keyStore.password=ChangeMeSecretCpass
ssl.hostnameVerification=false
ssl.clientAuth=none

#Quorum SASL
quorum.auth.enableSasl=true
quorum.auth.learnerRequireSasl=true
quorum.auth.serverRequireSasl=true
quorum.auth.learner.loginContext=QuorumLearner
quorum.auth.server.loginContext=QuorumServer
quorum.cnxn.threads.size=20

#SASL with Digest-MD5
requireClientAuthScheme=sasl
zookeeper.allowSaslFailedClients=false
zookeeper.sessionRequireClientSASLAuth=true
# You must add the authProvider.<ID> property for every server that is part of the Zookeeper cluster
authProvider.1=org.apache.zookeeper.server.auth.SASLAuthenticationProvider
authProvider.2=org.apache.zookeeper.server.auth.SASLAuthenticationProvider
authProvider.3=org.apache.zookeeper.server.auth.SASLAuthenticationProvider

/etc/systemd/system/zookeeper.service (что получилось в итоге)
[Unit]
Description=Apache Zookeeper server
Documentation=http://zookeeper.apache.org
Requires=network.target remote-fs.target
After=network.target remote-fs.target
 
[Service]
Type=forking
User=zookeeper
Group=zookeeper

Environment="JAVA_HOME=/usr/lib/jvm/jre-11"

#default heap size is 1000 Mb (set in zkEnv.sh)
Environment="ZK_SERVER_HEAP=2000"

Environment="SERVER_JVMFLAGS=-Djava.security.auth.login.config=/opt/zookeeper/conf/zookeeper-server-jaas.conf \
                             -Dzookeeper.authProvider.1=org.apache.zookeeper.server.auth.SASLAuthenticationProvider \
                             -Dzookeeper.authProvider.2=org.apache.zookeeper.server.auth.SASLAuthenticationProvider \
                             -Dzookeeper.authProvider.3=org.apache.zookeeper.server.auth.SASLAuthenticationProvider \
                             -Dzookeeper.allowSaslFailedClients=false \
                             -Dzookeeper.sessionRequireClientSASLAuth=true"

ExecStart=/opt/zookeeper/bin/zkServer.sh start /opt/zookeeper/conf/zookeeper.properties
ExecStop=/opt/zookeeper/bin/zkServer.sh stop
Restart=always
 
[Install]
WantedBy=multi-user.target

Проверка SASL+SSL

Зайдем в Zk через SSL+SASL и проверим, что мы можем посмотреть дерево:

CLIENT_JVMFLAGS="-Djava.security.auth.login.config=/opt/zookeeper/client-settings/zookeeper-client-jaas.conf" 
export CLIENT_JVMFLAGS
/opt/zookeeper/bin/zkCli.sh -server zoo1.local:2182,zoo2.local:2182,zoo3.local:2182 -client-configuration /opt/zookeeper/client-settings/zookeeper-client.properties ls /

Меняя логин-пароль в /opt/zookeeper/client-settings/zookeeper-client-jaas.conf, мы заходим в Zk под разными пользователями.

Если поменять логин-пароль в zookeeper-client-jaas.conf на заведомо неверный, то команда должна ломаться. Также подключение на несекретный порт 2181 тоже уже не должно работать.

Как работает ACL в действии?

С помощью утилиты zkCli подключимся к Zk пользователем bercut, создадим новый узел /test:

ls /
	[zookeeper]
create /test
create /test/1
set /test/1 123456

В корне любой пользователь по умолчанию может делать все что угодно. Проверим:

getAcl  /
	'world,'anyone
    : 		cdrwa

getAcl /test
	'world,'anyone
    : 		cdrwa

Буквы cdrwa означают:
(c) CREATE: можно создавать child node;
(d) DELETE: можно удалять узел;
(r) READ: можно читать узел и просматривать его подузлы;
(w) WRITE: можно записывать в узел;
(a) ADMIN: можно назначать права.

Изменим права. Заберем все права у всех пользователей, кроме bercut, на узел /test:

setAcl /test world:anyone:,sasl:bercut:cdrwa

Проверим изменения:

getAcl /test
	'world,'anyone
	: 
	'sasl,'bercut
	: cdrwa

getAcl /test/1
	'world,'anyone
	: cdrwa

Подключимся пользователем test. Для этого изменим конфиг для подключения /opt/zookeeper/client-settings/zookeeper-client-jaas.conf:

Client {
	   org.apache.zookeeper.server.auth.DigestLoginModule required
	   username="test"
	   password="ChangeMeSecretTUpass";
};

Он не может посмотреть, изменить или удалить узел /test:

ls /
  	[test, zookeeper]

ls /test
	Insufficient permission : /test

Пользователь test теперь не может посмотреть на узел /test, ACL сработали.

Однако, если подключиться пользователем super (super/ChangeMeSecretSUpass), то ACL работать не будут, повторюсь, пользователю super можно все, он же суперпользователь:

ls /
    [test, zookeeper]

ls /test
    [1]

get /test/1
	123456

Мониторинг

Внимательный читатель наверняка заметил в конфигурации необъясненные настройки metricsProvider.xxx. Да, это про мониторинг. Сильно не углубляюсь в то, как, что и зачем - статья не совсем об этом.

Если вам все же это интересно

Пример настройки мониторинга через Prometheus и Grafana

prometheus.yml (основная конфигурация Prometheus)
rule_files:
   - 'alerts/alerts.zookeeper.yml'
...
scrape_configs:
  - job_name: 'zookeeper'
    scrape_interval: 10s
    scrape_timeout:  9s
    metrics_path: /metrics
    file_sd_configs:
      - files:
        - 'inventory/*.yml'
    relabel_configs:
      - source_labels: [jobs]               #pick up config if it has 'zookeeper' in comma-separated list in label 'jobs', drop if not
        regex: '(.*,|^)zookeeper(,.*|$)'
        action: keep
      - regex: '^jobs$'                     #drop unused label 'jobs'
        action: labeldrop
      - source_labels: [__address__]        #save ip address into 'ip' label
        regex: '(.*)(:.*)?'
        replacement: '$1'
        target_label: ip
      - source_labels: [__address__]        #add port if it is absent in target, save ip:port to '__param_target' label
        regex: (.*)
        replacement: ${1}:8006
        target_label: __param_target
      - source_labels: [__param_target]     #copy '__param_target' label to 'instance' label 
        target_label: instance
      - source_labels: [__param_target]     #copy '__param_target' label to '__address__' label
        target_label: __address__
...

inventory/Bercut1.yml (список узлов)
---
- targets:
  - 192.168.1.11
  labels:
    jobs: "node_exporter,blackbox-ssh,zookeeper"
    owner: "habr"
    os: "linux"
    hostname: "zoo1.local"
    zooCluster: "Bercut1"
    noAlarmOn: "predict"
- targets:
  - 192.168.1.12
  labels:
    jobs: "node_exporter,blackbox-ssh,zookeeper"
    owner: "habr"
    os: "linux"
    hostname: "zoo2.local"
    zooCluster: "Bercut1"
    noAlarmOn: "predict"
- targets:
  - 192.168.1.13
  labels:
    jobs: "node_exporter,blackbox-ssh,zookeeper"
    owner: "habr"
    os: "linux"
    hostname: "zoo3.local"
    zooCluster: "Bercut1"
    noAlarmOn: "predict"

alerts/alerts.zookeeper.yml (описания алертов)
groups:
- name: zk-alerts
  rules:
  - alert: ZooKeeper server is down
    expr:  up{job="zookeeper"} == 0
    for: 1m
    labels:
      severity: critical
    annotations:
      summary: "Instance {{ $labels.instance }} ZooKeeper server is down"
      description: "{{ $labels.instance }} of job {{$labels.job}} ZooKeeper server is down: [{{ $value }}]."

  - alert: create too many znodes
    expr: znode_count{job="zookeeper"} > 1000000
    for: 1m
    labels:
      severity: warning
    annotations:
      summary: "Instance {{ $labels.instance }} create too many znodes"
      description: "{{ $labels.instance }} of job {{$labels.job}} create too many znodes: [{{ $value }}]."

  - alert: create too many connections
    expr: num_alive_connections{job="zookeeper"} > 50 # suppose we use the default maxClientCnxns: 60
    for: 1m
    labels:
      severity: warning
    annotations:
      summary: "Instance {{ $labels.instance }} create too many connections"
      description: "{{ $labels.instance }} of job {{$labels.job}} create too many connections: [{{ $value }}]."

  - alert: znode total occupied memory is too big
    expr: approximate_data_size{job="zookeeper"} /1024 /1024 > 1 * 1024 # more than 1024 MB(1 GB)
    for: 1m
    labels:
      severity: warning
    annotations:
      summary: "Instance {{ $labels.instance }} znode total occupied memory is too big"
      description: "{{ $labels.instance }} of job {{$labels.job}} znode total occupied memory is too big: [{{ $value }}] MB."

  - alert: set too many watch
    expr: watch_count{job="zookeeper"} > 10000
    for: 1m
    labels:
      severity: warning
    annotations:
      summary: "Instance {{ $labels.instance }} set too many watch"
      description: "{{ $labels.instance }} of job {{$labels.job}} set too many watch: [{{ $value }}]."

  - alert: a leader election happens
    expr: increase(election_time_count{job="zookeeper"}[5m]) > 0
    for: 1m
    labels:
      severity: warning
    annotations:
      summary: "Instance {{ $labels.instance }} a leader election happens"
      description: "{{ $labels.instance }} of job {{$labels.job}} a leader election happens: [{{ $value }}]."

  - alert: open too many files
    expr: open_file_descriptor_count{job="zookeeper"} > 300
    for: 1m
    labels:
      severity: warning
    annotations:
      summary: "Instance {{ $labels.instance }} open too many files"
      description: "{{ $labels.instance }} of job {{$labels.job}} open too many files: [{{ $value }}]."

  - alert: fsync time is too long
    expr: rate(fsynctime_sum{job="zookeeper"}[1m]) > 100
    for: 1m
    labels:
      severity: warning
    annotations:
      summary: "Instance {{ $labels.instance }} fsync time is too long"
      description: "{{ $labels.instance }} of job {{$labels.job}} fsync time is too long: [{{ $value }}]."

  - alert: take snapshot time is too long
    expr: rate(snapshottime_sum{job="zookeeper"}[5m]) > 100
    for: 1m
    labels:
      severity: warning
    annotations:
      summary: "Instance {{ $labels.instance }} take snapshot time is too long"
      description: "{{ $labels.instance }} of job {{$labels.job}} take snapshot time is too long: [{{ $value }}]."

  - alert: avg latency is too high
    expr: avg_latency{job="zookeeper"} > 100
    for: 1m
    labels:
      severity: warning
    annotations:
      summary: "Instance {{ $labels.instance }} avg latency is too high"
      description: "{{ $labels.instance }} of job {{$labels.job}} avg latency is too high: [{{ $value }}]."

  - alert: JvmMemoryFillingUp
    expr: jvm_memory_bytes_used{job="zookeeper"} / jvm_memory_bytes_max{area="heap"} > 0.8
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "JVM memory filling up (instance {{ $labels.instance }})"
      description: "JVM memory is filling up (> 80%)\n labels: {{ $labels }}  value = {{ $value }}\n"

Grafana default ZooKeeper dashboard

Послесловие

Я потратил достаточно много времени, чтобы найти описанные выше настройки. Некоторые из них хорошо описаны в официальной документации Zookeeper. Какие-то настройки там раскрыты очень скомкано и их по кусочкам нужно было вытягивать из настроек продуктов, которые работают вместе с Zk. А кое-что вообще пришлось искать на StackOverflow с дальнейшим блужданием по тикетам разработчиков Zk.

Конечно, было бы классно сразу просто выложить сценарий Ansible, который все автоматически настроит, но тогда бы не сложилось понимания, как это все работает.

Комментарии (0)