Home Linux & Systems Cybersecurity Cloud & DevOps Networks & Infrastructure SIEM & Monitoring DFIR & Threat Intel Development & Other All categories Projects About Tools

Keycloak in High Availability with Patroni, Etcd and Nginx

Leer en espanol
Keycloak in High Availability with Patroni, Etcd and Nginx

Table of contents

Keycloak is an open source identity and access management (IAM) platform developed by Red Hat. Provides centralized authentication and authorization using protocols ===

Introduction

Keycloak is an open source identity and access management (IAM) platform developed by Red Hat. Provides centralized authentication and authorization using standard protocols such as OpenID Connect, OAuth 2.0 and SAML 2.0, allowing you to implement Single Sign-On (SSO) throughout the organization without having to modify the applications.

In production environments, identity is a critical service: if Keycloak goes down, users cannot authenticate to any application. Therefore, deploy Keycloak in high availability (HA) It is essential to guarantee business continuity.

In this guide we will deploy a complete HA architecture with the following components:

  • 2 Keycloak nodes in active-active cluster with Infinispan for session replication.
  • 3 PostgreSQL nodes managed by Patroni and Etcd for high availability of the database with automatic failover.
  • 2 Nginx nodes as load balancers with Keepalived for a VIP (Virtual IP) with automatic failover.

The architecture diagram is as follows:

text
           Clientes
               |
    [VIP: 192.168.1.220]
       /             \
  [lb1: .212]    [lb2: .213]    <-- Nginx + Keepalived
       \             /
    (upstream HTTP/8080)
       /             \
 [kc1: .210]    [kc2: .211]    <-- Keycloak (clúster Infinispan)
       \             /
     (JDBC connection)
       /      |      \
 [db1:.214] [db2:.215] [db3:.216]  <-- PostgreSQL + Patroni + Etcd

Prerequisites

we will need 7 servers with Debian 12 (Bookworm) or Ubuntu 22.04. The role of each one is described below:

text
Hostname  IP               Rol
--------  ---------------  ---------------------------------
kc1       192.168.1.210    Nodo Keycloak 1
kc2       192.168.1.211    Nodo Keycloak 2
lb1       192.168.1.212    Balanceador Nginx + Keepalived (MASTER)
lb2       192.168.1.213    Balanceador Nginx + Keepalived (BACKUP)
db1       192.168.1.214    PostgreSQL + Patroni + Etcd (nodo 1)
db2       192.168.1.215    PostgreSQL + Patroni + Etcd (nodo 2)
db3       192.168.1.216    PostgreSQL + Patroni + Etcd (nodo 3)
VIP       192.168.1.220    IP virtual gestionada por Keepalived

Additional requirements:

  • Java 17+ at nodes kc1 and kc2.
  • DNS resolution or entries in /etc/hosts for all hostnames.
  • Root or sudo access on all nodes.
  • The following ports must be open on the internal firewall:
text
Puerto    Protocolo  Componente         Entre
--------  ---------  -----------------  ---------------------------
2379      TCP        Etcd client API    db1, db2, db3 <-> db1, db2, db3
2380      TCP        Etcd peer          db1, db2, db3 <-> db1, db2, db3
5432      TCP        PostgreSQL         kc1, kc2 -> db1, db2, db3
8008      TCP        Patroni REST API   db1, db2, db3 internamente
8080      TCP        Keycloak HTTP      lb1, lb2 -> kc1, kc2
7800      TCP        JGroups (Infinispan) kc1 <-> kc2
80/443    TCP        Nginx              Clientes -> lb1, lb2
112       VRRP       Keepalived         lb1 <-> lb2

Add the entries in /etc/hosts on all nodes:

Bash
cat >> /etc/hosts <<EOF
192.168.1.210  kc1
192.168.1.211  kc2
192.168.1.212  lb1
192.168.1.213  lb2
192.168.1.214  db1
192.168.1.215  db2
192.168.1.216  db3
EOF

Installing PostgreSQL and Patroni

Perform the following steps in the three database nodes (db1, db2 and db3).

We install PostgreSQL 16, Patroni and its dependencies:

Bash
# Actualizar repositorios
apt update && apt upgrade -y

# Instalar dependencias de Python y PostgreSQL
apt install -y python3 python3-pip python3-dev libpq-dev gcc

# Instalar el repositorio oficial de PostgreSQL
apt install -y curl ca-certificates gnupg
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/postgresql-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/postgresql-keyring.gpg] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list
apt update

