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

Manual Detallado: Instalación de Keycloak en Alta Disponibilidad

Manual Detallado: Instalación de Keycloak en Alta Disponibilidad

Tabla de contenidos

Introducción

Keycloak es un sistema de identidad y acceso de código abierto (IAM) que permite gestionar la autenticación y autorización en aplicaciones modernas mediante protocolos estándar como OpenID Connect, OAuth 2.0 y SAML 2.0. En entornos de producción es fundamental desplegarlo en alta disponibilidad para evitar que un único punto de fallo deje sin acceso a todas las aplicaciones que dependen de él.

En este manual configuraremos una arquitectura completa de Keycloak en alta disponibilidad con:

  • Clúster etcd de 3 nodos para coordinación distribuida
  • PostgreSQL en HA con Patroni (3 nodos) como backend de datos
  • 2 nodos Keycloak con replicación de sesiones via Infinispan
  • 2 servidores Nginx como balanceadores de carga con Keepalived (VIP)

La tabla de direccionamiento utilizada a lo largo del manual es la siguiente:

Rol Hostname IP
etcd + Patroni + PostgreSQL node1 db01 192.168.1.214
etcd + Patroni + PostgreSQL node2 db02 192.168.1.215
etcd + Patroni + PostgreSQL node3 db03 192.168.1.216
Keycloak node1 kc01 192.168.1.217
Keycloak node2 kc02 192.168.1.218
Nginx + Keepalived node1 lb01 192.168.1.219
Nginx + Keepalived node2 lb02 192.168.1.220
VIP (Virtual IP) 192.168.1.221

Requisitos previos

Para seguir este manual necesitamos:

  • 3 servidores Debian para base de datos (db01, db02, db03)
  • 2 servidores Debian para Keycloak (kc01, kc02)
  • 2 servidores Debian para Nginx/Keepalived (lb01, lb02)
  • Java 17+ en los nodos Keycloak
  • Acceso root/sudo en todos los nodos
  • Conectividad de red entre todos los nodos

Los puertos que deben estar abiertos entre los distintos niveles de la arquitectura:

text
Puerto(s)       Servicio              Dirección
-----------     ------------------    ----------------------------------
2379/tcp        etcd client API       entre nodos db
2380/tcp        etcd peer             entre nodos db
5432/tcp        PostgreSQL            db nodes ↔ Keycloak nodes
8008/tcp        Patroni REST API      entre nodos db
8080/tcp        Keycloak HTTP         lb nodes → kc nodes
8443/tcp        Keycloak HTTPS        lb nodes → kc nodes
7800/tcp        JGroups / Infinispan  entre nodos kc
80/tcp          Nginx HTTP            clientes → lb nodes
443/tcp         Nginx HTTPS           clientes → lb nodes
VRRP (112)      Keepalived            entre lb nodes

Instalación de la base de datos en HA

3.1 Instalación de paquetes (en los 3 nodos db)

Ejecutamos en db01, db02 y db03:

bash
sudo apt update && sudo apt install -y postgresql postgresql-contrib patroni etcd-server etcd-client curl

3.2 Configuración de etcd

Configuramos etcd en cada nodo editando /etc/default/etcd. Cada nodo tiene su propia dirección IP de escucha y de anuncio.

NODE1 — db01 (192.168.1.214):

bash
# /etc/default/etcd  — db01
ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380"
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0: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="node1=http://192.168.1.214:2380,node2=http://192.168.1.215:2380,node3=http://192.168.1.216:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"
ETCD_NAME="node1"
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_LOG_LEVEL="info"
ETCD_ENABLE_V2="true"

NODE2 — db02 (192.168.1.215):

bash
# /etc/default/etcd  — db02
ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380"
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0: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="node1=http://192.168.1.214:2380,node2=http://192.168.1.215:2380,node3=http://192.168.1.216:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"
ETCD_NAME="node2"
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_LOG_LEVEL="info"
ETCD_ENABLE_V2="true"

NODE3 — db03 (192.168.1.216):

bash
# /etc/default/etcd  — db03
ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380"
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0: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="node1=http://192.168.1.214:2380,node2=http://192.168.1.215:2380,node3=http://192.168.1.216:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"
ETCD_NAME="node3"
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_LOG_LEVEL="info"
ETCD_ENABLE_V2="true"

Habilitamos e iniciamos etcd en los 3 nodos:

bash
sudo systemctl enable etcd
sudo systemctl restart etcd

3.3 Verificación de etcd

Comprobamos que el clúster etcd está sano desde cualquier nodo:

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

