Inicio Linux & Systems Networks & Infrastructure Cybersecurity Cloud & DevOps SIEM & Monitoring DFIR & Threat Intel Development & Other Todas las categorias Herramientas

Keycloak en Alta Disponibilidad con Patroni, Etcd y Nginx

Keycloak en Alta Disponibilidad con Patroni, Etcd y Nginx

Tabla de contenidos

Introducción

Keycloak es una plataforma de gestión de identidades y accesos (IAM) de código abierto desarrollada por Red Hat. Proporciona autenticación y autorización centralizada mediante protocolos estándar como OpenID Connect, OAuth 2.0 y SAML 2.0, permitiendo implementar Single Sign-On (SSO) en toda la organización sin necesidad de modificar las aplicaciones.

En entornos de producción, la identidad es un servicio crítico: si Keycloak cae, los usuarios no pueden autenticarse en ninguna aplicación. Por ello, desplegar Keycloak en alta disponibilidad (HA) es imprescindible para garantizar la continuidad del negocio.

En esta guía desplegaremos una arquitectura HA completa con los siguientes componentes:

  • 2 nodos Keycloak en clúster activo-activo con Infinispan para replicación de sesiones.
  • 3 nodos PostgreSQL gestionados por Patroni y Etcd para alta disponibilidad de la base de datos con failover automático.
  • 2 nodos Nginx como balanceadores de carga con Keepalived para una VIP (Virtual IP) con failover automático.

El diagrama de la arquitectura es el siguiente:

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

Requisitos previos

Necesitaremos 7 servidores con Debian 12 (Bookworm) o Ubuntu 22.04. A continuación se describe el rol de cada uno:

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

Requisitos adicionales:

  • Java 17+ en los nodos kc1 y kc2.
  • Resolución DNS o entradas en /etc/hosts para todos los hostnames.
  • Acceso root o sudo en todos los nodos.
  • Los siguientes puertos deben estar abiertos en el firewall interno:
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

Añade las entradas en /etc/hosts en todos los nodos:

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

Instalación de PostgreSQL y Patroni

Ejecuta los siguientes pasos en los tres nodos de base de datos (db1, db2 y db3).

Instalamos PostgreSQL 16, Patroni y sus dependencias:

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 de cada componente:

  • PostgreSQL: el motor de base de datos relacional donde Keycloak almacena sus datos.
  • Patroni: agente que gestiona el ciclo de vida de PostgreSQL, realiza la elección de líder y ejecuta el failover automático.
  • Etcd: almacén distribuido de clave-valor que Patroni utiliza como almacén de consenso (DCS — Distributed Configuration Store) para coordinar qué nodo es el líder.

Detén PostgreSQL para que sea Patroni quien lo gestione:

bash
systemctl stop postgresql
systemctl disable postgresql

Configuración del clúster Etcd

Configura el servicio Etcd en cada uno de los tres nodos de base de datos. El fichero de configuración se encuentra en /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"

Inicia y habilita el servicio en los tres nodos:

bash
systemctl enable etcd
systemctl start etcd

Verifica el estado del clúster Etcd desde cualquiera de los nodos:

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

La salida esperada es:

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

Configuración de Patroni

Crea el directorio de configuración y el fichero de datos en los tres nodos:

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

Configuración de Patroni en 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

