
TL;DR: será una descripción de Keycloak, un sistema de control de acceso de código abierto, un análisis de su funcionamiento interno y detalles de configuración.
Introducción y conceptos básicos
En este artículo, veremos los conceptos clave que deben tenerse en cuenta al desplegar un clúster de Keycloak sobre Kubernetes.
Si desea saber más sobre Keycloak, consulte los enlaces al final del artículo. Para profundizar en la práctica, puede estudiar con un módulo que implementa las ideas principales de este artículo (la guía de instalación se encuentra allí, en este artículo se hará un resumen de su estructura y configuraciones, nota del traductor).
Keycloak es un sistema integral escrito en Java y construido sobre el servidor de aplicaciones . En resumen, es un framework para la autorización que brinda a los usuarios de aplicaciones la federación y la posibilidad de SSO (inicio de sesión único).
Le invitamos a leer la o para una comprensión detallada.
Despliegue de Keycloak
Keycloak necesita dos fuentes de datos persistentes para su funcionamiento:
- Una base de datos que se utiliza para almacenar datos estables, como información sobre usuarios.
- Un caché de Datagrid que se utiliza para almacenar en caché datos de la base de datos, así como para guardar algunos metadatos efímeros y frecuentemente cambiantes, como sesiones de usuario. Se implementa , que generalmente es mucho más rápido que la base de datos. Sin embargo, de todos modos, los datos almacenados en Infinispan son efímeros y no deben guardarse en ninguna parte al reiniciar el clúster.
Keycloak opera en cuatro modos diferentes:
- Modo normal — un solo proceso, configurado a través del archivo standalone.xml
- Clúster normal (opción de alta disponibilidad) — todos los procesos deben utilizar la misma configuración, que debe sincronizarse manualmente. Las configuraciones se almacenan en el archivo standalone-ha.xml, además de que se debe proporcionar acceso compartido a la base de datos y un equilibrador de carga.
- Clúster por dominio — desplegar un clúster en modo normal rápidamente se convierte en una tarea rutinaria y aburrida a medida que crece el clúster, ya que cada vez que se modifica la configuración, es necesario realizar todos los cambios en cada nodo del clúster. El modo de dominio resuelve este problema configurando un espacio de almacenamiento compartido y publicando la configuración. Estas configuraciones se almacenan en el archivo domain.xml
- Replicación entre centros de datos — en caso de que desee ejecutar Keycloak en un clúster de varios centros de datos, a menudo en diferentes ubicaciones geográficas. En esta modalidad, cada centro de datos tendrá su propio clúster de servidores Keycloak.
En este artículo, examinaremos en detalle la segunda opción, es decir, un clúster normal, y también tocaremos brevemente el tema de la replicación entre centros de datos, ya que estas dos opciones tienen sentido ejecutarlas en Kubernetes. Afortunadamente, en Kubernetes no hay problema con la sincronización de la configuración de varios pods (nodos de Keycloak), así que el clúster de dominio no será muy complicado de implementar.
Además, por favor, tenga en cuenta que la palabra clúster se utilizará hasta el final del artículo exclusivamente en relación con un grupo de nodos de Keycloak que trabajan juntos; no es necesario referirse al clúster de Kubernetes.
Clúster normal de Keycloak
Para ejecutar Keycloak en este modo, se necesita:
- configurar una base de datos compartida externa
- instalar un balanceador de carga
- tener una red interna que soporte ip multicast
No vamos a profundizar en la configuración de la base de datos externa, ya que no es el objetivo de este artículo. Supongamos que hay una base de datos funcional en algún lugar y tenemos un punto de conexión a ella. Simplemente agregaremos estos datos a las variables de entorno.
Para una mejor comprensión de cómo funciona Keycloak en un clúster de alta disponibilidad (HA), es importante saber cuánto depende de las capacidades de clúster de Wildfly.
Wildfly utiliza varios subsistemas, algunos de los cuales se utilizan como balanceadores de carga y otros para la tolerancia a fallos. El balanceador de carga asegura la disponibilidad de la aplicación en caso de sobrecarga en un nodo del clúster, mientras que la tolerancia a fallos garantiza la disponibilidad de la aplicación incluso en caso de fallo de parte de los nodos del clúster. Algunos de estos subsistemas son:
mod_cluster: trabaja junto con Apache como balanceador HTTP, depende de TCP multicast para encontrar nodos por defecto. Puede ser reemplazado por un balanceador externo.infinispan: caché distribuido que utiliza canales JGroups como nivel de transporte. También puede usar el protocolo HotRod para comunicarse con un clúster externo de Infinispan para sincronizar el contenido del caché.jgroups: proporciona soporte de comunicación de grupos para servicios de alta disponibilidad basados en canales JGroups. Los canales nombrados permiten que las instancias de la aplicación en el clúster se conecten en grupos, de manera que la comunicación tenga propiedades como fiabilidad, orden y sensibilidad a fallos.
Balanceador de carga
Al instalar el balanceador como controlador de ingreso en un clúster Kubernetes, es importante tener en cuenta las siguientes cosas:
El funcionamiento de Keycloak implica que la dirección remota del cliente que se conecta por HTTP al servidor de autenticación es la verdadera dirección IP del ordenador cliente. La configuración del balanceador y del ingreso debe establecer correctamente los encabezados HTTP X-Forwarded-For y X-Forwarded-Proto, así como mantener el encabezado original HOST. La última versión ingress-nginx (> 0.22.0)
Activar la bandera proxy-address-forwarding estableciendo la variable de entorno PROXY_ADDRESS_FORWARDING en true le da a Keycloak la comprensión de que está funcionando detrás de un proxy.
También es necesario habilitar sesiones persistentes en el ingreso. Keycloak utiliza la caché distribuida Infinispan para almacenar datos relacionados con la sesión de autenticación actual y la sesión del usuario. Las cachés funcionan con un único propietario por defecto; en otras palabras, esta sesión específica se almacena en un nodo del clúster, y otros nodos deben solicitarla de forma remota si necesitan acceder a esta sesión.
Específicamente, a nosotros, contrariamente a la documentación, no nos funcionó adjuntar la sesión con el nombre de cookie
AUTH_SESSION_ID. Keycloak entró en un bucle de redirección, por lo que recomendamos elegir otro nombre de cookie para la sesión persistente.
Además, Keycloak adjunta el nombre del nodo que respondió primero a AUTH_SESSION_ID, y dado que cada nodo en la variante de alta disponibilidad utiliza la misma base de datos, cada uno de ellos un identificador único de nodo para gestionar transacciones. Se recomienda establecer en JAVA_OPTS los parámetros jboss.node.name y jboss.tx.node.id como únicos para cada nodo; se podría, por ejemplo, poner el nombre del pod. Si se va a usar el nombre del pod, no olvide la limitación de 23 caracteres para las variables de jboss, por lo que es mejor usar StatefulSet en lugar de Deployment.
Otro problema es que si se elimina o reinicia el pod, su caché se pierde. Teniendo esto en cuenta, se debe establecer el número de propietarios de caché para todas las cachés en al menos dos, de modo que se mantenga una copia de la caché. La solución es ejecutar al iniciar el pod, colocándolo en el directorio /opt/jboss/startup-scripts dentro del contenedor:
Contenido del script
embed-server --server-config=standalone-ha.xml --std-out=echo
batch
echo * Estableciendo CACHE_OWNERS a "${env.CACHE_OWNERS}" en todos los contenedores de caché
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:write-attribute(name=owners, value=${env.CACHE_OWNERS:1})
run-batch
stop-embedded-serverdespués de lo cual se debe establecer el valor de la variable de entorno CACHE_OWNERS al requerido.
Red privada con soporte para IP multicast
Si utiliza Weavenet como CNI, el multicast funcionará de inmediato, y sus nodos de Keycloak se verán entre sí tan pronto como sean iniciados.
Si no tiene soporte para IP multicast en el clúster de Kubernetes, puede configurar JGroups para trabajar con otros protocolos para encontrar nodos.
La primera opción es usar KUBE_DNS, que utiliza un servicio sin cabecera para buscar nodos de Keycloak, simplemente pase a JGroups el nombre del servicio que se utilizará para la búsqueda de nodos.
Otra opción es aplicar el método KUBE_PING, que funciona con la API para buscar nodos (necesita configurar serviceAccount con permisos list y get, después de lo cual configure los pods para trabajar con esta serviceAccount).
Método de búsqueda de nodos para JGroups se configura estableciendo las variables de entorno JGROUPS_DISCOVERY_PROTOCOL y JGROUPS_DISCOVERY_PROPERTIES. Para KUBE_PING debe elegir los pods estableciendo namespace y labels.
️ Si utiliza multicast y ejecuta dos o más clústeres de Keycloak en un mismo clúster de Kubernetes (digamos, uno en el namespace
producción, el segundo —staging) — los nodos de un clúster de Keycloak pueden unirse a otro clúster. Asegúrese de utilizar una dirección multicast única para cada clúster estableciendo las variablesjboss.default.multicast.addressyjboss.modcluster.multicast.addressenJAVA_OPTS.
Replicación entre centros de datos