Salida esperada:

text
http://192.168.1.214:2379 is healthy: successfully committed proposal: took = 3.2ms
http://192.168.1.215:2379 is healthy: successfully committed proposal: took = 3.5ms
http://192.168.1.216:2379 is healthy: successfully committed proposal: took = 3.8ms

3.4 Configuración de Patroni

Creamos el directorio y el fichero de configuración de Patroni en cada nodo. Patroni se apoyará en etcd como DCS (Distributed Configuration Store).

NODE1 — db01 (192.168.1.214) — /etc/patroni/config.yml:

yaml
scope: keycloak-cluster
namespace: /db/
name: node1

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: 10
        max_replication_slots: 10
        wal_log_hints: "on"

  initdb:
    - encoding: UTF8
    - data-checksums

  pg_hba:
    - host replication replicator 192.168.1.214/32 md5
    - host replication replicator 192.168.1.215/32 md5
    - host replication replicator 192.168.1.216/32 md5
    - host all all 192.168.1.0/24 md5

  users:
    admin:
      password: admin_password
      options:
        - createrole
        - createdb

postgresql:
  listen: 192.168.1.214:5432
  connect_address: 192.168.1.214:5432
  data_dir: /var/lib/postgresql/14/main
  bin_dir: /usr/lib/postgresql/14/bin
  pgpass: /tmp/pgpass0
  authentication:
    replication:
      username: replicator
      password: replicator_password
    superuser:
      username: postgres
      password: postgres_password
    rewind:
      username: rewind_user
      password: rewind_password

tags:
  nofailover: false
  noloadbalance: false
  clonedfrom: false
  nosync: false

NODE2 — db02 (192.168.1.215) — /etc/patroni/config.yml:

yaml
scope: keycloak-cluster
namespace: /db/
name: node2

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: 10
        max_replication_slots: 10
        wal_log_hints: "on"

  initdb:
    - encoding: UTF8
    - data-checksums

  pg_hba:
    - host replication replicator 192.168.1.214/32 md5
    - host replication replicator 192.168.1.215/32 md5
    - host replication replicator 192.168.1.216/32 md5
    - host all all 192.168.1.0/24 md5

  users:
    admin:
      password: admin_password
      options:
        - createrole
        - createdb

postgresql:
  listen: 192.168.1.215:5432
  connect_address: 192.168.1.215:5432
  data_dir: /var/lib/postgresql/14/main
  bin_dir: /usr/lib/postgresql/14/bin
  pgpass: /tmp/pgpass0
  authentication:
    replication:
      username: replicator
      password: replicator_password
    superuser:
      username: postgres
      password: postgres_password
    rewind:
      username: rewind_user
      password: rewind_password

tags:
  nofailover: false
  noloadbalance: false
  clonedfrom: false
  nosync: false

NODE3 — db03 (192.168.1.216) — /etc/patroni/config.yml:

yaml
scope: keycloak-cluster
namespace: /db/
name: node3

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: 10
        max_replication_slots: 10
        wal_log_hints: "on"

  initdb:
    - encoding: UTF8
    - data-checksums

  pg_hba:
    - host replication replicator 192.168.1.214/32 md5
    - host replication replicator 192.168.1.215/32 md5
    - host replication replicator 192.168.1.216/32 md5
    - host all all 192.168.1.0/24 md5

  users:
    admin:
      password: admin_password
      options:
        - createrole
        - createdb

postgresql:
  listen: 192.168.1.216:5432
  connect_address: 192.168.1.216:5432
  data_dir: /var/lib/postgresql/14/main
  bin_dir: /usr/lib/postgresql/14/bin
  pgpass: /tmp/pgpass0
  authentication:
    replication:
      username: replicator
      password: replicator_password
    superuser:
      username: postgres
      password: postgres_password
    rewind:
      username: rewind_user
      password: rewind_password

tags:
  nofailover: false
  noloadbalance: false
  clonedfrom: false
  nosync: false

Detenemos PostgreSQL (Patroni lo gestionará por completo) y arrancamos Patroni en los 3 nodos:

bash
sudo systemctl stop postgresql
sudo systemctl enable patroni
sudo systemctl start patroni

3.5 Verificación del clúster Patroni

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

Salida esperada (node1 como líder, node2 y node3 como réplicas):

text
+ Cluster: keycloak-cluster (7234567890123456789) ------+----+-----------+
| Member | Host             | Role    | State   | TL | Lag in MB |
+--------+------------------+---------+---------+----+-----------+
| node1  | 192.168.1.214:5432 | Leader | running |  1 |           |
| node2  | 192.168.1.215:5432 | Replica | running |  1 |         0 |
| node3  | 192.168.1.216:5432 | Replica | running |  1 |         0 |
+--------+------------------+---------+---------+----+-----------+

