Безопасный Zookeeper: SASL with Digest-MD5
Привет! Столкнулся с тем, что быстро не нашел простой инструкции, как с использованием 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__
...
---
- 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"
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"
https://grafana.com/dashboards/10 465
Послесловие
Я потратил достаточно много времени, чтобы найти описанные выше настройки. Некоторые из них хорошо описаны в официальной документации Zookeeper. Какие-то настройки там раскрыты очень скомкано и их по кусочкам нужно было вытягивать из настроек продуктов, которые работают вместе с Zk. А кое-что вообще пришлось искать на StackOverflow с дальнейшим блужданием по тикетам разработчиков Zk.
Конечно, было бы классно сразу просто выложить сценарий Ansible, который все автоматически настроит, но тогда бы не сложилось понимания, как это все работает.