Перейти к основному содержимому
Версия: 6.2

Настройка провайдера Keycloak

Это первый из трех этапов настройки. Общий порядок приведен в статье Настройка двухфакторной аутентификации по протоколу OIDC.

Условные обозначения​

Обратите внимание!

Пути с $OS_HOME указывают не только на файлы, но и на сервер, где они находятся. Если сервисы размещены на разных серверах, заранее перенесите необходимые файлы между ними.

Базовый адрес Keycloak зависит от схемы публикации:

ТокенОписаниеПрямая публикацияЧерез прокси
<KEYCLOAK_BASE>базовый адрес Keycloakhttps://<KEYCLOAK_HOST>:8443https://<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). Доступны два варианта:

  1. CA кластера — ca-cert.pem содержит сертификат приватного CA, не привязанного к Smart Monitor Data Storage или конкретному серверу. Этот CA можно использовать для подписи сертификата Keycloak. В этом случае <KEYCLOAK_CA> — ca-cert.pem: файл уже находится в каталоге конфигурации Smart Monitor Data Storage; при необходимости скопируйте его в каталог Smart Monitor Web

  2. Отдельный 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 и задайте значения:

ПолеЗначениеИсточник
VendorActive Directory-
Connection URLldaps://<LDAP_DOMAIN>:636hosts + enable_ssl
Enable StartTLSOffenable_start_tls
Connection poolingOn-
Connection timeout30000connect_timeout
Bind typesimple-
Bind DNиз bind_dn-
Bind credentialsиз password-
Edit modeREAD_ONLYтолько чтение из AD
Sync RegistrationsOffпротиворечит READ_ONLY
Users DNиз userbase-
Username LDAP attributesAMAccountNameusername_attribute
RDN LDAP attributecnстандарт AD
UUID LDAP attributeobjectGUIDстандарт AD
User object classesperson, organizationalPerson, userстандарт AD
Search scopeSubtree-

Пользователь из федерации создается в Keycloak в момент первого обращения к нему, поэтому в общем списке на вкладке Users его нет, пока не выполнен поиск по имени.

Второй фактор - OTP​

Перейдите Authentication - Required actions - Configure OTP и установите значение Enabled.

Требования ко второму фактору задаются потоком Authentication - Flows - browser:

Шаг потокаЗначение
Browser - Conditional 2FARequired
OTP FormRequired

Измените оба параметра. Если подпоток имеет значение Required, а OTP Form — Alternative, для пользователя без привязки OTP этот шаг будет пропущен вместо предложения настроить аутентификатор.

Время жизни сессии​

Отдельного таймера для второго фактора нет. Код запрашивается только при новой аутентификации; момент повторной аутентификации определяется сессией SSO.

Настройки находятся на двух вкладках: Realm settings - Sessions задает параметры сессии, а Realm settings - Tokens — срок жизни токена.

Поле в консолиПоле в APIПо умолчаниюЗа что отвечает
SSO Session IdlessoSessionIdleTimeout1800 (30 мин)простой без активности
SSO Session MaxssoSessionMaxLifespan36000 (10 ч)предельный срок независимо от активности
Client Session IdleclientSessionIdleTimeout00 значит «как у SSO»
Client Session MaxclientSessionMaxLifespan0то же
Access Token LifespanaccessTokenLifespan300 (5 мин)срок жизни токена
Client Login TimeoutaccessCodeLifespan60 (1 мин)срок действия кода авторизации до обмена
Login timeoutaccessCodeLifespanLogin1800 (30 мин)на весь процесс входа
Login action timeoutaccessCodeLifespanUserAction300 (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.