Configuración de Patroni en db2/etc/patroni/config.yml (solo cambian name, listen y 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

Configuración de Patroni en 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

Crea el servicio systemd para Patroni en los tres nodos (/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

Verifica el estado del clúster Patroni:

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

Deberías ver una salida similar a esta, donde uno de los nodos es el líder (Leader) y los demás son réplicas (Replica):

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 |
+--------+-----------------+---------+---------+----+-----------+

Creación de la base de datos de Keycloak

Conéctate al nodo líder de Patroni (en el ejemplo, db1) y crea el usuario y la base de datos para Keycloak:

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

Una vez dentro de la sesión psql, ejecuta:

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

Instalación de Keycloak

Ejecuta los siguientes pasos en los dos nodos Keycloak (kc1 y kc2).

Instala Java 17, que es el requisito mínimo para Keycloak 24.x:

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

# Verificar versión de Java
java -version

Descarga e instala Keycloak 24.0.5 (la última versión estable disponible):

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

Configuración de Keycloak en modo clúster

Edita el fichero de configuración principal /opt/keycloak/conf/keycloak.conf en ambos nodos. La única diferencia entre kc1 y kc2 es la IP en el parámetro hostname y el parámetro de Infinispan.

Configuración en 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

Configuración en kc2 (192.168.1.211): idéntica a kc1.

Para la comunicación entre nodos del clúster, Keycloak usa JGroups con el protocolo TCPPING. Crea el fichero /opt/keycloak/conf/cache-ispn-tcp.xml en ambos nodos:

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

Edita la sección JGroups en cache-ispn-tcp.xml para definir los nodos del clúster:

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

Añade las variables de entorno de JGroups en el fichero /opt/keycloak/conf/keycloak.conf de cada nodo para que Infinispan descubra los peers vía 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"

Compila y arranca Keycloak en ambos nodos:

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

Crea el servicio systemd en ambos nodos (/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

Configuración del balanceador de carga Nginx

Ejecuta los siguientes pasos en los dos nodos de balanceo (lb1 y lb2).

Instala Nginx:

bash
apt update
apt install -y nginx
systemctl enable nginx

Crea la configuración del virtual host en /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;
    }
}

Activa la configuración y recarga Nginx:

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

Alta disponibilidad del balanceador con Keepalived

Keepalived implementa el protocolo VRRP (Virtual Router Redundancy Protocol) para gestionar una IP virtual (VIP) entre los dos nodos Nginx. Si lb1 cae, la VIP migra automáticamente a lb2 en cuestión de segundos.

bash
apt install -y keepalived

Configuración de lb1 (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
    }
}

Configuración de lb2 (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
    }
}

Habilita e inicia Keepalived en ambos nodos:

bash
systemctl enable keepalived
systemctl start keepalived

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

Verificación y pruebas

Con toda la infraestructura levantada, realiza las siguientes comprobaciones.

1. Acceso a la consola de administración:

Abre un navegador y accede a https://keycloak.example.com/admin. Deberías ver la pantalla de login de Keycloak. Inicia sesión con el usuario admin y la contraseña configurada.

2. Verificar el estado del clúster Keycloak:

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. Prueba de failover de Keycloak:

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. Prueba de failover de PostgreSQL (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. Prueba de failover del balanceador (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

Comandos útiles de monitorización:

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

Consideraciones de seguridad

Una vez que la infraestructura esté funcionando, es fundamental aplicar las siguientes medidas de seguridad antes de pasar a producción:

1. Cambiar todas las contraseñas por defecto:
Todas las contraseñas en este manual son ejemplos. Deben sustituirse por contraseñas seguras generadas aleatoriamente:

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

2. Habilitar TLS en todos los componentes:

  • Etcd: configura ETCD_PEER_CLIENT_CERT_AUTH, ETCD_PEER_CERT_FILE y ETCD_PEER_KEY_FILE para cifrar la comunicación entre peers.
  • Patroni ↔ Etcd: usa las opciones certfile y keyfile en la sección etcd del config de Patroni.
  • PostgreSQL: habilita SSL en postgresql.conf con ssl = on y configura los certificados.
  • Keycloak: aunque el cifrado SSL se termina en Nginx, considera habilitar HTTPS interno entre Nginx y Keycloak para entornos de alta seguridad.

3. Reglas de firewall entre componentes:

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. Restringir el acceso a la consola de administración de Keycloak:

Es recomendable limitar el acceso a /admin solo desde redes de confianza (por ejemplo, la red de administración). En la configuración de Nginx:

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. Rotar secretos regularmente y almacenarlos en un gestor de secretos como HashiCorp Vault o el gestor de secretos de tu proveedor cloud.

:wq!