Настройка провайдера Keycloak
Это первый из трех этапов настройки. Общий порядок приведен в статье Настройка двухфакторной аутентификации по протоколу OIDC.
Условные обозначения
Пути с $OS_HOME указывают не только на файлы, но и на сервер, где они находятся. Если сервисы размещены на разных серверах, заранее перенесите необходимые файлы между ними.
Базовый адрес Keycloak зависит от схемы публикации:
| Токен | Описание | Прямая публикация | Через прокси |
|---|---|---|---|
<KEYCLOAK_BASE> | базовый адрес Keycloak | https://<KEYCLOAK_HOST>:8443 | https://<KEYCLOAK_HOST>/keycloak |
Остальные обозначения от схемы публикации не зависят:
| Токен | Описание |
|---|---|
<KEYCLOAK_HOST> | внешнее имя хоста с Keycloak |
<KEYCLOAK_CA> | имя файла CA, которым подписан сертификат стороны, принимающей соединение: прокси, если он есть, иначе самого Keycloak. Копия файла размещается в каталоге конфигурации каждого сервиса отдельно |
<REALM_ID> | техническое имя realm |
<REALM_NAME> | читаемое имя realm (Display name) |
<LDAP_DOMAIN> | контроллер домена AD |
<POSTGRESQL_ADDRESS> | адрес сервера PostgreSQL |
<POSTGRESQL_PORT> | порт PostgreSQL |
$DB_PASS | пароль пользователя БД keycloak |
$OS_HOME | домашняя директория Smart Monitor Data Storage |
Подготовка Keycloak
Пользователь
Сначала создайте пользователя keycloak: он понадобится при проверке Java.
sudo useradd -r -d /opt/keycloak -s /sbin/nologin keycloak
Java
Keycloak 26.7 работает на OpenJDK 17, 21 или 25. В состав Smart Monitor входит Java 21, поэтому рекомендуется скопировать встроенную:
sudo cp -a $OS_HOME/jdk /opt/jdk-21
sudo chown -R root:root /opt/jdk-21
sudo chmod -R a+rX /opt/jdk-21
sudo -u keycloak /opt/jdk-21/bin/java -version
Системная Java подойдет, если доступны репозитории или Keycloak развернут на отдельном сервере:
sudo apt install -y openjdk-21-jre-headless # Debian/Ubuntu
sudo dnf install -y java-21-openjdk-headless # RHEL/Rocky
Дистрибутив
cd /tmp
curl -LO https://github.com/keycloak/keycloak/releases/download/26.7.1/keycloak-26.7.1.tar.gz
sudo tar -xzf keycloak-26.7.1.tar.gz -C /opt
sudo mv /opt/keycloak-26.7.1 /opt/keycloak
sudo chown -R keycloak:keycloak /opt/keycloak
Каталог переименовывается, чтобы пути в файле службы и в инструкции не зависели от версии Keycloak.
База данных
В инструкции используется PostgreSQL. Допустима любая БД из списка поддерживаемых. Встроенную H2 в эксплуатации применять не следует: ее нет в списке, а миграции схемы при обновлении на ней не гарантируются. Таблицы создавать не требуется, Liquibase поднимает схему сам.
CREATE USER keycloak WITH PASSWORD '$DB_PASS';
CREATE DATABASE keycloak OWNER keycloak ENCODING 'UTF8';
Для PostgreSQL 15 и новее дополнительно:
\c keycloak
GRANT ALL ON SCHEMA public TO keycloak;
Сертификат для Keycloak
При использовании прокси шаг пропускается целиком: соединение от браузера терминирует прокси своим сертификатом, а Keycloak слушает открытый HTTP на петлевом адресе.
Выбор CA
Сертификат Keycloak подписывается удостоверяющим центром (CA). Доступны два варианта:
-
CA кластера —
ca-cert.pemсодержит сертификат приватного CA, не привязанного к Smart Monitor Data Storage или конкретному серверу. Этот CA можно использовать для подписи сертификата Keycloak. В этом случае<KEYCLOAK_CA>—ca-cert.pem: файл уже находится в каталоге конфигурации Smart Monitor Data Storage; при необходимости скопируйте его в каталог Smart Monitor Web -
Отдельный CA — корпоративный PKI, внутренний ACME или самоподписанный CA, созданный для Keycloak. В этом случае
<KEYCLOAK_CA>— файл отдельного CA; его копия нужна в каталогах конфигурации Smart Monitor Data Storage и Smart Monitor Web
Выпуск на общем хосте
Если Keycloak развернут на том же сервере, что и Smart Monitor Data Storage, ключ CA доступен локально и все действия выполняются на одном сервере.
cd /opt/keycloak/conf
sudo openssl req -new -nodes -newkey rsa:2048 \
-keyout tls.key -out tls.csr \
-subj "/CN=<KEYCLOAK_HOST>"
printf 'subjectAltName=DNS:<KEYCLOAK_HOST>\n' | sudo tee tls.ext >/dev/null
sudo openssl x509 -req -in tls.csr -days 825 \
-CA $OS_HOME/config/ca-cert.pem \
-CAkey $OS_HOME/config/ca-key.pem -CAcreateserial \
-extfile tls.ext \
-out tls.crt
sudo chown keycloak:keycloak tls.key tls.crt
sudo chmod 640 tls.key
sudo chmod 644 tls.crt
Выпуск на отдельном сервере
Если Keycloak развернут на отдельном сервере, ключа CA на нем нет. Закрытый ключ Keycloak остается на его сервере, ключ CA — на сервере CA. Между серверами передаются только запрос и готовый сертификат.
На хосте Keycloak создайте ключ и запрос:
cd /opt/keycloak/conf
sudo openssl req -new -nodes -newkey rsa:2048 \
-keyout tls.key -out tls.csr \
-subj "/CN=<KEYCLOAK_HOST>"
Скопируйте файл tls.csr на машину, где находится ключ CA.
На хосте CA подпишите запрос:
printf 'subjectAltName=DNS:<KEYCLOAK_HOST>\n' > tls.ext
sudo openssl x509 -req -in tls.csr -days 825 \
-CA $OS_HOME/config/ca-cert.pem \
-CAkey $OS_HOME/config/ca-key.pem -CAcreateserial \
-extfile tls.ext \
-out tls.crt
На сервер Keycloak передается только tls.crt. Если CA является промежуточным, добавьте в tls.crt цепочку до корневого сертификата.
sudo chown keycloak:keycloak tls.key tls.crt
sudo chmod 640 tls.key
sudo chmod 644 tls.crt
Конфигурация
Файл: /opt/keycloak/conf/keycloak.conf
Прямая публикация
hostname=<KEYCLOAK_BASE>
https-certificate-file=/opt/keycloak/conf/tls.crt
https-certificate-key-file=/opt/keycloak/conf/tls.key
db=postgres
db-url=jdbc:postgresql://<POSTGRESQL_ADDRESS>:<POSTGRESQL_PORT>/keycloak
db-username=keycloak
db-password=$DB_PASS
Порт 8443 - значение по умолчанию для HTTPS, задавать отдельно не требуется; меняется параметром https-port. Keycloak занимает свой порт целиком, поэтому ни префикс пути, ни прием заголовков X-Forwarded-* ему не нужны.
Через прокси
hostname=<KEYCLOAK_BASE>
http-relative-path=/keycloak
http-enabled=true
http-host=127.0.0.1
http-port=8080
proxy-headers=xforwarded
db=postgres
db-url=jdbc:postgresql://<POSTGRESQL_ADDRESS>:<POSTGRESQL_PORT>/keycloak
db-username=keycloak
db-password=$DB_PASS
http-relative-path нужен, только если Keycloak публикуется под подпутем, а не в корне своего имени хоста. Если имя хоста у него собственное, параметр не задается, а <KEYCLOAK_BASE> выглядит как https://<KEYCLOAK_HOST> без префикса.
Если Keycloak делит имя хоста со Smart Monitor Web, префикс /auth не подойдет: на этом хосте пути /auth/openid/login и /auth/openid/logout уже заняты Smart Monitor Web.
http-host=127.0.0.1 закрывает прямой доступ к Keycloak мимо прокси.
Файл содержит пароль к базе данных, поэтому ограничьте доступ к нему:
sudo chown keycloak:keycloak /opt/keycloak/conf/keycloak.conf
sudo chmod 600 /opt/keycloak/conf/keycloak.conf
Прокси перед Keycloak
Шаг выполняется только в схеме с прокси. При прямой публикации он пропускается целиком.
Пример конфигурации nginx на сервере Keycloak:
location /keycloak/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_buffer_size 16k;
proxy_buffers 4 16k;
}
nginx -t && systemctl reload nginx
Запуск
sudo tee /etc/systemd/system/keycloak.service > /dev/null <<'EOF'
[Unit]
Description=Keycloak Identity Provider
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=keycloak
Group=keycloak
Environment=JAVA_HOME=/opt/jdk-21
Environment=JAVA_OPTS_KC_HEAP=-Xms256m -Xmx512m
ExecStart=/opt/keycloak/bin/kc.sh start
Restart=on-failure
RestartSec=10
TimeoutStartSec=180
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
[Install]
WantedBy=multi-user.target
EOF
sudo -u keycloak JAVA_HOME=/opt/jdk-21 /opt/keycloak/bin/kc.sh build
sudo systemctl daemon-reload
Администратор создается при остановленном сервисе. Команда интерактивная: она запросит имя пользователя и пароль.
sudo -u keycloak JAVA_HOME=/opt/jdk-21 /opt/keycloak/bin/kc.sh bootstrap-admin user
После создания администратора включите и запустите сервис:
sudo systemctl enable --now keycloak
Проверьте все три адреса, а не только issuer:
curl -sS --cacert $OS_HOME/config/<KEYCLOAK_CA> \
<KEYCLOAK_BASE>/realms/master/.well-known/openid-configuration \
| jq '.issuer, .authorization_endpoint, .token_endpoint'
Все три адреса должны содержать полный <KEYCLOAK_BASE>. Выполняйте запрос без -k.
Настройка realm
Создание realm
Секреты OTP привязаны к realm: при создании нового realm пользователям потребуется заново привязать аутентификатор. Отдельный realm следует создавать только для нового кластера.
Перейдите в Create realm - Realm name и укажите <REALM_ID>.
Перед дальнейшими действиями убедитесь, что консоль переключилась в новый realm: Manage realms, справа от <REALM_ID> надпись Current realm.
В разделе Realm settings - General задайте:
- параметр
Display name—<REALM_NAME> - параметр
Require SSL—External requests
Display name попадает в заголовок страницы входа и в название записи в приложении-аутентификаторе. С пустым значением там окажется техническое <REALM_ID>, и записи разных систем могут быть трудно различимы. Значение Require SSL менять не требуется: HTTP разрешен только с приватных адресов.
В разделе Realm settings - Login задайте:
- параметр
Verify email—Off
Иначе пользователи без почты не войдут.
Локализация окна входа
По умолчанию окно входа Keycloak отображается на английском языке. Чтобы настроить локализацию, перейдите в Realm Settings - Localization - Locales и задайте:
- параметр
Internationalization—Enabled - параметр
Supported locales— добавьтеRussian - параметр
Default locale—Russian
Перезапустите Keycloak:
systemctl restart keycloak
Создание темы Smart Monitor
Создайте каталоги и файлы темы:
sudo mkdir -p /opt/keycloak/themes/smartmonitor/login/{messages,resources}
sudo mkdir -p /opt/keycloak/themes/smartmonitor/login/resources/css
sudo touch /opt/keycloak/themes/smartmonitor/login/resources/css/smartmonitor.css
sudo touch /opt/keycloak/themes/smartmonitor/login/theme.properties
sudo touch /opt/keycloak/themes/smartmonitor/login/messages/{messages_en.properties,messages_ru.properties}
Добавьте содержимое в соответствующие файлы:
/opt/keycloak/themes/smartmonitor/login/resources/css/smartmonitor.css
:root {
--sm-green: #3B8646;
--sm-green-dark: #2F6D38;
--sm-bg: #F0F1F2;
--sm-border: #E1E3E6;
--sm-text: #1A1A1A;
}
body,
body.login-pf,
.login-pf-page,
.pf-v5-c-background-image,
#kc-container,
#kc-container-wrapper {
background: var(--sm-bg) !important;
background-image: none !important;
font-family: 'Source Sans 3', 'Source Sans Pro', -apple-system,
'Segoe UI', Roboto, Arial, sans-serif !important;
color: var(--sm-text);
}
.card-pf,
.pf-v5-c-card,
.pf-v5-c-login__main,
.pf-v5-c-login__main-header,
.pf-v5-c-login__main-body,
#kc-header,
#kc-header-wrapper,
#kc-content,
#kc-content-wrapper,
#kc-form,
#kc-form-wrapper {
background: transparent !important;
box-shadow: none !important;
border: none !important;
border-top: none !important;
border-bottom: none !important;
}
hr,
.pf-v5-c-divider,
.pf-v5-c-login__main-header::after,
.pf-v5-c-login__main-header::before {
display: none !important;
border: none !important;
content: none !important;
}
.login-pf-page,
.pf-v5-c-login,
.pf-v5-c-login__container,
.pf-v5-c-login__main,
#kc-container,
#kc-container-wrapper {
display: block !important;
justify-content: flex-start !important;
align-items: stretch !important;
min-height: 0 !important;
padding-top: 0 !important;
margin-top: 0 !important;
}
#kc-header,
#kc-header-wrapper,
.pf-v5-c-login__main-header,
.login-pf-header {
padding: 0 !important;
margin: 0 !important;
}
#kc-header,
#kc-header-wrapper {
font-size: 0 !important;
line-height: 0 !important;
padding: 0 !important;
margin: 0 !important;
text-align: center;
}
#kc-header-wrapper::before {
content: "";
display: block;
width: 340px;
max-width: 80vw;
height: 48px;
margin: 10% auto 16px auto;
background: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQyMSIgaGVpZ2h0PSIxOTkiIHZpZXdCb3g9IjAgMCAxNDIxIDE5OSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTUwMC42OTIgMTI4LjI2OUM1MDAuNjkyIDEzNC40NDcgNDk5LjE0OCAxMzkuNzIyIDQ5Ni4wNTkgMTQ0LjA5NUM0OTMuMDE3IDE0OC40MiA0ODguNzQgMTUxLjcyMyA0ODMuMjI3IDE1NC4wMDRDNDc3LjcxNCAxNTYuMjg1IDQ3MS4yMjYgMTU3LjQyNiA0NjMuNzY1IDE1Ny40MjZDNDU5LjgyIDE1Ny40MjYgNDU2LjA4OSAxNTcuMjM2IDQ1Mi41NzIgMTU2Ljg1NUM0NDkuMDU1IDE1Ni40NzUgNDQ1LjgyNCAxNTUuOTI5IDQ0Mi44NzcgMTU1LjIxNkM0MzkuOTMgMTU0LjUwMyA0MzcuMzQgMTUzLjYyNCA0MzUuMTA2IDE1Mi41NzhWMTQwLjk1OEM0MzguNjcxIDE0Mi40MzEgNDQzLjAyIDE0My44MSA0NDguMTUyIDE0NS4wOTNDNDUzLjI4NSAxNDYuMzI4IDQ1OC42NTYgMTQ2Ljk0NiA0NjQuMjY0IDE0Ni45NDZDNDY5LjQ5MiAxNDYuOTQ2IDQ3My45MTEgMTQ2LjI1NyA0NzcuNTIzIDE0NC44NzlDNDgxLjEzNSAxNDMuNDUzIDQ4My44NjggMTQxLjQzMyA0ODUuNzIyIDEzOC44MTlDNDg3LjYyMyAxMzYuMTU4IDQ4OC41NzMgMTMyLjk3NCA0ODguNTczIDEyOS4yNjdDNDg4LjU3MyAxMjUuNzAyIDQ4Ny43ODkgMTIyLjczMiA0ODYuMjIxIDEyMC4zNTVDNDg0LjY1MiAxMTcuOTMyIDQ4Mi4wMzggMTE1Ljc0NSA0NzguMzc5IDExMy43OTdDNDc0Ljc2NyAxMTEuODAxIDQ2OS44MjQgMTA5LjY4NiA0NjMuNTUxIDEwNy40NTJDNDU5LjEzMSAxMDUuODg0IDQ1NS4yMzQgMTA0LjE3MyA0NTEuODU5IDEwMi4zMTlDNDQ4LjQ4NSAxMDAuNDE4IDQ0NS42NTcgOTguMjc5NiA0NDMuMzc2IDk1LjkwMzNDNDQxLjA5NSA5My41MjcgNDM5LjM2IDkwLjc3MDUgNDM4LjE3MiA4Ny42MzM4QzQzNy4wMzEgODQuNDk3MSA0MzYuNDYxIDgwLjkwODkgNDM2LjQ2MSA3Ni44NjkxQzQzNi40NjEgNzEuMzA4NiA0MzcuODYzIDY2LjU1NiA0NDAuNjY3IDYyLjYxMTNDNDQzLjUxOSA1OC42MTkxIDQ0Ny40MzkgNTUuNTc3NSA0NTIuNDMgNTMuNDg2M0M0NTcuNDY3IDUxLjM0NzcgNDYzLjI0MiA1MC4yNzgzIDQ2OS43NTMgNTAuMjc4M0M0NzUuMzEzIDUwLjI3ODMgNDgwLjQ0NiA1MC44MDExIDQ4NS4xNTEgNTEuODQ2N0M0ODkuOTA0IDUyLjg5MjMgNDk0LjI1MyA1NC4yOTQzIDQ5OC4xOTcgNTYuMDUyN0w0OTQuNDE5IDY2LjQ2MDlDNDkwLjY2NCA2NC44OTI2IDQ4Ni42NzIgNjMuNTg1NiA0ODIuNDQyIDYyLjU0QzQ3OC4yNiA2MS40OTQ1IDQ3My45MzUgNjAuOTcxNyA0NjkuNDY4IDYwLjk3MTdDNDY1IDYwLjk3MTcgNDYxLjIyMiA2MS42MzcgNDU4LjEzMyA2Mi45Njc4QzQ1NS4wOTEgNjQuMjUxIDQ1Mi43NjIgNjYuMDgwNyA0NTEuMTQ2IDY4LjQ1N0M0NDkuNTMxIDcwLjgzMzMgNDQ4LjcyMyA3My42NjExIDQ0OC43MjMgNzYuOTQwNEM0NDguNzIzIDgwLjU5OTkgNDQ5LjQ4MyA4My42NDE2IDQ1MS4wMDQgODYuMDY1NEM0NTIuNTcyIDg4LjQ4OTMgNDU1LjA0NCA5MC42NTE3IDQ1OC40MTggOTIuNTUyN0M0NjEuODQgOTQuNDA2MiA0NjYuMzU1IDk2LjM1NDggNDcxLjk2MyA5OC4zOTg0QzQ3OC4wOTQgMTAwLjYzMiA0ODMuMjk4IDEwMy4wMDggNDg3LjU3NSAxMDUuNTI3QzQ5MS44NTMgMTA3Ljk5OSA0OTUuMTA4IDExMS4wNCA0OTcuMzQyIDExNC42NTJDNDk5LjU3NiAxMTguMjE3IDUwMC42OTIgMTIyLjc1NiA1MDAuNjkyIDEyOC4yNjlaTTYwNS4wNiA3Ni40NDE0QzYxMy42NjIgNzYuNDQxNCA2MjAuMTQ5IDc4LjY5ODkgNjI0LjUyMSA4My4yMTM5QzYyOC44OTQgODcuNjgxMyA2MzEuMDggOTQuODgxNSA2MzEuMDggMTA0LjgxNFYxNTZINjE5LjM4OVYxMDUuMzg1QzYxOS4zODkgOTkuMDYzOCA2MTguMDEgOTQuMzM1IDYxNS4yNTQgOTEuMTk4MkM2MTIuNTQ1IDg4LjAxNCA2MDguNTA1IDg2LjQyMTkgNjAzLjEzNSA4Ni40MjE5QzU5NS41NzggODYuNDIxOSA1OTAuMDg5IDg4LjYwODEgNTg2LjY2NyA5Mi45ODA1QzU4My4yNDUgOTcuMzUyOSA1ODEuNTM0IDEwMy43OTMgNTgxLjUzNCAxMTIuM1YxNTZINTY5Ljc3MVYxMDUuMzg1QzU2OS43NzEgMTAxLjE1NSA1NjkuMTU0IDk3LjYzOCA1NjcuOTE4IDk0LjgzNEM1NjYuNzMgOTIuMDI5OSA1NjQuOTI0IDg5LjkzODggNTYyLjUgODguNTYwNUM1NjAuMTI0IDg3LjEzNDggNTU3LjEwNiA4Ni40MjE5IDU1My40NDYgODYuNDIxOUM1NDguMjY2IDg2LjQyMTkgNTQ0LjEwNyA4Ny40OTEyIDU0MC45NzEgODkuNjI5OUM1MzcuODM0IDkxLjc2ODYgNTM1LjU1MyA5NC45NTI4IDUzNC4xMjcgOTkuMTgyNkM1MzIuNzQ5IDEwMy4zNjUgNTMyLjA2IDEwOC41MjEgNTMyLjA2IDExNC42NTJWMTU2SDUyMC4yMjZWNzcuODY3Mkg1MjkuNzc4TDUzMS41NjEgODguOTE3SDUzMi4yMDJDNTMzLjc3MSA4Ni4yMDggNTM1Ljc0MyA4My45MjY4IDUzOC4xMTkgODIuMDczMkM1NDAuNDk1IDgwLjIxOTcgNTQzLjE1NyA3OC44MTc3IDU0Ni4xMDQgNzcuODY3MkM1NDkuMDUgNzYuOTE2NyA1NTIuMTg3IDc2LjQ0MTQgNTU1LjUxNCA3Ni40NDE0QzU2MS40NTQgNzYuNDQxNCA1NjYuNDIxIDc3LjU1ODMgNTcwLjQxMyA3OS43OTJDNTc0LjQ1MyA4MS45NzgyIDU3Ny4zNzYgODUuMzI4OCA1NzkuMTgyIDg5Ljg0MzhINTc5LjgyM0M1ODIuMzkgODUuMzI4OCA1ODUuOTA3IDgxLjk3ODIgNTkwLjM3NCA3OS43OTJDNTk0Ljg0MSA3Ny41NTgzIDU5OS43MzcgNzYuNDQxNCA2MDUuMDYgNzYuNDQxNFpNNjg0LjYxOCA3Ni41ODRDNjkzLjkzMyA3Ni41ODQgNzAwLjg0OCA3OC42NzUxIDcwNS4zNjMgODIuODU3NEM3MDkuODc4IDg3LjAzOTcgNzEyLjEzNiA5My43MTcxIDcxMi4xMzYgMTAyLjg5VjE1Nkg3MDMuNTFMNzAxLjIyOSAxNDQuNDUxSDcwMC42NThDNjk4LjQ3MiAxNDcuMzAzIDY5Ni4xOTEgMTQ5LjcwMyA2OTMuODE0IDE1MS42NTFDNjkxLjQzOCAxNTMuNTUyIDY4OC42ODIgMTU1LjAwMiA2ODUuNTQ1IDE1NkM2ODIuNDU2IDE1Ni45NTEgNjc4LjY1NCAxNTcuNDI2IDY3NC4xMzkgMTU3LjQyNkM2NjkuMzg2IDE1Ny40MjYgNjY1LjE1NiAxNTYuNTk0IDY2MS40NDkgMTU0LjkzMUM2NTcuNzkgMTUzLjI2NyA2NTQuODkxIDE1MC43NDggNjUyLjc1MiAxNDcuMzc0QzY1MC42NjEgMTQ0IDY0OS42MTUgMTM5LjcyMiA2NDkuNjE1IDEzNC41NDJDNjQ5LjYxNSAxMjYuNzQ4IDY1Mi43MDQgMTIwLjc1OSA2NTguODgzIDExNi41NzdDNjY1LjA2MSAxMTIuMzk1IDY3NC40NzEgMTEwLjExNCA2ODcuMTEzIDEwOS43MzNMNzAwLjU4NyAxMDkuMTYzVjEwNC4zODdDNzAwLjU4NyA5Ny42MzggNjk5LjEzNyA5Mi45MDkyIDY5Ni4yMzggOTAuMjAwMkM2OTMuMzM5IDg3LjQ5MTIgNjg5LjI1MiA4Ni4xMzY3IDY4My45NzcgODYuMTM2N0M2NzkuODg5IDg2LjEzNjcgNjc1Ljk5MiA4Ni43MzA4IDY3Mi4yODUgODcuOTE4OUM2NjguNTc4IDg5LjEwNzEgNjY1LjA2MSA5MC41MDkxIDY2MS43MzQgOTIuMTI1TDY1OC4wOTkgODMuMTQyNkM2NjEuNjE2IDgxLjMzNjYgNjY1LjY1NSA3OS43OTIgNjcwLjIxOCA3OC41MDg4QzY3NC43OCA3Ny4yMjU2IDY3OS41OCA3Ni41ODQgNjg0LjYxOCA3Ni41ODRaTTcwMC40NDQgMTE3LjUwNEw2ODguNTM5IDExOC4wMDNDNjc4Ljc5NiAxMTguMzgzIDY3MS45MjkgMTE5Ljk3NSA2NjcuOTM3IDEyMi43NzlDNjYzLjk0NCAxMjUuNTgzIDY2MS45NDggMTI5LjU1MiA2NjEuOTQ4IDEzNC42ODVDNjYxLjk0OCAxMzkuMTUyIDY2My4zMDMgMTQyLjQ1NSA2NjYuMDEyIDE0NC41OTRDNjY4LjcyMSAxNDYuNzMyIDY3Mi4zMDkgMTQ3LjgwMiA2NzYuNzc2IDE0Ny44MDJDNjgzLjcxNSAxNDcuODAyIDY4OS4zOTUgMTQ1Ljg3NyA2OTMuODE0IDE0Mi4wMjdDNjk4LjIzNCAxMzguMTc4IDcwMC40NDQgMTMyLjQwMyA3MDAuNDQ0IDEyNC43MDRWMTE3LjUwNFpNNzcyLjAxOSA3Ni40NDE0Qzc3My41ODcgNzYuNDQxNCA3NzUuMjI3IDc2LjUzNjUgNzc2LjkzOCA3Ni43MjY2Qzc3OC42NDggNzYuODY5MSA3ODAuMTY5IDc3LjA4MyA3ODEuNSA3Ny4zNjgyTDc4MC4wMDMgODguMzQ2N0M3NzguNzIgODguMDE0IDc3Ny4yOTQgODcuNzUyNiA3NzUuNzI2IDg3LjU2MjVDNzc0LjE1NyA4Ny4zNzI0IDc3Mi42ODQgODcuMjc3MyA3NzEuMzA2IDg3LjI3NzNDNzY4LjE2OSA4Ny4yNzczIDc2NS4xOTkgODcuOTE4OSA3NjIuMzk1IDg5LjIwMjFDNzU5LjYzOCA5MC40Mzc4IDc1Ny4yMTQgOTIuMjQzOCA3NTUuMTIzIDk0LjYyMDFDNzUzLjAzMiA5Ni45NDg5IDc1MS4zOTIgOTkuNzc2NyA3NTAuMjA0IDEwMy4xMDRDNzQ5LjAxNiAxMDYuMzgzIDc0OC40MjIgMTEwLjA0MiA3NDguNDIyIDExNC4wODJWMTU2SDczNi41MTdWNzcuODY3Mkg3NDYuMzU0TDc0Ny42MzggOTIuMjY3Nkg3NDguMTM3Qzc0OS43NTMgODkuMzY4NSA3NTEuNzAxIDg2LjczMDggNzUzLjk4MiA4NC4zNTQ1Qzc1Ni4yNjQgODEuOTMwNyA3NTguOTAxIDgwLjAwNTkgNzYxLjg5NiA3OC41ODAxQzc2NC45MzcgNzcuMTU0MyA3NjguMzEyIDc2LjQ0MTQgNzcyLjAxOSA3Ni40NDE0Wk04MjEuNDkzIDE0Ny43M0M4MjMuNDQyIDE0Ny43MyA4MjUuNDM4IDE0Ny41NjQgODI3LjQ4MSAxNDcuMjMxQzgyOS41MjUgMTQ2Ljg5OSA4MzEuMTg4IDE0Ni40OTUgODMyLjQ3MiAxNDYuMDJWMTU1LjIxNkM4MzEuMDkzIDE1NS44MzQgODI5LjE2OSAxNTYuMzU2IDgyNi42OTcgMTU2Ljc4NEM4MjQuMjczIDE1Ny4yMTIgODIxLjg5NyAxNTcuNDI2IDgxOS41NjggMTU3LjQyNkM4MTUuNDM0IDE1Ny40MjYgODExLjY3OSAxNTYuNzEzIDgwOC4zMDUgMTU1LjI4N0M4MDQuOTMgMTUzLjgxNCA4MDIuMjIxIDE1MS4zNDIgODAwLjE3OCAxNDcuODczQzc5OC4xODIgMTQ0LjQwNCA3OTcuMTg0IDEzOS42MDQgNzk3LjE4NCAxMzMuNDczVjg3LjEzNDhINzg2LjA2MlY4MS4zNjA0TDc5Ny4yNTUgNzYuNzI2Nkw4MDEuOTYgNTkuNzU5OEg4MDkuMDg5Vjc3Ljg2NzJIODMxLjk3M1Y4Ny4xMzQ4SDgwOS4wODlWMTMzLjExNkM4MDkuMDg5IDEzOC4wMTEgODEwLjIwNiAxNDEuNjcxIDgxMi40MzkgMTQ0LjA5NUM4MTQuNzIxIDE0Ni41MTkgODE3LjczOSAxNDcuNzMgODIxLjQ5MyAxNDcuNzNaTTkzMy44NDUgMTU2TDg5OC44NDIgNjMuNzUySDg5OC4yNzFDODk4LjQ2MiA2NS43MDA1IDg5OC42MjggNjguMDc2OCA4OTguNzcxIDcwLjg4MDlDODk4LjkxMyA3My42ODQ5IDg5OS4wMzIgNzYuNzUwMyA4OTkuMTI3IDgwLjA3NzFDODk5LjIyMiA4My4zNTY0IDg5OS4yNyA4Ni43MDcgODk5LjI3IDkwLjEyODlWMTU2SDg4OC4wMDZWNTEuNzc1NEg5MDUuOTcxTDkzOC45NzggMTM4LjQ2M0g5MzkuNDc3TDk3Mi45ODIgNTEuNzc1NEg5OTAuODA1VjE1Nkg5NzguODI4Vjg5LjI3MzRDOTc4LjgyOCA4Ni4xODQyIDk3OC44NzYgODMuMDk1MSA5NzguOTcxIDgwLjAwNTlDOTc5LjA2NiA3Ni44NjkxIDk3OS4xODUgNzMuOTIyNSA5NzkuMzI3IDcxLjE2NkM5NzkuNDcgNjguMzYyIDk3OS42MTIgNjUuOTM4MiA5NzkuNzU1IDYzLjg5NDVIOTc5LjE4NUw5NDMuNzU0IDE1Nkg5MzMuODQ1Wk0xMDg0LjkxIDExNi43OTFDMTA4NC45MSAxMjMuMjA3IDEwODQuMDcgMTI4LjkzNCAxMDgyLjQxIDEzMy45NzJDMTA4MC43NSAxMzkuMDA5IDEwNzguMzUgMTQzLjI2MyAxMDc1LjIxIDE0Ni43MzJDMTA3Mi4wNyAxNTAuMjAyIDEwNjguMjcgMTUyLjg2MyAxMDYzLjggMTU0LjcxN0MxMDU5LjM4IDE1Ni41MjMgMTA1NC4zNyAxNTcuNDI2IDEwNDguNzYgMTU3LjQyNkMxMDQzLjUzIDE1Ny40MjYgMTAzOC43MyAxNTYuNTIzIDEwMzQuMzYgMTU0LjcxN0MxMDMwLjA0IDE1Mi44NjMgMTAyNi4yOCAxNTAuMjAyIDEwMjMuMSAxNDYuNzMyQzEwMTkuOTYgMTQzLjI2MyAxMDE3LjUxIDEzOS4wMDkgMTAxNS43NiAxMzMuOTcyQzEwMTQuMDQgMTI4LjkzNCAxMDEzLjE5IDEyMy4yMDcgMTAxMy4xOSAxMTYuNzkxQzEwMTMuMTkgMTA4LjIzNiAxMDE0LjY0IDEwMC45NjUgMTAxNy41NCA5NC45NzY2QzEwMjAuNDQgODguOTQwOCAxMDI0LjU3IDg0LjM1NDUgMTAyOS45NCA4MS4yMTc4QzEwMzUuMzYgNzguMDMzNSAxMDQxLjggNzYuNDQxNCAxMDQ5LjI2IDc2LjQ0MTRDMTA1Ni4zOSA3Ni40NDE0IDEwNjIuNjIgNzguMDMzNSAxMDY3Ljk0IDgxLjIxNzhDMTA3My4zMSA4NC40MDIgMTA3Ny40NyA4OS4wMTIgMTA4MC40MiA5NS4wNDc5QzEwODMuNDEgMTAxLjAzNiAxMDg0LjkxIDEwOC4yODQgMTA4NC45MSAxMTYuNzkxWk0xMDI1LjQ1IDExNi43OTFDMTAyNS40NSAxMjMuMDY0IDEwMjYuMjggMTI4LjUwNiAxMDI3Ljk1IDEzMy4xMTZDMTAyOS42MSAxMzcuNzI2IDEwMzIuMTggMTQxLjI5MSAxMDM1LjY1IDE0My44MUMxMDM5LjExIDE0Ni4zMjggMTA0My41OCAxNDcuNTg4IDEwNDkuMDUgMTQ3LjU4OEMxMDU0LjQ3IDE0Ny41ODggMTA1OC45MSAxNDYuMzI4IDEwNjIuMzggMTQzLjgxQzEwNjUuOSAxNDEuMjkxIDEwNjguNDkgMTM3LjcyNiAxMDcwLjE1IDEzMy4xMTZDMTA3MS44MSAxMjguNTA2IDEwNzIuNjQgMTIzLjA2NCAxMDcyLjY0IDExNi43OTFDMTA3Mi42NCAxMTAuNTY1IDEwNzEuODEgMTA1LjE5NSAxMDcwLjE1IDEwMC42OEMxMDY4LjQ5IDk2LjExNzIgMTA2NS45MiA5Mi42MDAzIDEwNjIuNDUgOTAuMTI4OUMxMDU4Ljk4IDg3LjY1NzYgMTA1NC40OSA4Ni40MjE5IDEwNDguOTggODYuNDIxOUMxMDQwLjg1IDg2LjQyMTkgMTAzNC44OSA4OS4xMDcxIDEwMzEuMDggOTQuNDc3NUMxMDI3LjMzIDk5Ljg0OCAxMDI1LjQ1IDEwNy4yODYgMTAyNS40NSAxMTYuNzkxWk0xMTQyLjM3IDc2LjQ0MTRDMTE1MS42MyA3Ni40NDE0IDExNTguNjQgNzguNzIyNyAxMTYzLjQgODMuMjg1MkMxMTY4LjE1IDg3LjgwMDEgMTE3MC41MiA5NS4wNzE2IDExNzAuNTIgMTA1LjFWMTU2SDExNTguODNWMTA1Ljg4NEMxMTU4LjgzIDk5LjM3MjcgMTE1Ny4zNCA5NC41MDEzIDExNTQuMzQgOTEuMjY5NUMxMTUxLjQgODguMDM3OCAxMTQ2Ljg2IDg2LjQyMTkgMTE0MC43MyA4Ni40MjE5QzExMzIuMDggODYuNDIxOSAxMTI1Ljk5IDg4Ljg2OTUgMTEyMi40OCA5My43NjQ2QzExMTguOTYgOTguNjU5OCAxMTE3LjIgMTA1Ljc2NSAxMTE3LjIgMTE1LjA4VjE1NkgxMTA1LjM3Vjc3Ljg2NzJIMTExNC45MkwxMTE2LjcgODkuMTMwOUgxMTE3LjM0QzExMTkuMDEgODYuMzc0MyAxMTIxLjEyIDg0LjA2OTMgMTEyMy42OSA4Mi4yMTU4QzExMjYuMjUgODAuMzE0OCAxMTI5LjEzIDc4Ljg4OSAxMTMyLjMxIDc3LjkzODVDMTEzNS41IDc2Ljk0MDQgMTEzOC44NSA3Ni40NDE0IDExNDIuMzcgNzYuNDQxNFpNMTIwNi43NCA3Ny44NjcyVjE1NkgxMTk0LjkxVjc3Ljg2NzJIMTIwNi43NFpNMTIwMC45NiA0OC42Mzg3QzEyMDIuOTEgNDguNjM4NyAxMjA0LjU4IDQ5LjI4MDMgMTIwNS45NiA1MC41NjM1QzEyMDcuMzggNTEuNzk5MiAxMjA4LjA5IDUzLjc0NzcgMTIwOC4wOSA1Ni40MDkyQzEyMDguMDkgNTkuMDIzMSAxMjA3LjM4IDYwLjk3MTcgMTIwNS45NiA2Mi4yNTQ5QzEyMDQuNTggNjMuNTM4MSAxMjAyLjkxIDY0LjE3OTcgMTIwMC45NiA2NC4xNzk3QzExOTguOTIgNjQuMTc5NyAxMTk3LjIxIDYzLjUzODEgMTE5NS44MyA2Mi4yNTQ5QzExOTQuNSA2MC45NzE3IDExOTMuODQgNTkuMDIzMSAxMTkzLjg0IDU2LjQwOTJDMTE5My44NCA1My43NDc3IDExOTQuNSA1MS43OTkyIDExOTUuODMgNTAuNTYzNUMxMTk3LjIxIDQ5LjI4MDMgMTE5OC45MiA0OC42Mzg3IDEyMDAuOTYgNDguNjM4N1pNMTI1Ny4wNyAxNDcuNzNDMTI1OS4wMiAxNDcuNzMgMTI2MS4wMSAxNDcuNTY0IDEyNjMuMDYgMTQ3LjIzMUMxMjY1LjEgMTQ2Ljg5OSAxMjY2Ljc2IDE0Ni40OTUgMTI2OC4wNSAxNDYuMDJWMTU1LjIxNkMxMjY2LjY3IDE1NS44MzQgMTI2NC43NCAxNTYuMzU2IDEyNjIuMjcgMTU2Ljc4NEMxMjU5Ljg1IDE1Ny4yMTIgMTI1Ny40NyAxNTcuNDI2IDEyNTUuMTQgMTU3LjQyNkMxMjUxLjAxIDE1Ny40MjYgMTI0Ny4yNiAxNTYuNzEzIDEyNDMuODggMTU1LjI4N0MxMjQwLjUxIDE1My44MTQgMTIzNy44IDE1MS4zNDIgMTIzNS43NSAxNDcuODczQzEyMzMuNzYgMTQ0LjQwNCAxMjMyLjc2IDEzOS42MDQgMTIzMi43NiAxMzMuNDczVjg3LjEzNDhIMTIyMS42NFY4MS4zNjA0TDEyMzIuODMgNzYuNzI2NkwxMjM3LjU0IDU5Ljc1OThIMTI0NC42N1Y3Ny44NjcySDEyNjcuNTVWODcuMTM0OEgxMjQ0LjY3VjEzMy4xMTZDMTI0NC42NyAxMzguMDExIDEyNDUuNzggMTQxLjY3MSAxMjQ4LjAyIDE0NC4wOTVDMTI1MC4zIDE0Ni41MTkgMTI1My4zMSAxNDcuNzMgMTI1Ny4wNyAxNDcuNzNaTTEzNTEuMjQgMTE2Ljc5MUMxMzUxLjI0IDEyMy4yMDcgMTM1MC40MSAxMjguOTM0IDEzNDguNzUgMTMzLjk3MkMxMzQ3LjA4IDEzOS4wMDkgMTM0NC42OCAxNDMuMjYzIDEzNDEuNTUgMTQ2LjczMkMxMzM4LjQxIDE1MC4yMDIgMTMzNC42MSAxNTIuODYzIDEzMzAuMTQgMTU0LjcxN0MxMzI1LjcyIDE1Ni41MjMgMTMyMC43MSAxNTcuNDI2IDEzMTUuMSAxNTcuNDI2QzEzMDkuODcgMTU3LjQyNiAxMzA1LjA3IDE1Ni41MjMgMTMwMC43IDE1NC43MTdDMTI5Ni4zNyAxNTIuODYzIDEyOTIuNjIgMTUwLjIwMiAxMjg5LjQzIDE0Ni43MzJDMTI4Ni4zIDE0My4yNjMgMTI4My44NSAxMzkuMDA5IDEyODIuMDkgMTMzLjk3MkMxMjgwLjM4IDEyOC45MzQgMTI3OS41MyAxMjMuMjA3IDEyNzkuNTMgMTE2Ljc5MUMxMjc5LjUzIDEwOC4yMzYgMTI4MC45NyAxMDAuOTY1IDEyODMuODcgOTQuOTc2NkMxMjg2Ljc3IDg4Ljk0MDggMTI5MC45MSA4NC4zNTQ1IDEyOTYuMjggODEuMjE3OEMxMzAxLjcgNzguMDMzNSAxMzA4LjE0IDc2LjQ0MTQgMTMxNS42IDc2LjQ0MTRDMTMyMi43MyA3Ni40NDE0IDEzMjguOTUgNzguMDMzNSAxMzM0LjI4IDgxLjIxNzhDMTMzOS42NSA4NC40MDIgMTM0My44IDg5LjAxMiAxMzQ2Ljc1IDk1LjA0NzlDMTM0OS43NSAxMDEuMDM2IDEzNTEuMjQgMTA4LjI4NCAxMzUxLjI0IDExNi43OTFaTTEyOTEuNzkgMTE2Ljc5MUMxMjkxLjc5IDEyMy4wNjQgMTI5Mi42MiAxMjguNTA2IDEyOTQuMjggMTMzLjExNkMxMjk1Ljk1IDEzNy43MjYgMTI5OC41MSAxNDEuMjkxIDEzMDEuOTggMTQzLjgxQzEzMDUuNDUgMTQ2LjMyOCAxMzA5LjkyIDE0Ny41ODggMTMxNS4zOCAxNDcuNTg4QzEzMjAuOCAxNDcuNTg4IDEzMjUuMjUgMTQ2LjMyOCAxMzI4LjcxIDE0My44MUMxMzMyLjIzIDE0MS4yOTEgMTMzNC44MiAxMzcuNzI2IDEzMzYuNDkgMTMzLjExNkMxMzM4LjE1IDEyOC41MDYgMTMzOC45OCAxMjMuMDY0IDEzMzguOTggMTE2Ljc5MUMxMzM4Ljk4IDExMC41NjUgMTMzOC4xNSAxMDUuMTk1IDEzMzYuNDkgMTAwLjY4QzEzMzQuODIgOTYuMTE3MiAxMzMyLjI2IDkyLjYwMDMgMTMyOC43OSA5MC4xMjg5QzEzMjUuMzIgODcuNjU3NiAxMzIwLjgzIDg2LjQyMTkgMTMxNS4zMSA4Ni40MjE5QzEzMDcuMTkgODYuNDIxOSAxMzAxLjIyIDg5LjEwNzEgMTI5Ny40MiA5NC40Nzc1QzEyOTMuNjYgOTkuODQ4IDEyOTEuNzkgMTA3LjI4NiAxMjkxLjc5IDExNi43OTFaTTE0MDcuMiA3Ni40NDE0QzE0MDguNzcgNzYuNDQxNCAxNDEwLjQxIDc2LjUzNjUgMTQxMi4xMiA3Ni43MjY2QzE0MTMuODMgNzYuODY5MSAxNDE1LjM1IDc3LjA4MyAxNDE2LjY5IDc3LjM2ODJMMTQxNS4xOSA4OC4zNDY3QzE0MTMuOTEgODguMDE0IDE0MTIuNDggODcuNzUyNiAxNDEwLjkxIDg3LjU2MjVDMTQwOS4zNCA4Ny4zNzI0IDE0MDcuODcgODcuMjc3MyAxNDA2LjQ5IDg3LjI3NzNDMTQwMy4zNSA4Ny4yNzczIDE0MDAuMzggODcuOTE4OSAxMzk3LjU4IDg5LjIwMjFDMTM5NC44MiA5MC40Mzc4IDEzOTIuNCA5Mi4yNDM4IDEzOTAuMzEgOTQuNjIwMUMxMzg4LjIyIDk2Ljk0ODkgMTM4Ni41OCA5OS43NzY3IDEzODUuMzkgMTAzLjEwNEMxMzg0LjIgMTA2LjM4MyAxMzgzLjYxIDExMC4wNDIgMTM4My42MSAxMTQuMDgyVjE1NkgxMzcxLjdWNzcuODY3MkgxMzgxLjU0TDEzODIuODIgOTIuMjY3NkgxMzgzLjMyQzEzODQuOTQgODkuMzY4NSAxMzg2Ljg5IDg2LjczMDggMTM4OS4xNyA4NC4zNTQ1QzEzOTEuNDUgODEuOTMwNyAxMzk0LjA5IDgwLjAwNTkgMTM5Ny4wOCA3OC41ODAxQzE0MDAuMTIgNzcuMTU0MyAxNDAzLjUgNzYuNDQxNCAxNDA3LjIgNzYuNDQxNFoiIGZpbGw9IiMzQjg2NDYiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yMzQuNTQ3IDQ5LjQzNzhDMjE1LjA4MiA2Mi4wNDM3IDE5OC4yNDYgNzkuNDE3NiAxODIuMzEyIDk2LjU5MjNDMTczLjEwNyA4Ni42NzA4IDE2My42MDEgNzYuNjgyOCAxNTMuNDYxIDY3LjU4NTlMMTQ1LjUwNyA3NS4xNzc4QzE1NS4zNTUgODMuOTYyMSAxNjQuODI4IDkzLjg1MTIgMTc0LjMxNiAxMDQuMDY0TDE4Mi4zMjYgMTEyLjUzM0wxOTUuMTE0IDk4LjgzN0wxOTUuMTU2IDk4Ljg3ODJDMjA5LjczOCA4My4zOTU2IDIyNC40NDEgNjguOTkxMyAyNDAuNjgyIDU4LjQ5NDJMMjM0LjU0NyA0OS40Mzc4Wk0yNjAuMTg2IDQ4LjI3NThDMjcwLjg0MyA0NC4wMDA4IDI4Mi4yNjEgNDEuNTUwOSAyOTQuNzUyIDQxLjU1MDlDMzE0LjYxIDQxLjU1MDkgMzI5LjE2NyA0OS40MTIzIDMzOC44NTMgNjEuMDEzM0MzNDguNjQyIDcyLjczOTIgMzUzLjY4MiA4OC41ODE3IDM1My42ODIgMTA0LjY0N0MzNTMuNjgyIDEyMC43MTIgMzQ4LjY0MiAxMzYuNTU0IDMzOC44NTMgMTQ4LjI4QzMyOS4xNjcgMTU5Ljg4MSAzMTQuNjEgMTY3Ljc0MyAyOTQuNzUyIDE2Ny43NDNDMjU4LjIwOCAxNjcuNzQzIDIzMC44NDggMTQ2Ljc3MyAyMDQuODQyIDEyMC40NzhMMTk3LjM1NSAxMjguNDk3QzIyMy44NjUgMTU1LjI3OCAyNTQuMDA5IDE3OC42ODQgMjk0Ljc1MiAxNzguNjg0QzMxNy44MjcgMTc4LjY4NCAzMzUuNDcxIDE2OS40MDQgMzQ3LjI1MiAxNTUuMjkyQzM1OC45MjkgMTQxLjMwNiAzNjQuNjIzIDEyMi44NjUgMzY0LjYyMyAxMDQuNjQ3QzM2NC42MjMgODYuNDI4NSAzNTguOTI5IDY3Ljk4NzcgMzQ3LjI1MiA1NC4wMDExQzMzNS40NzEgMzkuODg5NiAzMTcuODI3IDMwLjYwOTQgMjk0Ljc1MiAzMC42MDk0QzI3OS44MzcgMzAuNjA5NCAyNjYuMzQzIDMzLjc0NTkgMjUzLjkyNyAzOS4wMzYzTDI2MC4xODYgNDguMjc1OFpNMTY3LjA5IDEyOC42NzdDMTQwLjYyMyAxNTUuMzgzIDExMC41MjMgMTc4LjY4NCA2OS44NzA5IDE3OC42ODRDNDYuNzk1NyAxNzguNjg0IDI5LjE1MjUgMTY5LjQwNCAxNy4zNzEyIDE1NS4yOTJDNS42OTQxNSAxNDEuMzA2IDAgMTIyLjg2NSAwIDEwNC42NDdDMCA4Ni40Mjg1IDUuNjk0MTUgNjcuOTg3NyAxNy4zNzEyIDU0LjAwMTFDMjkuMTUyNSAzOS44ODk2IDQ2Ljc5NTcgMzAuNjA5NCA2OS44NzA5IDMwLjYwOTRDOTUuNjg0NyAzMC42MDk0IDExNy4yNDMgNDAuMDA0NiAxMzYuMzE5IDUzLjcwMDlMMTI4LjI1NSA2MS4zOTdDMTExLjAyNiA0OS4zNDg4IDkyLjE1NTcgNDEuNTUwOSA2OS44NzA5IDQxLjU1MDlDNTAuMDEyNyA0MS41NTA5IDM1LjQ1NTcgNDkuNDEyMyAyNS43NzAzIDYxLjAxMzNDMTUuOTgwNyA3Mi43MzkyIDEwLjk0MTUgODguNTgxNyAxMC45NDE1IDEwNC42NDdDMTAuOTQxNSAxMjAuNzEyIDE1Ljk4MDcgMTM2LjU1NCAyNS43NzAzIDE0OC4yOEMzNS40NTU3IDE1OS44ODEgNTAuMDEyNyAxNjcuNzQzIDY5Ljg3MDkgMTY3Ljc0M0MxMDYuMzA4IDE2Ny43NDMgMTMzLjYxNSAxNDYuODk1IDE1OS41NTMgMTIwLjcwOUwxNjcuMDkgMTI4LjY3N1oiIGZpbGw9IiMzQjg2NDYiLz4KPC9zdmc+Cg0K")
no-repeat center;
background-size: contain;
}
.pf-v5-c-login__header::before,
.login-pf-header::before {
content: none !important;
background: none !important;
}
#kc-page-title,
h1#kc-page-title {
font-family: inherit !important;
font-size: 20px !important;
font-weight: 400 !important;
color: var(--sm-text) !important;
text-align: center !important;
margin: 0 0 16px 0 !important;
padding: 0 !important;
line-height: 1.3 !important;
}
.login-pf-page .card-pf,
#kc-form-wrapper,
#kc-form-login,
#kc-otp-login-form,
form#kc-form-login,
.pf-v5-c-login__main-body form,
.pf-v5-c-login__main-body > div {
width: 100% !important;
max-width: 350px !important;
margin-left: auto !important;
margin-right: auto !important;
}
.pf-v5-c-form-control,
.pf-v5-c-input-group,
.pf-v5-c-input-group__item {
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0 !important;
width: 100% !important;
}
.pf-v5-c-form-control::before,
.pf-v5-c-form-control::after {
display: none !important;
border: none !important;
content: none !important;
}
input#username,
input#password,
input#otp,
input[type="text"],
input[type="password"] {
display: block !important;
width: 100% !important;
height: 40px !important;
background-color: #FFFFFF !important;
border: 1px solid var(--sm-border) !important;
border-radius: 4px !important;
box-shadow: none !important;
outline: none !important;
font-family: inherit !important;
font-size: 14px !important;
color: var(--sm-text) !important;
padding: 0 12px 0 42px !important;
background-repeat: no-repeat !important;
background-position: 13px center !important;
background-size: 17px 17px !important;
}
input#username {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238C8C8C' stroke-width='1.8'%3E%3Ccircle cx='12' cy='8' r='3.5'/%3E%3Cpath d='M4.5 20c0-4 3.4-6.5 7.5-6.5s7.5 2.5 7.5 6.5'/%3E%3C/svg%3E") !important;
}
input#password,
input#otp {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238C8C8C' stroke-width='1.8'%3E%3Crect x='5' y='10.5' width='14' height='9.5' rx='1.5'/%3E%3Cpath d='M8 10.5V7.5a4 4 0 0 1 8 0v3'/%3E%3C/svg%3E") !important;
}
input:focus {
border-color: var(--sm-green) !important;
box-shadow: 0 0 0 1px var(--sm-green) !important;
}
input::placeholder { color: #9AA0A6 !important; }
.pf-v5-c-input-group,
#kc-form-login .pf-v5-c-input-group {
position: relative !important;
display: block !important;
width: 100% !important;
}
.pf-v5-c-input-group__item {
display: block !important;
width: 100% !important;
}
.pf-v5-c-input-group__item:has(button),
.pf-v5-c-input-group__item.pf-m-plain {
position: absolute !important;
right: 4px !important;
top: 50% !important;
transform: translateY(-50%) !important;
width: auto !important;
}
.pf-v5-c-button.pf-m-control,
button[aria-label*="assword"],
button[aria-label*="ароль"],
#kc-form-login .pf-v5-c-input-group button {
position: absolute !important;
right: 4px !important;
top: 50% !important;
transform: translateY(-50%) !important;
margin: 0 !important;
padding: 0 8px !important;
height: 32px !important;
background: transparent !important;
border: none !important;
box-shadow: none !important;
color: #8C8C8C !important;
}
.pf-v5-c-form__label,
.pf-v5-c-form__label-text,
label[for="username"],
label[for="password"],
label[for="otp"] {
display: block !important;
font-family: inherit !important;
font-size: 13px !important;
font-weight: 400 !important;
line-height: 1.2 !important;
color: #5F6368 !important;
margin: 0 0 2px 2px !important;
padding: 0 !important;
text-align: left !important;
}
.pf-v5-c-form__group-label,
.pf-v5-c-form__group-label-main,
.pf-v5-c-form__group-main {
padding-bottom: 0 !important;
margin-bottom: 0 !important;
}
.pf-v5-c-form__group-control {
margin-top: 0 !important;
padding-top: 0 !important;
}
.pf-v5-c-form,
#kc-form-login,
#kc-otp-login-form {
--pf-v5-c-form--GridGap: 10px !important;
row-gap: 10px !important;
gap: 10px !important;
}
.pf-v5-c-form__group,
#kc-form-login .form-group {
--pf-v5-c-form__group--GridGap: 2px !important;
row-gap: 2px !important;
gap: 2px !important;
margin-bottom: 0 !important;
}
#kc-login,
input[type="submit"],
.pf-v5-c-button.pf-m-primary {
width: 100% !important;
height: 40px !important;
background-color: var(--sm-green) !important;
border: 1px solid var(--sm-green) !important;
border-radius: 4px !important;
color: #FFFFFF !important;
font-family: inherit !important;
font-size: 15px !important;
font-weight: 600 !important;
box-shadow: none !important;
margin-top: 4px !important;
cursor: pointer;
}
#kc-login:hover,
.pf-v5-c-button.pf-m-primary:hover {
background-color: var(--sm-green-dark) !important;
border-color: var(--sm-green-dark) !important;
}
[id*="locale" i],
[class*="locale" i],
#kc-locale,
#kc-locale-wrapper,
#kc-locale-dropdown,
select#locale,
.pf-v5-c-login__main-header .pf-v5-c-dropdown,
.pf-v5-c-login__main-header .pf-v5-c-menu-toggle,
.pf-v5-c-login__main-header-utilities,
#kc-form-options,
#kc-registration,
#kc-registration-container,
#kc-info,
#kc-info-wrapper,
.checkbox,
.pf-v5-c-login__main-footer-band,
#kc-social-providers {
display: none !important;
}
.pf-v5-c-login__main-header,
#kc-page-title {
display: block !important;
width: 100% !important;
}
.alert-error,
.pf-v5-c-alert.pf-m-danger,
#input-error,
.pf-v5-c-form__helper-text.pf-m-error {
font-family: inherit !important;
font-size: 13px !important;
color: #B3261E !important;
}
@media (max-width: 480px) {
#kc-header-wrapper::before {
width: 260px;
height: 40px;
margin-top: 28px;
}
}
/opt/keycloak/themes/smartmonitor/login/messages/messages_en.properties
loginTitle=Smart Monitor
loginAccountTitle=Sign in via 2FA
username=Username
password=Password
doLogIn=Sign in
loginOtpOneTime=One-time code
doSubmit=Confirm
/opt/keycloak/themes/smartmonitor/login/messages/messages_ru.properties
loginTitle=Smart Monitor
loginAccountTitle=Вход через 2FA
username=Имя пользователя
password=Пароль
doLogIn=Войти
loginOtpOneTime=Одноразовый код
doSubmit=Подтвердить
/opt/keycloak/themes/smartmonitor/login/theme.properties
parent=keycloak.v2
styles=css/styles.css css/smartmonitor.css
locales=en,ru
Назначьте права на каталог темы:
sudo chown -R keycloak:keycloak /opt/keycloak/themes/smartmonitor
sudo chmod -R a+rX /opt/keycloak/themes/smartmonitor
Чтобы подключить тему, перейдите в Realm settings - Themes и задайте:
- параметр
Login theme—smartmonitor
Перезапустите Keycloak:
systemctl restart keycloak
Федерация с Active Directory
Сначала перенесите сертификат домена, иначе Test connection не пройдет:
sudo cp $OS_HOME/config/opensearch-security/ldaps-ca-cert.pem /opt/keycloak/conf/
sudo chown keycloak:keycloak /opt/keycloak/conf/ldaps-ca-cert.pem
sudo chmod 644 /opt/keycloak/conf/ldaps-ca-cert.pem
В keycloak.conf:
truststore-paths=/opt/keycloak/conf/ldaps-ca-cert.pem
После этого перезапустите сервис keycloak:
sudo systemctl restart keycloak
Значения полей берутся из действующей конфигурации Smart Monitor Data Storage:
sed -n '/^ ldap:/,/^ authz:/p' $OS_HOME/config/opensearch-security/config.yml
При настройке User Federation используйте кнопки Test connection и Test authentication для проверки соединения с контроллером домена AD и аутентификации соответственно.
Перейдите User Federation - Add LDAP provider и задайте значения:
| Поле | Значение | Источник |
|---|---|---|
Vendor | Active Directory | - |
Connection URL | ldaps://<LDAP_DOMAIN>:636 | hosts + enable_ssl |
Enable StartTLS | Off | enable_start_tls |
Connection pooling | On | - |
Connection timeout | 30000 | connect_timeout |
Bind type | simple | - |
Bind DN | из bind_dn | - |
Bind credentials | из password | - |
Edit mode | READ_ONLY | только чтение из AD |
Sync Registrations | Off | противоречит READ_ONLY |
Users DN | из userbase | - |
Username LDAP attribute | sAMAccountName | username_attribute |
RDN LDAP attribute | cn | стандарт AD |
UUID LDAP attribute | objectGUID | стандарт AD |
User object classes | person, organizationalPerson, user | стандарт AD |
Search scope | Subtree | - |
Пользователь из федерации создается в Keycloak в момент первого обращения к нему, поэтому в общем списке на вкладке Users его нет, пока не выполнен поиск по имени.
Второй фактор - OTP
Перейдите Authentication - Required actions - Configure OTP и установите значение Enabled.
Требования ко второму фактору задаются потоком Authentication - Flows - browser:
| Шаг потока | Значение |
|---|---|
Browser - Conditional 2FA | Required |
OTP Form | Required |
Измените оба параметра. Если подпоток имеет значение Required, а OTP Form — Alternative, для пользователя без привязки OTP этот шаг будет пропущен вместо предложения настроить аутентификатор.
Время жизни сессии
Отдельного таймера для второго фактора нет. Код запрашивается только при новой аутентификации; момент повторной аутентификации определяется сессией SSO.
Настройки находятся на двух вкладках: Realm settings - Sessions задает параметры сессии, а Realm settings - Tokens — срок жизни токена.
| Поле в консоли | Поле в API | По умолчанию | За что отвечает |
|---|---|---|---|
SSO Session Idle | ssoSessionIdleTimeout | 1800 (30 мин) | простой без активности |
SSO Session Max | ssoSessionMaxLifespan | 36000 (10 ч) | предельный срок независимо от активности |
Client Session Idle | clientSessionIdleTimeout | 0 | 0 значит «как у SSO» |
Client Session Max | clientSessionMaxLifespan | 0 | то же |
Access Token Lifespan | accessTokenLifespan | 300 (5 мин) | срок жизни токена |
Client Login Timeout | accessCodeLifespan | 60 (1 мин) | срок действия кода авторизации до обмена |
Login timeout | accessCodeLifespanLogin | 1800 (30 мин) | на весь процесс входа |
Login action timeout | accessCodeLifespanUserAction | 300 (5 мин) | на каждый шаг, включая привязку OTP |
Единицы измерения
В консоли рядом с полем можно выбрать единицу времени: секунды, минуты, часы или дни. В API и базе данных значения хранятся в секундах, а в конфигурации Smart Monitor Web — в миллисекундах:
smart_monitor.cookie.ttl: 3600000 # 1 час
smart_monitor.session.ttl: 3600000
smart_monitor.session.keepalive: true
Фактические значения realm:
sudo -u keycloak JAVA_HOME=/opt/jdk-21 /opt/keycloak/bin/kcadm.sh get realms/<REALM_ID> \
--fields ssoSessionIdleTimeout,ssoSessionMaxLifespan,accessTokenLifespan,accessCodeLifespanUserAction
Время до повторного запроса второго фактора ограничивают SSO Session Idle при простое и SSO Session Max независимо от активности. Действует минимальный из трех сроков: срок токена, сессии SSO и сессии Smart Monitor Web. При keepalive: true последняя продлевается при активности, поэтому практическим ограничением обычно остается SSO Session Idle.
Текущие сессии отображаются в разделе Sessions и на вкладке Users - пользователь - Sessions в полях Started и Last access. Содержимое токена доступно в Clients - клиент - Client scopes - Evaluate - Generated ID Token, в полях iat и exp.
Далее: Настройка клиента Keycloak.