Comunicación
Keycloak utiliza múltiples clústeres independientes de cachés Infinispan para cada centro de datos donde se encuentran los clústeres de Keycloak, compuestos por nodos de Keycloak. Sin embargo, no hay diferencias entre los nodos de Keycloak en diferentes centros de datos.
Los nodos de Keycloak utilizan una Java Data Grid externa (servidores Infinispan) para la comunicación entre centros de datos. La conexión funciona a través del protocolo .
Los cachés de Infinispan deben estar configurados con el atributo remoteStore, para que los datos puedan almacenarse en cachés remotos (en otro centro de datos, nota del traductor) . Hay clústeres separados de Infinispan entre los servidores JDG, por lo que los datos almacenados en JDG1 en el sitio site1 se replicarán en JDG2 en el sitio site2.
Y, por último, el servidor receptor JDG notifica a los servidores Keycloak de su clúster a través de conexiones de cliente, lo cual es una característica del protocolo HotRod. Los nodos de Keycloak en site2 actualizan sus cachés Infinispan, y una sesión de usuario específica también se vuelve accesible en los nodos de Keycloak en site2.
Para algunos cachés, también es posible no hacer copias de seguridad y renunciar completamente a la escritura de datos a través del servidor Infinispan. Para ello, se debe eliminar la configuración remote-store para un caché específico de Infinispan (en el archivo standalone-ha.xml), después de lo cual un caché específico replicated-cache también dejará de ser necesario en el lado del servidor Infinispan.
Configuración de cachés
Hay dos tipos de cachés en Keycloak:
Local. Se encuentra cerca de la base de datos, sirve para reducir la carga en la base de datos y también para disminuir la latencia de respuesta. En este tipo de caché se almacenan realm, clientes, roles y metadatos de usuario. Este tipo de caché no se replica, incluso si este caché es parte de un clúster de Keycloak. Si se modifica algún registro en la caché, se envía un mensaje de cambio a los demás servidores en el clúster, después de lo cual el registro se elimina de la caché. Ver descripción
worka continuación, para una descripción más detallada del procedimiento.Replicado. Maneja sesiones de usuario, tokens offline y también monitorea errores de inicio de sesión para detectar intentos de phishing de contraseñas y otros ataques. Los datos almacenados en estas cachés son temporales, se mantienen solo en memoria, pero pueden ser replicados a través del clúster.
Cachés Infinispan
Sesiones — concepto en Keycloak, cachés individuales que se llaman authenticationSessions, se utilizan para almacenar datos de usuarios específicos. Las solicitudes de estas cachés generalmente son necesarias para los navegadores y servidores Keycloak, no para las aplicaciones. Aquí es donde surge la dependencia de las sesiones sticky, y dichas cachés no deben ser replicadas, incluso en modo Activo-Activo.
Tokens de acción. Un concepto más, utilizado usualmente para diferentes escenarios, cuando, por ejemplo, el usuario debe hacer algo de manera asíncrona por correo. Por ejemplo, durante el proceso de olvidé la contraseña caché actionTokens se utiliza para rastrear metadatos relacionados con los tokens: por ejemplo, un token ya ha sido utilizado y no puede ser activado nuevamente. Este tipo de caché usualmente debe replicarse entre centros de datos.
Caché y expiración de datos almacenados funciona para aliviar la carga de la base de datos. Dicha caché mejora el rendimiento, pero añade un problema obvio. Si un servidor Keycloak actualiza los datos, los otros servidores deben ser notificados para que puedan actualizar sus cachés. Keycloak utiliza cachés locales realms, usuarios y autorización para almacenar datos de la base.
También hay una caché separada work, que se replica entre todos los centros de datos. Esta no almacena datos de la base, sino que sirve para enviar mensajes sobre la expiración de datos a los nodos del clúster entre centros de datos. En otras palabras, una vez que los datos son actualizados, el nodo de Keycloak envía un mensaje a otros nodos en su centro de datos, así como a nodos en otros centros de datos. Al recibir dicho mensaje, cada nodo limpia los datos correspondientes en sus cachés locales.
Sesiones de usuario. Las cachés con los nombres sessions, clientSessions, offlineSessions y offlineClientSessions, generalmente se replican entre centros de datos y sirven para almacenar datos sobre las sesiones de usuario que están activas durante la actividad del usuario en el navegador. Estas cachés trabajan con la aplicación que maneja solicitudes HTTP de los usuarios finales, por lo que están relacionadas con sesiones sticky y deben ser replicadas entre centros de datos.
Protección contra ataques de fuerza bruta. Caché loginFailures sirve para realizar el seguimiento de datos de errores de acceso, como cuántas veces un usuario ingresó una contraseña incorrecta. La replicación de esta caché es responsabilidad del administrador. Sin embargo, para un conteo preciso, es recomendable activar la replicación entre centros de datos. Pero, por otro lado, si no se replican estos datos, se puede mejorar el rendimiento, y si se presenta esta cuestión, la replicación puede no activarse.
Al desplegar un clúster de Infinispan, es necesario agregar las definiciones de caché en el archivo de configuración:
Es necesario configurar y arrancar el clúster de Infinispan antes de iniciar el clúster de Keycloak.
Luego, se debe configurar remoteStore para las cachés de Keycloak. Para ello, es suficiente con un script, que se realiza de manera similar al anterior, que se usa para configurar la variable CACHE_OWNERS, debe guardarse en un archivo y colocarse en el directorio. /opt/jboss/startup-scripts:
Contenido del script
embed-server --server-config=standalone-ha.xml --std-out=echo
batch
echo *** Actualizar el subsistema infinispan ***
/subsystem=infinispan/cache-container=keycloak:write-attribute(name=module, value=org.keycloak.keycloak-model-infinispan)
echo ** Añadir enlace de socket remoto al servidor infinispan **
/socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=remote-cache:add(host=${remote.cache.host:localhost}, port=${remote.cache.port:11222})
echo ** Actualizar el elemento de trabajo del caché replicado **
/subsystem=infinispan/cache-container=keycloak/replicated-cache=work/store=remote:add(
passivation=false,
fetch-state=false,
purge=false,
preload=false,
shared=true,
remote-servers=["remote-cache"],
cache=work,
properties={
rawValues=true,
marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory,
protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion}
}
)
/subsystem=infinispan/cache-container=keycloak/replicated-cache=work:write-attribute(name=statistics-enabled,value=true)
echo ** Actualizar el elemento de sesiones del caché distribuido **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=remote:add(
passivation=false,
fetch-state=false,
purge=false,
preload=false,
shared=true,
remote-servers=["remote-cache"],
cache=sessions,
properties={
rawValues=true,
marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory,
protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion}
}
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:write-attribute(name=statistics-enabled,value=true)
echo ** Actualizar el elemento offlineSessions del caché distribuido **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions/store=remote:add(
passivation=false,
fetch-state=false,
purge=false,
preload=false,
shared=true,
remote-servers=["remote-cache"],
cache=offlineSessions,
properties={
rawValues=true,
marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory,
protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion}
}
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:write-attribute(name=statistics-enabled,value=true)
echo ** Actualizar el elemento clientSessions del caché distribuido **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=remote:add(
passivation=false,
fetch-state=false,
purge=false,
preload=false,
shared=true,
remote-servers=["remote-cache"],
cache=clientSessions,
properties={
rawValues=true,
marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory,
protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion}
}
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:write-attribute(name=statistics-enabled,value=true)
echo ** Actualizar el elemento offlineClientSessions del caché distribuido **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/store=remote:add(
passivation=false,
fetch-state=false,
purge=false,
preload=false,
shared=true,
remote-servers=["remote-cache"],
cache=offlineClientSessions,
properties={
rawValues=true,
marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory,
protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion}
}
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:write-attribute(name=statistics-enabled,value=true)
echo ** Actualizar el elemento loginFailures del caché distribuido **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures/store=remote:add(
passivation=false,
fetch-state=false,
purge=false,
preload=false,
shared=true,
remote-servers=["remote-cache"],
cache=loginFailures,
properties={
rawValues=true,
marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory,
protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion}
}
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:write-attribute(name=statistics-enabled,value=true)
echo ** Actualizar el elemento actionTokens del caché distribuido **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/store=remote:add(
passivation=false,
fetch-state=false,
purge=false,
preload=false,
shared=true,
cache=actionTokens,
remote-servers=["remote-cache"],
properties={
rawValues=true,
marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory,
protocolVersion=${keycloak.connectionsInfinispan.hotrodProtocolVersion}
}
)
/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:write-attribute(name=statistics-enabled,value=true)
echo ** Actualizar el elemento authenticationSessions del caché distribuido **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:write-attribute(name=statistics-enabled,value=true)
echo *** Actualizar el subsistema undertow ***
/subsystem=undertow/server=default-server/http-listener=default:write-attribute(name=proxy-address-forwarding,value=true)
run-batch
stop-embedded-serverNo olvides configurar JAVA_OPTS para nodos Keycloak para el funcionamiento de HotRod: remote.cache.host, remote.cache.port y el nombre del servicio jboss.site.name.
Enlaces y documentación adicional
El artículo fue traducido y preparado para Habr por empleados del — intensivos, videocursos y formación corporativa de profesionales en activo (Kubernetes, DevOps, Docker, Ansible, Ceph, SRE)
Fuente: habr.com