3.6 Creación de la base de datos Keycloak

Nos conectamos al nodo líder (en este ejemplo node1/db01) y creamos la base de datos y el usuario para Keycloak:

sql
sudo -u postgres psql

CREATE USER keycloak WITH PASSWORD 'keycloak_password';
CREATE DATABASE keycloak OWNER keycloak;
GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak;
\q

A continuación añadimos las entradas necesarias en pg_hba.conf para que los nodos Keycloak puedan conectarse (si no las añadiste ya en la sección pg_hba del bootstrap de Patroni):

bash
# Añadir al final de pg_hba.conf (gestionado por Patroni via patronictl edit-config o directamente)
host    keycloak    keycloak    192.168.1.217/32    md5
host    keycloak    keycloak    192.168.1.218/32    md5

# Recargar la configuración
patronictl -c /etc/patroni/config.yml reload keycloak-cluster

Instalación de Keycloak (en kc01 y kc02)

4.1 Instalación de Java 17

Keycloak requiere Java 17 o superior. Lo instalamos en ambos nodos Keycloak:

bash
sudo apt update && sudo apt install -y openjdk-17-jdk
java -version

Salida esperada:

text
openjdk version "17.0.11" 2024-04-16
OpenJDK Runtime Environment (build 17.0.11+9-Debian-1)
OpenJDK 64-Bit Server VM (build 17.0.11+9-Debian-1, mixed mode, sharing)

4.2 Descarga e instalación de Keycloak

Descargamos la distribución oficial de Keycloak, la extraemos en /opt y creamos el usuario del sistema:

bash
export KC_VERSION=24.0.4
cd /opt
sudo wget https://github.com/keycloak/keycloak/releases/download/${KC_VERSION}/keycloak-${KC_VERSION}.tar.gz
sudo tar xzf keycloak-${KC_VERSION}.tar.gz
sudo ln -s /opt/keycloak-${KC_VERSION} /opt/keycloak
sudo groupadd keycloak
sudo useradd -r -g keycloak -d /opt/keycloak -s /sbin/nologin keycloak
sudo chown -R keycloak:keycloak /opt/keycloak-${KC_VERSION}

4.3 Configuración de Keycloak para producción

Editamos /opt/keycloak/conf/keycloak.conf en ambos nodos. La URL de la base de datos apunta a la VIP (192.168.1.221) que siempre resolverá al nodo PostgreSQL activo vía HAProxy o al balanceador de Patroni; alternativamente podemos usar directamente la IP del líder actual. Lo más robusto es desplegar HAProxy o pgBouncer apuntando a la REST API de Patroni, pero para simplificar el manual usamos la VIP:

properties
# Base
hostname=keycloak.example.com
http-enabled=true
http-port=8080
proxy-headers=xforwarded

# Database
db=postgres
db-url=jdbc:postgresql://192.168.1.221:5432/keycloak
db-username=keycloak
db-password=keycloak_password

# Cluster / Infinispan
cache=ispn
cache-stack=tcp

# Health & Metrics
health-enabled=true
metrics-enabled=true

4.4 Configuración del clúster Infinispan (JGroups TCP)

Keycloak utiliza Infinispan para replicar sesiones de usuario entre nodos. Configuramos el stack TCP con TCPPING para descubrimiento estático. Editamos /opt/keycloak/conf/cache-ispn.xml y localizamos la sección del stack TCP para sustituir el protocolo de descubrimiento por TCPPING:

xml
<!-- Dentro de <stack name="tcp"> -->
<TCP bind_addr="match-interface:eth0"
     bind_port="7800"
     recv_buf_size="20000000"
     send_buf_size="640000"
     max_bundle_size="64000" />
<TCPPING initial_hosts="192.168.1.217[7800],192.168.1.218[7800]"
         ergonomics="false"
         port_range="0" />
<MERGE3 min_interval="10000"
        max_interval="30000" />
<FD_SOCK />
<FD_ALL timeout="60000" interval="15000" />
<VERIFY_SUSPECT timeout="5000" />
<BARRIER />
<pbcast.NAKACK2 use_mcast_xmit="false"
                discard_delivered_msgs="true" />
<UNICAST3 />
<pbcast.STABLE desired_avg_gossip="50000"
               max_bytes="4000000" />