# Instalar PostgreSQL 16
apt install -y postgresql-16 postgresql-client-16

# Instalar Patroni con soporte para Etcd y PostgreSQL
pip3 install patroni[etcd] psycopg2-binary

# Instalar Etcd
apt install -y etcd

Roles of each component:

  • PostgreSQL- The relational database engine where Keycloak stores its data.
  • Patroni- Agent that manages the PostgreSQL lifecycle, performs leader election, and executes automatic failover.
  • Etc.– Distributed key-value store that Patroni uses as a consensus store (DCS – Distributed Configuration Store) to coordinate which node is the leader.

Stop PostgreSQL so Patroni can manage it:

Bash
systemctl stop postgresql
systemctl disable postgresql

Etcd cluster configuration

Configure the Etcd service on each of the three database nodes. The configuration file is located in /etc/default/etcd.

db1 (192.168.1.214):

ini
ETCD_NAME="db1"
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_LISTEN_PEER_URLS="http://192.168.1.214:2380"
ETCD_LISTEN_CLIENT_URLS="http://192.168.1.214:2379,http://127.0.0.1:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.1.214:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.1.214:2379"
ETCD_INITIAL_CLUSTER="db1=http://192.168.1.214:2380,db2=http://192.168.1.215:2380,db3=http://192.168.1.216:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="keycloak-etcd-cluster"

db2 (192.168.1.215):

ini
ETCD_NAME="db2"
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_LISTEN_PEER_URLS="http://192.168.1.215:2380"
ETCD_LISTEN_CLIENT_URLS="http://192.168.1.215:2379,http://127.0.0.1:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.1.215:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.1.215:2379"
ETCD_INITIAL_CLUSTER="db1=http://192.168.1.214:2380,db2=http://192.168.1.215:2380,db3=http://192.168.1.216:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="keycloak-etcd-cluster"

db3 (192.168.1.216):

ini
ETCD_NAME="db3"
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_LISTEN_PEER_URLS="http://192.168.1.216:2380"
ETCD_LISTEN_CLIENT_URLS="http://192.168.1.216:2379,http://127.0.0.1:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.1.216:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.1.216:2379"
ETCD_INITIAL_CLUSTER="db1=http://192.168.1.214:2380,db2=http://192.168.1.215:2380,db3=http://192.168.1.216:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="keycloak-etcd-cluster"

Start and enable the service on all three nodes:

Bash
systemctl enable etcd
systemctl start etcd

Check the status of the Etcd cluster from any of the nodes:

Bash
etcdctl --endpoints=http://192.168.1.214:2379,http://192.168.1.215:2379,http://192.168.1.216:2379 endpoint health

The expected output is:

text
http://192.168.1.214:2379 is healthy: successfully committed proposal: took = 2.3ms
http://192.168.1.215:2379 is healthy: successfully committed proposal: took = 2.5ms
http://192.168.1.216:2379 is healthy: successfully committed proposal: took = 2.7ms

Patroni Settings

Create the configuration directory and the data file on the three nodes:

Bash
mkdir -p /etc/patroni
mkdir -p /data/patroni
chown postgres:postgres /data/patroni
chmod 700 /data/patroni

Patroni configuration in db1/etc/patroni/config.yml:

yaml
scope: keycloak-postgres
namespace: /db/
name: db1

restapi:
  listen: 192.168.1.214:8008
  connect_address: 192.168.1.214:8008

etcd:
  hosts: 192.168.1.214:2379,192.168.1.215:2379,192.168.1.216:2379

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    postgresql:
      use_pg_rewind: true
      use_slots: true
      parameters:
        wal_level: replica
        hot_standby: "on"
        max_wal_senders: 5
        max_replication_slots: 5
        wal_log_hints: "on"

  initdb:
    - encoding: UTF8
    - data-checksums

  pg_hba:
    - host replication replicator 192.168.1.0/24 md5
    - host all all 0.0.0.0/0 md5

  users:
    admin:
      password: "AdminPassword123!"  # CAMBIAR EN PRODUCCIÓN
      options:
        - createrole
        - createdb

postgresql:
  listen: 192.168.1.214:5432
  connect_address: 192.168.1.214:5432
  data_dir: /data/patroni
  bin_dir: /usr/lib/postgresql/16/bin
  pgpass: /tmp/pgpass0
  authentication:
    replication:
      username: replicator
      password: "ReplicaPassword123!"  # CAMBIAR EN PRODUCCIÓN
    superuser:
      username: postgres
      password: "PostgresPassword123!"  # CAMBIAR EN PRODUCCIÓN

