Безопасный Zookeeper: SASL with Digest-MD5

316a9ab60d802ab10467e722f0900860.jpg

Привет! Столкнулся с тем, что быстро не нашел простой инструкции, как с использованием 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. 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. 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

https://grafana.com/dashboards/10 465

Послесловие

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

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

© Habrahabr.ru