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

