
TL;DR: ci sarà una descrizione di Keycloak, un sistema di controllo accessi open source, un'analisi della sua architettura interna e dettagli sulla configurazione.
Introduzione e idee principali
In questo articolo esploreremo le idee principali da tenere a mente quando si distribuisce un cluster di Keycloak su Kubernetes.
Se desiderate saperne di più su Keycloak, consultate i link alla fine dell'articolo. Per approfondire la pratica, potete studiare con il modulo che implementa le idee principali di questo articolo (la guida all'avvio è lì, in questo articolo sarà fornita una panoramica della struttura e delle impostazioni, nota del traduttore).
Keycloak è un sistema complesso, scritto in Java e costruito su un server applicativo . In breve, è un framework per autorizzazione che offre agli utenti delle applicazioni funzionalità di federazione e un'opzione SSO (single sign-on).
Invitiamo a leggere la documentazione ufficiale o per una comprensione approfondita.
Avvio di Keycloak
Per l'avvio di Keycloak sono necessari due fonti di dati persistenti:
- Un database utilizzato per memorizzare dati stabili, come le informazioni sugli utenti
- Un cache Datagrid, utilizzato per memorizzare in cache i dati dal database, oltre a memorizzare alcuni metadati a vita breve e spesso modificati, come le sessioni utente. È implementato con , che generalmente è significativamente più veloce del database. Tuttavia, i dati memorizzati in Infinispan sono effimeri e non devono essere persi durante il riavvio del cluster.
Keycloak opera in quattro modalità diverse:
- Normale — un solo processo, configurato tramite il file standalone.xml
- Un cluster normale (versione ad alta disponibilità) — tutti i processi devono utilizzare la stessa configurazione, che deve essere sincronizzata manualmente. Le impostazioni sono memorizzate nel file standalone-ha.xml, è necessario anche fornire accesso condiviso al database e un bilanciatore di carico.
- Cluster di dominio — l'avvio di un cluster in modalità normale diventa rapidamente un'attività routinaria e noiosa con l'aumento del cluster, poiché ogni volta che si modifica la configurazione, è necessario apportare le modifiche a ciascun nodo del cluster. La modalità di dominio risolve questo problema configurando uno spazio di archiviazione condiviso per la pubblicazione della configurazione. Queste impostazioni sono memorizzate nel file domain.xml
- Replica tra i data center — nel caso in cui desideriate avviare Keycloak in un cluster composto da più data center, spesso in diverse località geografiche. In questa modalità ciascun data center avrà il proprio cluster di server Keycloak.
In questo articolo esamineremo in dettaglio la seconda opzione, vale a dire un cluster normale, e toccheremo brevemente anche il tema della replica tra i data center, poiché queste due opzioni hanno senso quando si avviano in Kubernetes. Fortunatamente in Kubernetes non ci sono problemi di sincronizzazione delle impostazioni di più pod (nodi Keycloak), quindi il cluster di dominio non sarà particolarmente difficile da realizzare.
Si prega inoltre di notare che la parola cluster sarà utilizzata esclusivamente fino alla fine dell'articolo per riferirsi a un gruppo di nodi Keycloak che lavorano insieme; non è necessario riferirsi al cluster Kubernetes.
Cluster normale di Keycloak
Per avviare Keycloak in questa modalità è necessario:
- configurare un database esterno condiviso
- installare un bilanciatore di carico
- disporre di una rete interna con supporto per ip multicast
Non tratteremo la configurazione del database esterno, poiché non è l'obiettivo di questo articolo. Supponiamo che ci sia un database funzionante da qualche parte — e abbiamo un punto di connessione ad esso. Aggiungeremo semplicemente questi dati alle variabili d'ambiente.
Per comprendere meglio come Keycloak funziona in un cluster ad alta disponibilità (HA), è importante sapere quanto questo dipenda dalle capacità di clustering di Wildfly.
Wildfly applica diversi sottosistemi, alcuni dei quali vengono utilizzati come bilanciatore di carico, altri per l'alta disponibilità. Il bilanciatore di carico garantisce la disponibilità dell'applicazione in caso di sovraccarico del nodo del cluster, mentre l'alta disponibilità garantisce la disponibilità dell'applicazione anche in caso di guasto di parte dei nodi del cluster. Alcuni di questi sottosistemi sono:
mod_cluster: collabora con Apache come bilanciatore HTTP, dipende da TCP multicast per la ricerca dei nodi per impostazione predefinita. Può essere sostituito da un bilanciatore esterno.infinispan: cache distribuita che utilizza i canali JGroups come livello di trasporto. Può anche applicare il protocollo HotRod per comunicare con un cluster esterno di Infinispan per sincronizzare il contenuto della cache.jgroups: fornisce supporto alle comunicazioni dei gruppi per servizi ad alta disponibilità basati su canali JGroups. I canali nominati consentono alle istanze dell'applicazione nel cluster di connettersi in gruppi in modo che la comunicazione possieda proprietà come affidabilità, ordinamento e tolleranza ai guasti.
Bilanciatore di carico
Quando si imposta il bilanciatore di carico come controller ingress nel cluster Kubernetes, è importante tenere presenti le seguenti considerazioni:
Il funzionamento di Keycloak implica che l'indirizzo remoto del client, connesso via HTTP al server di autenticazione, sia il reale indirizzo IP del computer client. Le impostazioni del bilanciatore di carico e dell'ingress devono impostare correttamente gli header HTTP X-Forwarded-For e X-Forwarded-Proto, e mantenere l'header originale HOST. L'ultima versione ingress-nginx (> 0.22.0)
L'attivazione del flag proxy-address-forwarding tramite la configurazione della variabile d'ambiente PROXY_ADDRESS_FORWARDING in true dà a Keycloak la consapevolezza di trovarsi dietro un proxy.
È anche necessario abilitare sticky sessions nell'ingress. Keycloak utilizza una cache distribuita Infinispan per memorizzare i dati relativi all'attuale sessione di autenticazione e alla sessione utente. Le cache operano con un unico proprietario per impostazione predefinita, in altre parole, questa particolare sessione è memorizzata su un nodo del cluster, mentre gli altri nodi devono richiederla da remoto se necessitano di accedere a questa sessione.
In particolare a noi, contrariamente alla documentazione, non ha funzionato l'allegato della sessione con nome cookie
AUTH_SESSION_ID. Keycloak ha creato un loop di reindirizzamento, quindi consigliamo di scegliere un altro nome per il cookie per la sticky session.
Inoltre, Keycloak allega il nome del nodo che ha risposto per primo a AUTH_SESSION_ID, e poiché ogni nodo nella versione ad alta disponibilità utilizza lo stesso database, ognuno di essi un identificatore di nodo separato e unico per la gestione delle transazioni. È consigliabile impostare in JAVA_OPTS i parametri jboss.node.name e jboss.tx.node.id unici per ciascun nodo — per esempio, si può utilizzare il nome del pod. Se si usa il nome del pod, non dimenticare il vincolo di 23 caratteri per le variabili jboss, quindi è meglio utilizzare StatefulSet anziché Deployment.
Un'altra insidia — se il pod viene rimosso o riavviato, la sua cache andrà persa. Tenendo conto di questo, è opportuno impostare il numero di proprietari della cache per tutte le cache ad almeno due, così rimarrà una copia della cache. La soluzione è avviare al momento dell'avvio del pod, posizionandolo nella directory /opt/jboss/startup-scripts nel contenitore:
Contenuto dello script
embed-server --server-config=standalone-ha.xml --std-out=echo
batch
echo * Impostando CACHE_OWNERS su "${env.CACHE_OWNERS}" in tutti i contenitori di cache
/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-serverdopo di che imposta il valore della variabile ambiente CACHE_OWNERS nel valore richiesto.
Rete privata con supporto per ip multicast
Se utilizzi Weavenet come CNI, il multicast funzionerà immediatamente e i tuoi nodi Keycloak si vedranno a vicenda non appena saranno avviati.
Se non hai il supporto per ip multicast nel cluster Kubernetes, puoi configurare JGroups per lavorare con altri protocolli per la ricerca dei nodi.
La prima opzione è l'uso di KUBE_DNS, che utilizza servizi headless per cercare i nodi Keycloak, basta passare a JGroups il nome del servizio che verrà utilizzato per la ricerca dei nodi.
Un'altra opzione è l'uso del metodo KUBE_PING, che interagisce con l'API per cercare i nodi (devi configurare serviceAccount con i permessi list e get, dopo di che configura i pod per lavorare con questo serviceAccount).
Il metodo di ricerca dei nodi per JGroups si configura impostando le variabili d'ambiente JGROUPS_DISCOVERY_PROTOCOL e JGROUPS_DISCOVERY_PROPERTIES. Per KUBE_PING devi scegliere i pod specificando namespace e labels.
️ Se utilizzi multicast e avvii due o più cluster Keycloak nello stesso cluster Kubernetes (supponiamo uno nel namespace
production, il secondo —staging) — i nodi di un cluster Keycloak potrebbero unirsi a un altro cluster. Assicurati di utilizzare un indirizzo multicast unico per ogni cluster impostando le variabilijboss.default.multicast.addressejboss.modcluster.multicast.addressinJAVA_OPTS.
Replica tra i data center