tags:
  nofailover: false
  noloadbalance: false
  clonefrom: false
  nosync: false

Patroni configuration in db2/etc/patroni/config.yml (they only change name, listen and connect_address):

yaml
scope: keycloak-postgres
namespace: /db/
name: db2

restapi:
  listen: 192.168.1.215:8008
  connect_address: 192.168.1.215:8008

etcd:
  hosts: 192.168.1.214:2379,192.168.1.215:2379,192.168.1.216:2379

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    postgresql:
      use_pg_rewind: true
      use_slots: true
      parameters:
        wal_level: replica
        hot_standby: "on"
        max_wal_senders: 5
        max_replication_slots: 5
        wal_log_hints: "on"

  initdb:
    - encoding: UTF8
    - data-checksums

  pg_hba:
    - host replication replicator 192.168.1.0/24 md5
    - host all all 0.0.0.0/0 md5

  users:
    admin:
      password: "AdminPassword123!"  # CAMBIAR EN PRODUCCIÓN
      options:
        - createrole
        - createdb

postgresql:
  listen: 192.168.1.215:5432
  connect_address: 192.168.1.215:5432
  data_dir: /data/patroni
  bin_dir: /usr/lib/postgresql/16/bin
  pgpass: /tmp/pgpass0
  authentication:
    replication:
      username: replicator
      password: "ReplicaPassword123!"  # CAMBIAR EN PRODUCCIÓN
    superuser:
      username: postgres
      password: "PostgresPassword123!"  # CAMBIAR EN PRODUCCIÓN

tags:
  nofailover: false
  noloadbalance: false
  clonefrom: false
  nosync: false

Patroni configuration in db3/etc/patroni/config.yml:

yaml
scope: keycloak-postgres
namespace: /db/
name: db3

restapi:
  listen: 192.168.1.216:8008
  connect_address: 192.168.1.216:8008

etcd:
  hosts: 192.168.1.214:2379,192.168.1.215:2379,192.168.1.216:2379

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    postgresql:
      use_pg_rewind: true
      use_slots: true
      parameters:
        wal_level: replica
        hot_standby: "on"
        max_wal_senders: 5
        max_replication_slots: 5
        wal_log_hints: "on"

  initdb:
    - encoding: UTF8
    - data-checksums

  pg_hba:
    - host replication replicator 192.168.1.0/24 md5
    - host all all 0.0.0.0/0 md5

  users:
    admin:
      password: "AdminPassword123!"  # CAMBIAR EN PRODUCCIÓN
      options:
        - createrole
        - createdb

postgresql:
  listen: 192.168.1.216:5432
  connect_address: 192.168.1.216:5432
  data_dir: /data/patroni
  bin_dir: /usr/lib/postgresql/16/bin
  pgpass: /tmp/pgpass0
  authentication:
    replication:
      username: replicator
      password: "ReplicaPassword123!"  # CAMBIAR EN PRODUCCIÓN
    superuser:
      username: postgres
      password: "PostgresPassword123!"  # CAMBIAR EN PRODUCCIÓN

tags:
  nofailover: false
  noloadbalance: false
  clonefrom: false
  nosync: false

Create the systemd service for Patroni on all three nodes (/etc/systemd/system/patroni.service):

ini
[Unit]
Description=Patroni - High Availability PostgreSQL
After=syslog.target network.target etcd.service
Wants=etcd.service

[Service]
Type=simple
User=postgres
Group=postgres
ExecStart=/usr/local/bin/patroni /etc/patroni/config.yml
KillMode=process
TimeoutSec=30
Restart=no

[Install]
WantedBy=multi-user.target
Bash
systemctl daemon-reload
systemctl enable patroni
systemctl start patroni

Check the status of the Patroni cluster:

Bash
patronictl -c /etc/patroni/config.yml list

You should see output similar to this, where one of the nodes is the Leader and the others are Replicas:

text
+ Cluster: keycloak-postgres (7890123456789012345) +----+-----------+
| Member | Host            | Role    | State   | TL | Lag in MB |
+--------+-----------------+---------+---------+----+-----------+
| db1    | 192.168.1.214:5432 | Leader  | running |  1 |           |
| db2    | 192.168.1.215:5432 | Replica | running |  1 |         0 |
| db3    | 192.168.1.216:5432 | Replica | running |  1 |         0 |
+--------+-----------------+---------+---------+----+-----------+

