Setting Up the Keycloak Provider
This is the first of three setup stages. The overall sequence is described in Setting Up Two-Factor Authentication over the OIDC Protocol.
Notation
Paths containing $OS_HOME refer not only to files but also to the server where they reside. If the services run on different servers, transfer the required files between them in advance.
The Keycloak base address depends on the publishing scheme:
| Token | Description | Direct publishing | Behind a proxy |
|---|---|---|---|
<KEYCLOAK_BASE> | Keycloak base address | https://<KEYCLOAK_HOST>:8443 | https://<KEYCLOAK_HOST>/keycloak |
The remaining tokens do not depend on the publishing scheme:
| Token | Description |
|---|---|
<KEYCLOAK_HOST> | external hostname of the Keycloak machine |
<KEYCLOAK_CA> | name of the CA file that signed the certificate of the side terminating the connection: the proxy if there is one, otherwise Keycloak itself. A copy of the file is placed in the configuration directory of each service separately |
<REALM_ID> | technical realm name |
<REALM_NAME> | human-readable realm name (Display name) |
<LDAP_DOMAIN> | AD domain controller |
<POSTGRESQL_ADDRESS> | PostgreSQL server address |
<POSTGRESQL_PORT> | PostgreSQL port |
$DB_PASS | password of the keycloak database user |
$OS_HOME | SA Data Storage home directory |
Preparing Keycloak
User
Create the keycloak user first; it is needed for the Java check.
sudo useradd -r -d /opt/keycloak -s /sbin/nologin keycloak
Java
Keycloak 26.7 runs on OpenJDK 17, 21, or 25. Search Anywhere Framework ships with Java 21, so copying the bundled one is recommended:
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
The system Java is suitable if the repositories are available or Keycloak is deployed on a separate server:
sudo apt install -y openjdk-21-jre-headless # Debian/Ubuntu
sudo dnf install -y java-21-openjdk-headless # RHEL/Rocky
Distribution
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
The directory is renamed so that paths in the service unit file and this guide do not depend on the Keycloak version.
Database
This guide uses PostgreSQL. Any database from the list of supported ones is acceptable. The embedded H2 database should not be used in production: it is not on the list, and schema migrations during upgrades are not guaranteed on it. Tables do not need to be created, Liquibase sets up the schema itself.
CREATE USER keycloak WITH PASSWORD '$DB_PASS';
CREATE DATABASE keycloak OWNER keycloak ENCODING 'UTF8';
For PostgreSQL 15 and newer, additionally:
\c keycloak
GRANT ALL ON SCHEMA public TO keycloak;
Certificate for Keycloak
When a proxy is used, this step is skipped entirely: the connection from the browser is terminated by the proxy with its own certificate, while Keycloak listens on plain HTTP on the loopback address.
Choosing a CA
The Keycloak certificate is signed by a certificate authority (CA). There are two options:
-
Cluster CA —
ca-cert.pemcontains the certificate of a private CA that is not tied to SA Data Storage or a particular server. This CA can sign the Keycloak certificate. In this case,<KEYCLOAK_CA>isca-cert.pem: the file is already in the SA Data Storage configuration directory; copy it to the SA Web directory if needed -
Separate CA — a corporate PKI, an internal ACME, or a self-signed CA created for Keycloak. In this case,
<KEYCLOAK_CA>is the separate CA file; a copy is required in both the SA Data Storage and SA Web configuration directories
Issuing on a shared host
If Keycloak runs on the same server as SA Data Storage, the CA key is available locally and all steps can be performed on that server.
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
Issuing on a separate server
If Keycloak runs on a separate server, the CA key is not available there. The Keycloak private key stays on its server, and the CA key stays on the CA server. Only the request and the issued certificate are transferred between servers.
On the Keycloak host, create the key and the request:
cd /opt/keycloak/conf
sudo openssl req -new -nodes -newkey rsa:2048 \
-keyout tls.key -out tls.csr \
-subj "/CN=<KEYCLOAK_HOST>"
Copy the tls.csr file to the machine where the CA key resides.
On the CA host, sign the request:
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
Transfer only tls.crt to the Keycloak server. If the CA is an intermediate CA, append the chain up to the root certificate to tls.crt.
sudo chown keycloak:keycloak tls.key tls.crt
sudo chmod 640 tls.key
sudo chmod 644 tls.crt
Configuration
File: /opt/keycloak/conf/keycloak.conf
Direct publishing
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
Port 8443 is the default value for HTTPS and does not need to be set separately; it is changed with the https-port parameter. Keycloak occupies its port entirely, so it needs neither a path prefix nor the acceptance of X-Forwarded-* headers.
Behind a proxy
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 is required only if Keycloak is published under a subpath rather than at the root of its own hostname. If it has its own hostname, the parameter is not set, and <KEYCLOAK_BASE> looks like https://<KEYCLOAK_HOST> without a prefix.
If Keycloak shares a hostname with SA Web, the /auth prefix will not work: on that host the /auth/openid/login and /auth/openid/logout paths are already taken by SA Web.
http-host=127.0.0.1 blocks direct access to Keycloak bypassing the proxy.
The file contains the database password, so restrict access to it:
sudo chown keycloak:keycloak /opt/keycloak/conf/keycloak.conf
sudo chmod 600 /opt/keycloak/conf/keycloak.conf
Proxy in front of Keycloak
The step is performed only in the scheme with a proxy. With direct publishing it is skipped entirely.
Example nginx configuration on the Keycloak server:
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
Startup
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
The administrator is created while the service is stopped. The command is interactive: it prompts for a username and a password.
sudo -u keycloak JAVA_HOME=/opt/jdk-21 /opt/keycloak/bin/kc.sh bootstrap-admin user
After the administrator is created, enable and start the service:
sudo systemctl enable --now keycloak
Check all three addresses, not just issuer:
curl -sS --cacert $OS_HOME/config/<KEYCLOAK_CA> \
<KEYCLOAK_BASE>/realms/master/.well-known/openid-configuration \
| jq '.issuer, .authorization_endpoint, .token_endpoint'
All three addresses must contain the full <KEYCLOAK_BASE>. Run the request without -k.
Setting up the realm
Creating a realm
OTP secrets are bound to a realm: when a new realm is created, users must bind their authenticator again. Create a separate realm only for a new cluster.
Go to Create realm - Realm name and enter <REALM_ID>.
Before proceeding, make sure the console has switched to the new realm: in Manage realms, the Current realm label is shown to the right of <REALM_ID>.
Set the following in Realm settings - General:
- setting
Display name—<REALM_NAME> - setting
Require SSL—External requests
Display name appears in the sign-in page title and in the entry name in the authenticator app. If it is empty, the technical <REALM_ID> ends up there, and entries from different systems may be hard to tell apart. The Require SSL value does not need to be changed: HTTP is allowed only from private addresses.
Set the following in Realm settings - Login:
- setting
Verify email—Off
Otherwise, users without an email address will not be able to sign in.
Login Page Localization
By default, the Keycloak login page is displayed in English. To configure localization, go to Realm Settings - Localization - Locales and set:
- setting
Internationalization—Enabled - setting
Supported locales— addRussian - setting
Default locale—Russian
Restart Keycloak:
systemctl restart keycloak
Creating the Search Anywhere Framework Theme
Create the theme directories and files:
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}
Add the following content to the corresponding files:
/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
Set permissions on the theme directory:
sudo chown -R keycloak:keycloak /opt/keycloak/themes/smartmonitor
sudo chmod -R a+rX /opt/keycloak/themes/smartmonitor
To enable the theme, go to Realm settings - Themes and set:
- setting
Login theme—smartmonitor
Restart Keycloak:
systemctl restart keycloak
Active Directory federation
Transfer the domain certificate first, otherwise Test connection will fail:
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
In keycloak.conf:
truststore-paths=/opt/keycloak/conf/ldaps-ca-cert.pem
After that, restart the keycloak service:
sudo systemctl restart keycloak
The field values are taken from the current SA Data Storage configuration:
sed -n '/^ ldap:/,/^ authz:/p' $OS_HOME/config/opensearch-security/config.yml
When configuring User Federation, use Test connection to check the connection to the AD domain controller and Test authentication to check authentication.
Go to User Federation - Add LDAP provider and set the values:
| Field | Value | Source |
|---|---|---|
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 | from bind_dn | - |
Bind credentials | from password | - |
Edit mode | READ_ONLY | read-only access to AD |
Sync Registrations | Off | contradicts READ_ONLY |
Users DN | from userbase | - |
Username LDAP attribute | sAMAccountName | username_attribute |
RDN LDAP attribute | cn | AD standard |
UUID LDAP attribute | objectGUID | AD standard |
User object classes | person, organizationalPerson, user | AD standard |
Search scope | Subtree | - |
A federated user is created in Keycloak the moment they are first accessed, so they do not appear in the general list on the Users tab until a search by name is performed.
Second factor - OTP
Go to Authentication - Required actions - Configure OTP and set the value to Enabled.
The second-factor requirements are defined by the Authentication - Flows - browser flow:
| Flow step | Value |
|---|---|
Browser - Conditional 2FA | Required |
OTP Form | Required |
Change both settings. If the subflow is Required but OTP Form is Alternative, Keycloak skips this step for a user without OTP configured instead of prompting them to set up an authenticator.
Session lifetime
The second factor has no separate timer. The code is requested only during a new authentication; the SSO session determines when reauthentication is required.
The settings are on two tabs: Realm settings - Sessions controls session settings, and Realm settings - Tokens controls token lifetime.
| Field in the console | Field in the API | Default | Purpose |
|---|---|---|---|
SSO Session Idle | ssoSessionIdleTimeout | 1800 (30 min) | idle time without activity |
SSO Session Max | ssoSessionMaxLifespan | 36000 (10 h) | maximum lifetime regardless of activity |
Client Session Idle | clientSessionIdleTimeout | 0 | 0 means "same as SSO" |
Client Session Max | clientSessionMaxLifespan | 0 | the same |
Access Token Lifespan | accessTokenLifespan | 300 (5 min) | token lifetime |
Client Login Timeout | accessCodeLifespan | 60 (1 min) | lifetime of the authorization code before the exchange |
Login timeout | accessCodeLifespanLogin | 1800 (30 min) | for the entire sign-in process |
Login action timeout | accessCodeLifespanUserAction | 300 (5 min) | for each step, including OTP binding |
Units of measurement
In the console, select a time unit next to the field: seconds, minutes, hours, or days. The API and database store values in seconds, while the SA Web configuration uses milliseconds:
smart_monitor.cookie.ttl: 3600000 # 1 hour
smart_monitor.session.ttl: 3600000
smart_monitor.session.keepalive: true
The actual realm values:
sudo -u keycloak JAVA_HOME=/opt/jdk-21 /opt/keycloak/bin/kcadm.sh get realms/<REALM_ID> \
--fields ssoSessionIdleTimeout,ssoSessionMaxLifespan,accessTokenLifespan,accessCodeLifespanUserAction
The time before the second factor is requested again is limited by SSO Session Idle during inactivity and by SSO Session Max regardless of activity. The shortest of three lifetimes applies: the token, the SSO session, and the SA Web session. With keepalive: true, activity extends the latter, so SSO Session Idle is usually the practical limit.
Current sessions appear in the Sessions section and under Users - user - Sessions, in the Started and Last access fields. To view token contents, go to Clients - client - Client scopes - Evaluate - Generated ID Token and check the iat and exp fields.