<pbcast.GMS print_local_addr="true"
            join_timeout="2000" />
<UFC max_credits="2000000"
     min_threshold="0.4" />
<MFC max_credits="2000000"
     min_threshold="0.4" />
<FRAG2 frag_size="60000" />

4.5 Build y primer arranque

Antes de iniciar Keycloak en modo producción debemos ejecutar el comando build para compilar la configuración de forma optimizada:

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

Primer arranque para crear el usuario administrador (solo es necesario la primera vez en uno de los nodos):

bash
sudo -u keycloak KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=admin \
  /opt/keycloak/bin/kc.sh start --optimized

Una vez arrancado correctamente, paramos con Ctrl+C y procedemos a configurar el servicio systemd.

4.6 Servicio systemd

Creamos el fichero de unidad /etc/systemd/system/keycloak.service:

ini
[Unit]
Description=Keycloak IAM
After=network.target

[Service]
Type=exec
User=keycloak
Group=keycloak
ExecStart=/opt/keycloak/bin/kc.sh start --optimized
Restart=on-failure
RestartSec=10
LimitNOFILE=65536

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

4.7 Verificación del clúster Keycloak

Comprobamos que Keycloak responde correctamente y que los dos nodos han formado el clúster Infinispan:

bash
# Health endpoint (desde cualquier nodo o el balanceador)
curl -s http://192.168.1.217:8080/health | python3 -m json.tool
curl -s http://192.168.1.218:8080/health | python3 -m json.tool

# Verificar que el clúster Infinispan tiene 2 miembros (en los logs de Keycloak)
sudo journalctl -u keycloak -f | grep -i "cluster\|infinispan\|jgroups\|members"

En los logs deberías ver algo similar a:

text
INFO  [org.infinispan.CLUSTER] (keycloak-cache-init) ISPN000094: Received new cluster view for channel ISPN:
[kc01|1] (2) [kc01, kc02]

Balanceador de carga Nginx con Keepalived

5.1 Instalación (en lb01 y lb02)

bash
sudo apt update && sudo apt install -y nginx keepalived

5.2 Configuración de Nginx

Creamos el fichero /etc/nginx/conf.d/keycloak.conf en ambos balanceadores. Usamos least_conn para distribuir la carga de forma equitativa y configuramos cabeceras de proxy para que Keycloak conozca la IP real del cliente:

nginx
upstream keycloak_backend {
    least_conn;
    server 192.168.1.217:8080 max_fails=3 fail_timeout=30s;
    server 192.168.1.218:8080 max_fails=3 fail_timeout=30s;
    keepalive 32;
}

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

    location / {
        proxy_pass http://keycloak_backend;
        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_http_version 1.1;
        proxy_set_header Connection "";
        proxy_buffer_size 128k;
        proxy_buffers 4 256k;
        proxy_busy_buffers_size 256k;
        proxy_read_timeout 300s;
        proxy_connect_timeout 10s;
    }

    location /health {
        proxy_pass http://keycloak_backend/health;
        proxy_set_header Host $host;
        access_log off;
    }
}

Verificamos la configuración y recargamos Nginx:

bash
sudo nginx -t && sudo systemctl reload nginx

5.3 Configuración de Keepalived

Keepalived implementa VRRP para mover la IP virtual (VIP 192.168.1.221) automáticamente al nodo activo. Primero creamos el script de comprobación de salud de Nginx:

bash
cat > /etc/keepalived/check_nginx.sh <<'EOF'
#!/bin/bash
if ! systemctl is-active --quiet nginx; then
    exit 1
fi
if ! curl -sf http://127.0.0.1/health > /dev/null 2>&1; then
    exit 1
fi
exit 0
EOF
chmod +x /etc/keepalived/check_nginx.sh

MASTER — lb01 (192.168.1.219) — /etc/keepalived/keepalived.conf:

bash
global_defs {
    router_id LB01
    script_user root
    enable_script_security
}

vrrp_script chk_nginx {
    script "/etc/keepalived/check_nginx.sh"
    interval 2
    weight -20
    fall 2
    rise 2
}

vrrp_instance VI_1 {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 100
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass keycloak_vrrp_secret
    }
    virtual_ipaddress {
        192.168.1.221/24
    }
    track_script {
        chk_nginx
    }
}

BACKUP — lb02 (192.168.1.220) — /etc/keepalived/keepalived.conf:

bash
global_defs {
    router_id LB02
    script_user root
    enable_script_security
}

vrrp_script chk_nginx {
    script "/etc/keepalived/check_nginx.sh"
    interval 2
    weight -20
    fall 2
    rise 2
}