Creating the Keycloak database

Connect to the Patroni leader node (in the example, db1) and create the user and database for Keycloak:

Bash
# Conectar como superusuario de PostgreSQL
sudo -u postgres psql -h 192.168.1.214 -p 5432

Once inside the psql session, run:

sql
-- Crear usuario de Keycloak (cambia la contraseña en producción)
CREATE USER keycloak WITH PASSWORD 'KeycloakDBPassword123!';

-- Crear base de datos
CREATE DATABASE keycloak OWNER keycloak ENCODING 'UTF8' LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8' TEMPLATE template0;

-- Otorgar todos los privilegios
GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak;

-- Verificar
\l keycloak
\q

Keycloak installation

Perform the following steps in the two Keycloak nodes (kc1 and kc2).

Install Java 17, which is the minimum requirement for Keycloak 24.x:

Bash
apt update
apt install -y openjdk-17-jdk curl

# Verificar versión de Java
java -version

Download and install Keycloak 24.0.5 (the latest stable version available):

Bash
# Descargar Keycloak
cd /opt
curl -LO https://github.com/keycloak/keycloak/releases/download/24.0.5/keycloak-24.0.5.tar.gz

# Extraer
tar -xzf keycloak-24.0.5.tar.gz
mv keycloak-24.0.5 keycloak
rm keycloak-24.0.5.tar.gz

# Crear usuario del sistema
groupadd -r keycloak
useradd -r -g keycloak -d /opt/keycloak -s /sbin/nologin keycloak

# Asignar permisos
chown -R keycloak:keycloak /opt/keycloak
chmod -R 750 /opt/keycloak

Configuring Keycloak in cluster mode

Edit the main configuration file /opt/keycloak/conf/keycloak.conf in both nodes. The only difference between kc1 and kc2 is the IP in the parameter hostname and the Infinispan parameter.

Configuration on kc1 (192.168.1.210):

ini
# Base de datos
db=postgres
db-url=jdbc:postgresql://192.168.1.214:5432,192.168.1.215:5432,192.168.1.216:5432/keycloak?targetServerType=primary&loadBalanceHosts=false
db-username=keycloak
db-password=KeycloakDBPassword123!
db-pool-min-size=5
db-pool-initial-size=5
db-pool-max-size=20

# HTTP
http-enabled=true
http-host=0.0.0.0
http-port=8080
https-port=8443

# Hostname (usa la URL pública expuesta por el balanceador)
hostname=https://keycloak.example.com
hostname-admin=https://keycloak.example.com
hostname-strict=false
hostname-strict-backchannel=false

# Proxy
proxy=edge

# Clúster Infinispan
cache=ispn
cache-stack=tcp

# Logging
log=console
log-level=INFO

Configuration in kc2 (192.168.1.211): identical to kc1.

For communication between cluster nodes, Keycloak uses JGroups with the protocol TCPPING. Create the file /opt/keycloak/conf/cache-ispn-tcp.xml on both nodes:

Bash
# Copiar la plantilla incluida con Keycloak
cp /opt/keycloak/conf/cache-ispn.xml /opt/keycloak/conf/cache-ispn-tcp.xml

Edit the section JGroups in cache-ispn-tcp.xml to define the cluster nodes:

Bash
# En kc1, añade la variable de entorno para JGroups TCPPING
# En /opt/keycloak/conf/keycloak.conf añade al final:
cache-ispn-config=conf/cache-ispn-tcp.xml

Add the JGroups environment variables to the file /opt/keycloak/conf/keycloak.conf of each node so that Infinispan discovers the peers via TCPPING:

Bash
# En kc1
export JAVA_OPTS_APPEND="-Djgroups.tcpping.initial_hosts=192.168.1.210[7800],192.168.1.211[7800] -Djgroups.bind.address=192.168.1.210"

# En kc2
export JAVA_OPTS_APPEND="-Djgroups.tcpping.initial_hosts=192.168.1.210[7800],192.168.1.211[7800] -Djgroups.bind.address=192.168.1.211"

Compile and start Keycloak on both nodes:

Bash
sudo -u keycloak /opt/keycloak/bin/kc.sh build
sudo -u keycloak /opt/keycloak/bin/kc.sh start

Create the systemd service on both nodes (/etc/systemd/system/keycloak.service):