Connessione
Keycloak utilizza cluster di cache Infinispan separati per ogni data center, in cui sono situati i cluster Keycloak composti da nodi Keycloak. Tuttavia, non c'è differenza tra i nodi Keycloak in diversi data center.
I nodi di Keycloak utilizzano una rete Java Data Grid esterna (server Infinispan) per la connessione tra i data center. La connessione funziona secondo il protocollo .
Le cache Infinispan devono essere configurate con l'attributo remoteStore, affinché i dati possano essere salvati in cache remote (in un altro data center, nota del traduttore) cache. Ci sono cluster separati di infinispan tra i server JDG, quindi i dati salvati su JDG1 nel sito site1 saranno replicati su JDG2 nel sito site2.
Infine, il server ricevente JDG informa i server Keycloak del proprio cluster tramite connessioni client, il che è una caratteristica del protocollo HotRod. I nodi di Keycloak su site2 aggiornano le proprie cache Infinispan, e la specifica sessione utente diventa anche disponibile sui nodi di Keycloak su site2.
Per alcune cache è anche possibile non fare backup e rinunciare completamente alla registrazione dei dati tramite il server Infinispan. Per farlo, è necessario rimuovere la configurazione remote-store alla specifica cache Infinispan (nel file standalone-ha.xml), dopo di che una specifica replicated-cache non sarà più necessaria sul lato del server Infinispan.
Configurazione delle cache
Ci sono due tipi di cache in Keycloak:
Locale. Si trova vicino al database, serve a ridurre il carico sul database e a diminuire la latenza di risposta. In questo tipo di cache vengono memorizzati realm, client, ruoli e metadati degli utenti. Questo tipo di cache non è replicato, anche se questa cache fa parte di un cluster Keycloak. Se viene modificata una certa voce nella cache, ai server rimanenti nel cluster viene inviato un messaggio di aggiornamento, dopo di che la voce viene rimossa dalla cache. Vedi descrizione
workdi seguito, per una descrizione più dettagliata della procedura.Replicato. Gestisce le sessioni utente, i token offline e monitora anche gli errori di accesso per identificare i tentativi di phishing delle password e altri attacchi. I dati memorizzati in queste cache sono temporanei, vengono conservati solo in memoria, ma possono essere replicati nel cluster.
Cache Infinispan
Sessioni — una concezione in Keycloak, singole cache, che si chiamano authenticationSessions, vengono utilizzate per memorizzare i dati di specifici utenti. Le richieste da queste cache sono solitamente necessarie per i browser e i server Keycloak, non per le applicazioni. Qui emerge la dipendenza dalle sticky sessions, e tali cache non devono essere replicate, nemmeno in caso di modalità Active-Active.
Token di azione. Un'altra concezione, solitamente applicata a vari scenari, quando, ad esempio, l'utente deve fare qualcosa in modo asincrono tramite email. Ad esempio, durante la procedura dimentica password cache actionTokens è utilizzato per monitorare i metadati dei token correlati — ad esempio un token è già stato utilizzato e non può essere attivato nuovamente. Questo tipo di cache dovrebbe generalmente essere replicato tra i data center.
Caching e invalidazione dei dati memorizzati funziona per alleggerire il carico dal database. Questo tipo di caching migliora le prestazioni, ma introduce un problema evidente. Se un server Keycloak aggiorna i dati, gli altri server devono essere informati affinché possano aggiornare i dati nelle loro cache. Keycloak utilizza cache locali reami, users e autorizzazione per memorizzare i dati del database.
C'è anche una cache separata work, che è replicata tra tutti i data center. Essa non memorizza alcun dato del database, ma serve a inviare messaggi di invalidazione dei dati ai nodi del cluster tra i data center. In altre parole, non appena i dati vengono aggiornati, il nodo Keycloak invia un messaggio agli altri nodi nel suo data center, così come ai nodi di altri data center. Dopo aver ricevuto un messaggio di questo tipo, ogni nodo pulisce i dati corrispondenti nelle proprie cache locali.
Sessioni utente. Le cache con nomi sessions, clientSessions, offlineSessions e offlineClientSessions, di solito sono replicate tra i data center e servono a memorizzare i dati delle sessioni utente attive durante l'attività dell'utente nel browser. Queste cache lavorano con l'applicazione che gestisce le richieste HTTP dagli utenti finali, quindi sono collegate a sessioni appiccicose e devono essere replicate tra i data center.
Protezione contro attacchi a forza bruta. La cache loginFailures serve a tenere traccia dei dati degli errori di accesso, ad esempio quante volte un utente ha inserito la password errata. La replica di questa cache è responsabilità dell'amministratore. Ma per un conteggio accurato, è opportuno attivare la replica tra i data center. D'altra parte, se non si replica questi dati, si potrà migliorare le prestazioni, e se si presenta questa questione, la replica può anche non essere attivata.
Quando si distribuisce un cluster Infinispan è necessario aggiungere le definizioni delle cache nel file di configurazione:
È necessario configurare e avviare un cluster Infinispan prima di avviare il cluster Keycloak
Poi bisogna configurare remoteStore i cache per Keycloak. A tal fine, è sufficiente uno script, realizzato in modo simile a quello precedente, da utilizzare per configurare la variabile CACHE_OWNERS, bisogna salvarlo in un file e posizionarlo nella directory /opt/jboss/startup-scripts:
Contenuto dello script
embed-server --server-config=standalone-ha.xml --std-out=echo
batch
echo *** Aggiorna il sottosistema infinispan ***
/subsystem=infinispan/cache-container=keycloak:write-attribute(name=module, value=org.keycloak.keycloak-model-infinispan)
echo ** Aggiungi binding socket remoto al server 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 ** Aggiorna l'elemento di lavoro replicated-cache **
/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 ** Aggiorna l'elemento sessioni distributed-cache **
/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 ** Aggiorna l'elemento offlineSessions della cache distribuita **
/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 ** Aggiorna l'elemento clientSessions della cache distribuita **
/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 ** Aggiorna l'elemento offlineClientSessions della cache distribuita **
/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 ** Aggiorna l'elemento loginFailures della cache distribuita **
/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 ** Aggiorna l'elemento actionTokens della cache distribuita **
/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 ** Aggiorna l'elemento authenticationSessions della cache distribuita **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:write-attribute(name=statistics-enabled,value=true)
echo *** Aggiorna il sottosistema undertow ***
/subsystem=undertow/server=default-server/http-listener=default:write-attribute(name=proxy-address-forwarding,value=true)
run-batch
stop-embedded-serverNon dimenticate di impostare JAVA_OPTS per i nodi Keycloak per lavorare con HotRod: remote.cache.host, remote.cache.port e il nome del servizio jboss.site.name.
Link e documentazione aggiuntiva
L'articolo è stato tradotto e preparato per Habr dai dipendenti — corsi intensivi, videolezioni e formazione aziendale da esperti pratici (Kubernetes, DevOps, Docker, Ansible, Ceph, SRE)
Fonte: habr.com