vrrp_instance VI_1 {
    state BACKUP
    interface eth0
    virtual_router_id 51
    priority 90
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass keycloak_vrrp_secret
    }
    virtual_ipaddress {
        192.168.1.221/24
    }
    track_script {
        chk_nginx
    }
}

Habilitamos e iniciamos Keepalived en ambos balanceadores:

bash
sudo systemctl enable keepalived
sudo systemctl start keepalived

5.4 Verificación del balanceador

Comprobamos que la VIP está asignada a lb01 y que el acceso a Keycloak funciona a través de ella:

bash
# Verificar que la VIP está en lb01
ip addr show eth0 | grep 192.168.1.221

# Verificar acceso a Keycloak a través de la VIP
curl -s http://192.168.1.221/health | python3 -m json.tool

Salida esperada del health check:

text
{
    "status": "UP",
    "checks": [
        {
            "name": "Keycloak readiness check",
            "status": "UP"
        }
    ]
}

Pruebas de alta disponibilidad

Una vez completada la instalación, es imprescindible verificar que el sistema tolera correctamente los fallos en cada capa.

Test 1: fallo de un nodo Keycloak

bash
# En kc01: detener Keycloak
sudo systemctl stop keycloak

# Desde un cliente externo: verificar que el acceso sigue funcionando vía VIP
curl -s http://192.168.1.221/health

# Verificar que Nginx está enviando tráfico solo a kc02
sudo tail -f /var/log/nginx/access.log

# Restaurar
sudo systemctl start keycloak

Las sesiones activas de los usuarios deben mantenerse gracias a la replicación de Infinispan entre kc01 y kc02.

Test 2: failover del líder PostgreSQL/Patroni

bash
# Forzar un failover de Patroni (desde cualquier nodo db)
patronictl -c /etc/patroni/config.yml failover keycloak-cluster --master node1 --force

# Verificar que un nuevo líder ha sido elegido
patronictl -c /etc/patroni/config.yml list

# Verificar que Keycloak sigue respondiendo
curl -s http://192.168.1.221/health

Patroni elegerá automáticamente una nueva réplica como líder en menos de 30 segundos (valor de ttl).

Test 3: fallo del balanceador MASTER (lb01)

bash
# En lb01: detener Keepalived para simular el fallo
sudo systemctl stop keepalived

# En lb02: verificar que la VIP ha migrado
ip addr show eth0 | grep 192.168.1.221

# Verificar que el acceso a Keycloak sigue funcionando
curl -s http://192.168.1.221/health

# Restaurar lb01
sudo systemctl start keepalived

Keepalived detectará la pérdida de los anuncios VRRP de lb01 y lb02 asumirá el rol de MASTER, asignándose la VIP en menos de 3 segundos.

Consideraciones de seguridad

Para un despliegue robusto en producción, ten en cuenta las siguientes recomendaciones:

  • TLS en todas las capas: habilita HTTPS en Nginx (Let's Encrypt o certificado corporativo), cifrado TLS en etcd (ETCD_CERT_FILE, ETCD_KEY_FILE), en PostgreSQL (ssl = on) y en las comunicaciones Keycloak-proxy.
  • Contraseñas seguras: cambia inmediatamente todas las contraseñas de ejemplo usadas en este manual (keycloak_password, replicator_password, postgres_password, keycloak_vrrp_secret). Usa un gestor de secretos como HashiCorp Vault o AWS Secrets Manager.
  • Reglas de firewall: implementa reglas estrictas entre capas. Los nodos Keycloak solo deben aceptar conexiones desde los balanceadores en el puerto 8080. Los nodos de base de datos solo deben aceptar conexiones desde los nodos Keycloak en el puerto 5432. Los puertos de etcd (2379/2380) deben ser accesibles únicamente entre los nodos db.
  • Consola de administración: restringe el acceso a /auth/admin/ por IP mediante una directiva allow/deny en Nginx, o usa un VPN.
  • Backups de PostgreSQL: configura copias de seguridad periódicas con pg_basebackup o pgBackRest. Patroni no es un sustituto de los backups: protege contra fallos, no contra errores humanos.
  • Monitorización: expone las métricas de Keycloak (metrics-enabled=true) a Prometheus y visualízalas en Grafana. Monitoriza el estado del clúster Patroni con patroni_exporter y el estado de etcd con su propio exporter.
  • Rotación de logs: configura logrotate para los logs de Nginx y Keycloak para evitar el agotamiento del espacio en disco.

:wq!