ini
[Unit]
Description=Keycloak Identity Provider
After=network.target

[Service]
User=keycloak
Group=keycloak
WorkingDirectory=/opt/keycloak
Environment=KEYCLOAK_ADMIN=admin
Environment=KEYCLOAK_ADMIN_PASSWORD=AdminPassword123!
# En kc1:
Environment=JAVA_OPTS_APPEND=-Djgroups.tcpping.initial_hosts=192.168.1.210[7800],192.168.1.211[7800] -Djgroups.bind.address=192.168.1.210
ExecStart=/opt/keycloak/bin/kc.sh start
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=keycloak
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
Bash
systemctl daemon-reload
systemctl enable keycloak
systemctl start keycloak
systemctl status keycloak

Nginx Load Balancer Configuration

Perform the following steps in the two balancing nodes (lb1 and lb2).

Install Nginx:

Bash
apt update
apt install -y nginx
systemctl enable nginx

Create the virtual host configuration in /etc/nginx/sites-available/keycloak:

nginx
upstream keycloak_nodes {
    # Distribución round-robin por defecto
    # Para sticky sessions basadas en IP:
    ip_hash;

    server 192.168.1.210:8080 max_fails=3 fail_timeout=30s;
    server 192.168.1.211:8080 max_fails=3 fail_timeout=30s;

    keepalive 32;
}

server {
    listen 80;
    server_name keycloak.example.com;

    # Redirigir HTTP a HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name keycloak.example.com;

    # Certificados SSL (Let's Encrypt o corporativos)
    ssl_certificate     /etc/nginx/ssl/keycloak.crt;
    ssl_certificate_key /etc/nginx/ssl/keycloak.key;

    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 10m;

    # Cabeceras de seguridad
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options SAMEORIGIN always;
    add_header X-Content-Type-Options nosniff always;

    # Health check endpoint
    location /health {
        access_log off;
        return 200 "OK\n";
    }

    location / {
        proxy_pass http://keycloak_nodes;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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-Host $host;
        proxy_set_header X-Forwarded-Port $server_port;

        proxy_connect_timeout   60s;
        proxy_send_timeout      300s;
        proxy_read_timeout      300s;

        proxy_buffer_size       128k;
        proxy_buffers           4 256k;
        proxy_busy_buffers_size 256k;

        # Soporte para WebSocket (usado por la consola de administración)
        proxy_cache_bypass $http_upgrade;
    }
}

Activate the configuration and reload Nginx:

Bash
ln -s /etc/nginx/sites-available/keycloak /etc/nginx/sites-enabled/
rm /etc/nginx/sites-enabled/default
nginx -t
systemctl reload nginx

Balancer High Availability with Keepalived

Keepalived implements the protocol VRRP (Virtual Router Redundancy Protocol) to manage a virtual IP (VIP) between the two Nginx nodes. If lb1 goes down, the VIP automatically migrates to lb2 within seconds.

Bash
apt install -y keepalived

lb1 setting (MASTER)/etc/keepalived/keepalived.conf:

Bash
global_defs {
   router_id LB1
}

vrrp_script check_nginx {
   script "pgrep nginx"
   interval 2
   weight -10
}

vrrp_instance VI_KEYCLOAK {
    state MASTER
    interface eth0          # ajusta a tu interfaz de red
    virtual_router_id 51
    priority 110
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass keycloakVRRP  # CAMBIAR EN PRODUCCIÓN
    }

    virtual_ipaddress {
        192.168.1.220/24
    }

    track_script {
        check_nginx
    }
}

lb2 configuration (BACKUP)/etc/keepalived/keepalived.conf:

Bash
global_defs {
   router_id LB2
}

vrrp_script check_nginx {
   script "pgrep nginx"
   interval 2
   weight -10
}

vrrp_instance VI_KEYCLOAK {
    state BACKUP
    interface eth0          # ajusta a tu interfaz de red
    virtual_router_id 51
    priority 100
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass keycloakVRRP  # CAMBIAR EN PRODUCCIÓN
    }

    virtual_ipaddress {
        192.168.1.220/24
    }

    track_script {
        check_nginx
    }
}

Enable and start Keepalived on both nodes:

Bash
systemctl enable keepalived
systemctl start keepalived

# Verificar que la VIP está asignada en lb1
ip addr show eth0 | grep 192.168.1.220

Verification and testing

With all the infrastructure in place, perform the following checks.

1. Access to the administration console:

Open a browser and access https://keycloak.example.com/admin. You should see the Keycloak login screen. Log in with the user admin and the configured password.

2. Check the status of the Keycloak cluster:

Bash
# Comprobar logs en kc1 y kc2 para ver que se han unido al clúster
journalctl -u keycloak -f | grep -i "cluster\|ispn\|jgroups"

# Debe aparecer algo como:
# ISPN000094: Received new cluster view for channel ISPN: [kc1|1] (2) [kc1, kc2]

3. Keycloak failover test:

Bash
# En kc1, para el servicio
systemctl stop keycloak

# Verifica que el servicio sigue respondiendo a través del LB (kc2 debe responder)
curl -s -o /dev/null -w "%{http_code}" https://keycloak.example.com/health/ready
# Respuesta esperada: 200

# Vuelve a arrancar kc1
systemctl start keycloak

4. PostgreSQL Failover Test (Patroni):

Bash
# En db1 (líder actual), para Patroni
systemctl stop patroni

# Observa cómo Patroni promueve automáticamente db2 o db3 como nuevo líder
patronictl -c /etc/patroni/config.yml list

# Verifica que Keycloak sigue funcionando (la URL de JDBC apunta a todos los nodos)
curl -s -o /dev/null -w "%{http_code}" https://keycloak.example.com/health/ready

5. Balancer failover test (Keepalived):

Bash
# En lb1, para Nginx para simular un fallo
systemctl stop nginx

# La VIP debería migrar a lb2 en pocos segundos
# Verificar desde lb2:
ip addr show eth0 | grep 192.168.1.220

# Comprobar conectividad
curl -s -o /dev/null -w "%{http_code}" https://keycloak.example.com/health/ready

Useful monitoring commands:

Bash
# Estado del clúster Patroni
patronictl -c /etc/patroni/config.yml list

# Estado detallado de Patroni vía API REST
curl http://192.168.1.214:8008/cluster | python3 -m json.tool

# Estado de Etcd
etcdctl --endpoints=http://192.168.1.214:2379,http://192.168.1.215:2379,http://192.168.1.216:2379 endpoint status --write-out=table

# Logs de Keycloak
journalctl -u keycloak -n 100 --no-pager

# Conexiones activas en Nginx
nginx -T | grep upstream

Security considerations

Once the infrastructure is up and running, it is essential to apply the following security measures before moving to production:

1. Change all default passwords:
All passwords in this manual are examples. They should be replaced with randomly generated strong passwords:

Bash
# Generar contraseñas seguras de 32 caracteres
openssl rand -base64 32

2. Enable TLS on all components:

  • Etc.: configure ETCD_PEER_CLIENT_CERT_AUTH, ETCD_PEER_CERT_FILE and ETCD_PEER_KEY_FILE to encrypt communication between peers.
  • Patroni ↔ Etcd: use options certfile and keyfile in the section etcd from the Patroni config.
  • PostgreSQL- Enable SSL on postgresql.conf with ssl = on and configure the certificates.
  • Keycloak- Although SSL encryption is terminated in Nginx, consider enabling internal HTTPS between Nginx and Keycloak for high security environments.

3. Firewall rules between components:

Bash
# En los nodos de base de datos: permitir acceso PostgreSQL solo desde kc1 y kc2
ufw allow from 192.168.1.210 to any port 5432
ufw allow from 192.168.1.211 to any port 5432

# Comunicación Etcd solo entre nodos de DB
ufw allow from 192.168.1.214 to any port 2379:2380
ufw allow from 192.168.1.215 to any port 2379:2380
ufw allow from 192.168.1.216 to any port 2379:2380

# En los nodos Keycloak: permitir acceso al puerto 8080 solo desde LBs
ufw allow from 192.168.1.212 to any port 8080
ufw allow from 192.168.1.213 to any port 8080

# Comunicación JGroups solo entre nodos KC
ufw allow from 192.168.1.210 to any port 7800
ufw allow from 192.168.1.211 to any port 7800

4. Restrict access to the Keycloak management console:

It is advisable to limit access to /admin only from trusted networks (for example, the administration network). In Nginx configuration:

nginx
location /admin {
    allow 192.168.10.0/24;  # red de administración
    deny all;

    proxy_pass http://keycloak_nodes;
    # ... resto de directivas proxy ...
}

5. Rotate secrets regularly and store them in a secrets manager such as HashiCorp Vault or your cloud provider's secrets manager.

:wq!

Comments