
TL;DR: será uma descrição do Keycloak, um sistema de controle de acesso de código aberto, uma análise de sua estrutura interna, detalhes de configuração.
Introdução e ideias principais
Neste artigo, veremos as ideias principais que devem ser lembradas ao implantar um cluster Keycloak sobre o Kubernetes.
Se você deseja saber mais sobre o Keycloak, consulte os links no final do artigo. Para se aprofundar na prática, você pode estudar com um módulo que implementa as ideias principais deste artigo (um guia de início também está lá, neste artigo haverá uma visão geral da estrutura e das configurações, Nota do tradutor).
Keycloak é um sistema abrangente, escrito em Java e construído sobre o servidor de aplicativos . Resumindo, é um framework para autorização, que oferece aos usuários de aplicativos a federação e a possibilidade de SSO (single sign-on).
Convidamos você a ler a ou para uma compreensão detalhada.
Executando o Keycloak
Para o Keycloak, são necessárias duas fontes de dados persistentemente armazenadas para funcionar:
- Um banco de dados, usado para armazenar dados persistentes, como informações sobre usuários.
- Cache Datagrid, que é usado para armazenar em cache dados do banco e também para armazenar alguns metadados efêmeros e frequentemente alterados, como sessões de usuários. Ele é realizado , que geralmente é significativamente mais rápido que um banco de dados. Mas, de qualquer forma, os dados armazenados no Infinispan são efêmeros — e não precisam ser salvos em nenhum lugar ao reiniciar o cluster.
O Keycloak funciona em quatro modos diferentes:
- Comum — um e apenas um processo, configurado através do arquivo standalone.xml
- Cluster normal (opção de alta disponibilidade) — todos os processos devem usar a mesma configuração, que deve ser sincronizada manualmente. As configurações são armazenadas no arquivo standalone-ha.xml, além disso, deve haver um acesso compartilhado ao banco de dados e um balanceador de carga.
- Cluster de domínio — executar um cluster em modo normal rapidamente se torna uma tarefa rotineira e tediosa à medida que o cluster cresce, pois cada vez que a configuração muda, todas as alterações precisam ser feitas em cada nó do cluster. O modo de domínio resolve essa questão configurando um armazenamento comum e publicando a configuração. Essas configurações são armazenadas no arquivo domain.xml
- Replicação entre data centers Se você deseja executar o Keycloak em um cluster de múltiplos datacenters, geralmente geograficamente distintos. Neste modo de operação, cada datacenter terá seu próprio cluster de servidores Keycloak.
Neste artigo, vamos examinar detalhadamente a segunda opção, ou seja, um cluster padrão, além de abordarmos um pouco sobre a replicação entre datacenters, já que essas duas opções fazem sentido serem executadas no Kubernetes. Felizmente, no Kubernetes não há problema com a sincronização das configurações de vários pods (nós do Keycloak), então o cluster de domínio não será muito difícil de implementar.
Além disso, por favor, note que a palavra cluster será utilizada ao longo do artigo exclusivamente em relação a um grupo de nós do Keycloak que trabalham juntos; não há necessidade de se referir ao cluster do Kubernetes.
Cluster padrão do Keycloak
Para executar o Keycloak neste modo, é necessário:
- configurar um banco de dados externo compartilhado
- instalar um balanceador de carga
- ter uma rede interna com suporte a IP multicast
Não abordaremos a configuração do banco de dados externo, uma vez que não é o objetivo deste artigo. Vamos considerar que há um banco de dados funcional em algum lugar — e que temos um ponto de conexão. Apenas adicionaremos esses dados às variáveis de ambiente.
Para uma melhor compreensão de como o Keycloak funciona em um cluster de alta disponibilidade (HA), é importante entender como isso tudo depende das capacidades de clusterização do Wildfly.
O Wildfly utiliza vários subsistemas; alguns deles são usados como balanceador de carga, enquanto outros são utilizados para garantir a alta disponibilidade. O balanceador de carga garante a acessibilidade do aplicativo durante a sobrecarga de um nó do cluster, e a alta disponibilidade garante que o aplicativo permaneça acessível mesmo se parte dos nós do cluster falhar. Alguns desses subsistemas:
mod_cluster: trabalha junto ao Apache como balanceador HTTP, dependendo do multicast TCP para localizar nós por padrão. Pode ser substituído por um balanceador externo.infinispan: cache distribuído que usa canais JGroups como nível de transporte. Adicionalmente, pode empregar o protocolo HotRod para se comunicar com um cluster externo Infinispan para sincronização do conteúdo do cache.jgroups: fornece suporte para comunicação em grupo para serviços de alta disponibilidade com base em canais JGroups. Canais nomeados permitem que instâncias de aplicação em um cluster se conectem em grupos, oferecendo propriedades de comunicação como confiabilidade, ordenação e sensibilidade a falhas.
Balanceador de carga
Ao configurar o balanceador como controlador de ingress no cluster Kubernetes, é importante ter em mente as seguintes considerações:
A operação do Keycloak pressupõe que o endereço remoto do cliente, conectado via HTTP ao servidor de autenticação, seja o verdadeiro endereço IP do computador cliente. As configurações do balanceador e do ingress devem definir corretamente os cabeçalhos HTTP X-Forwarded-For e X-Forwarded-Proto, além de manter o cabeçalho original HOST. A versão mais recente ingress-nginx (> 0.22.0)
A ativação da flag proxy-address-forwarding por meio da definição da variável de ambiente PROXY_ADDRESS_FORWARDING em verdadeiro dá ao Keycloak a compreensão de que ele está operando atrás de um proxy.
Também é necessário habilitar sessões persistentes no ingress. O Keycloak aplica o cache distribuído Infinispan para armazenar dados relacionados à sessão de autenticação atual e à sessão do usuário. Os caches operam com um proprietário por padrão, em outras palavras, essa sessão específica é mantida em um determinado nó do cluster, e os outros nós devem acessá-la remotamente se precisarem de acesso a essa sessão.
Especificamente, contrariando a documentação, a anexação da sessão com o nome de cookie
AUTH_SESSION_ID. O Keycloak causou um redirecionamento em loop, portanto, recomendamos escolher um nome de cookie diferente para a sticky session.
Além disso, o Keycloak anexa o nome do nó que respondeu primeiro ao AUTH_SESSION_ID, e como cada nó na versão de alta disponibilidade usa o mesmo banco de dados, cada um deles um identificador de nó separado e único para gerenciar transações. Recomenda-se configurar as JAVA_OPTS para os parâmetros jboss.node.name e jboss.tx.node.id como únicos para cada nó — por exemplo, pode-se usar o nome do pod. Se for usar o nome do pod, não se esqueça da limitação de 23 caracteres para variáveis jboss, portanto, é melhor utilizar StatefulSet em vez de Deployment.
Outra armadilha — se o pod for removido ou reiniciado, seu cache é perdido. Diante disso, é aconselhável definir o número de proprietários do cache para todos os caches em pelo menos dois, assim uma cópia do cache será mantida. A solução é iniciar ao iniciar o pod, colocando-o no diretório /opt/jboss/startup-scripts no contêiner:
Conteúdo do script
embed-server --server-config=standalone-ha.xml --std-out=echo
batch
echo * Definindo CACHE_OWNERS para "${env.CACHE_OWNERS}" em todos os containers de 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-serverapós o que definir o valor da variável de ambiente CACHE_OWNERS como necessário.
Rede privada com suporte a ip multicast
Se estiver usando Weavenet como CNI, o multicast funcionará imediatamente — e seus nós Keycloak poderão se ver assim que forem iniciados.
Se você não tiver suporte a ip multicast no cluster Kubernetes, é possível configurar o JGroups para trabalhar com outros protocolos de descoberta de nós.
A primeira opção é usar KUBE_DNS, que utiliza serviço headless para descoberta dos nós Keycloak, basta passar ao JGroups o nome do serviço que será usado para descoberta dos nós.
Outra opção é aplicar o método KUBE_PING, que funciona com a API para descoberta de nós (é preciso configurar a serviceAccount com permissões list e get, após o que deve-se configurar os pods para trabalhar com esta serviceAccount).
Método de descoberta dos nós para o JGroups é configurado através da definição das variáveis de ambiente JGROUPS_DISCOVERY_PROTOCOL e JGROUPS_DISCOVERY_PROPERTIES. Para KUBE_PING é necessário escolher os pods definindo namespace e labels.
️ Se você usar multicast e iniciar dois ou mais clusters Keycloak em um mesmo cluster Kubernetes (digamos, um em namespace
productionUma observação.staging) — os nós de um cluster Keycloak poderão se juntar a outro cluster. Certifique-se de usar um endereço multicast exclusivo para cada cluster através da configuração das variáveisjboss.default.multicast.addressejboss.modcluster.multicast.addressemJAVA_OPTS.
Replicação entre data centers

Comunicação
Keycloak utiliza múltiplos clusters separados das caches Infinispan para cada data center onde os clusters Keycloak estão localizados, compostos por nós Keycloak. Mas não há diferença entre os nós Keycloak em diferentes data centers.
Os nós do Keycloak utilizam uma grade de dados Java externa (servidores Infinispan) para comunicação entre data centers. A conexão funciona pelo protocolo .
Os caches do Infinispan devem ser configurados com o atributo remoteStore, para que os dados possam ser salvos em caches remotos (em outro data center, Nota do tradutor) . Existem clusters separados de Infinispan entre os servidores JDG, então os dados salvos no JDG1 no site site1 serão replicados no JDG2 no site site2.
E finalmente, o servidor receptor JDG notifica os servidores do Keycloak em seu cluster através de conexões de clientes, o que é uma característica do protocolo HotRod. Os nós do Keycloak em site2 atualizam seus caches Infinispan, e uma sessão de usuário específica se torna também acessível nos nós do Keycloak em site2.
Para alguns caches também é possível não fazer backups e renunciar completamente à gravação de dados através do servidor Infinispan. Para isso, é necessário remover a configuração remote-store do cache específico do Infinispan (no arquivo standalone-ha.xml), após o que um cache específico replicated-cache também deixará de ser necessário no lado do servidor Infinispan.
Configuração de caches
Existem dois tipos de caches no Keycloak:
Local. Ele está localizado próximo ao banco de dados, serve para reduzir a carga no banco de dados, bem como para diminuir a latência de resposta. Neste tipo de cache são armazenados realm, clientes, funções e metadados de usuários. Este tipo de cache não é replicado, mesmo que este cache seja parte do cluster do Keycloak. Se uma gravação é alterada no cache, uma mensagem de alteração é enviada aos demais servidores no cluster, após o que a gravação é removida do cache. Veja a descrição
worka seguir, para uma descrição mais detalhada do procedimento.Replicável. Processa sessões de usuários, tokens offline, e também monitora erros de login para identificar tentativas de phishing de senhas e outros ataques. Os dados armazenados nesses caches são temporários, ficam apenas na memória, mas podem ser replicados pelo cluster.
Caches do Infinispan
Sessões — uma concepção no Keycloak, caches separados, que são chamados de authenticationSessions, são utilizados para armazenar dados de usuários específicos. As solicitações desses caches geralmente são necessárias para o navegador e os servidores do Keycloak, não para os aplicativos. Aqui se manifesta a dependência de sessões sticky, e esses caches não precisam ser replicados, mesmo no modo Active-Active.
Tokens de ação. Uma nova concepção, geralmente aplicada a vários cenários, quando, por exemplo, o usuário precisa fazer algo assíncrono por e-mail. Por exemplo, durante o procedimento esquecer a senha cache actionTokens é usado para rastrear os metadados relacionados aos tokens — por exemplo, o token já foi utilizado e não pode ser ativado novamente. Esse tipo de cache geralmente deve ser replicado entre os data centers.
Cache e expiração de dados armazenados funciona para aliviar a carga no banco de dados. Esse tipo de cache melhora o desempenho, mas traz um problema óbvio. Se um servidor Keycloak atualiza os dados, os outros servidores devem ser notificados para que possam atualizar os dados em seus caches. O Keycloak utiliza caches locais realms, tinham endereços de e-mail em domínios e autorização para armazenar dados do banco.
Há também um cache separado work, que é replicado em todos os data centers. Ele não armazena quaisquer dados do banco, mas serve para enviar mensagens sobre a expiração de dados aos nós do cluster entre os data centers. Em outras palavras, assim que os dados são atualizados, o nó Keycloak envia uma mensagem a outros nós em seu data center, bem como a nós em outros data centers. Após receber tal mensagem, cada nó realiza a limpeza dos dados relevantes em seus caches locais.
Sessões do usuário. Caches com nomes sessions, clientSessions, offlineSessions e offlineClientSessions, geralmente são replicados entre os data centers e servem para armazenar dados sobre sessões de usuário que estão ativas durante a atividade do usuário no navegador. Esses caches trabalham com o aplicativo que processa solicitações HTTP dos usuários finais, então estão conectados a sessões persistentes e devem ser replicados entre os data centers.
Proteção contra ataques de força bruta. O cache loginFailures serve para rastrear dados de erros de login, como quantas vezes o usuário inseriu a senha incorreta. A replicação desse cache é responsabilidade do administrador. Mas para uma contagem precisa, é necessário ativar a replicação entre os data centers. Por outro lado, se esses dados não forem replicados, o desempenho pode melhorar, e se a questão surgir — a replicação pode não ser ativada.
Ao configurar o cluster Infinispan, é necessário adicionar definições de caches no arquivo de configuração:
É necessário configurar e iniciar o cluster Infinispan antes de iniciar o cluster Keycloak
Em seguida, é preciso configurar remoteStore os caches do Keycloak. Para isso, basta um script, que é feito de maneira semelhante ao anterior, que é utilizado para configurar a variável CACHE_OWNERS, deve-se salvá-lo em um arquivo e colocá-lo no diretório /opt/jboss/startup-scripts:
Conteúdo do script
embed-server --server-config=standalone-ha.xml --std-out=echo
batch
echo *** Atualizar subsistema infinispan ***
/subsystem=infinispan/cache-container=keycloak:write-attribute(name=module, value=org.keycloak.keycloak-model-infinispan)
echo ** Adicionar binding de socket remoto ao 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 ** Atualizar elemento de trabalho 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 ** Atualizar elemento de sessões do 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 ** Atualizar elemento offlineSessions do distributed-cache **
/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 ** Atualizar elemento clientSessions do distributed-cache **
/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 ** Atualizar elemento offlineClientSessions do distributed-cache **
/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 ** Atualizar elemento loginFailures do distributed-cache **
/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 ** Atualizar elemento actionTokens do distributed-cache **
/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 ** Atualizar elemento authenticationSessions do distributed-cache **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:write-attribute(name=statistics-enabled,value=true)
echo *** Atualizar subsistema undertow ***
/subsystem=undertow/server=default-server/http-listener=default:write-attribute(name=proxy-address-forwarding,value=true)
run-batch
stop-embedded-serverNão se esqueça de configurar JAVA_OPTS para nós Keycloak para o funcionamento do HotRod: remote.cache.host, remote.cache.port e o nome do serviço jboss.site.name.
Links e documentação adicional
O artigo foi traduzido e preparado para o Habr pelos colaboradores do — intensivos, cursos em vídeo e treinamento corporativo de especialistas em atividade (Kubernetes, DevOps, Docker, Ansible, Ceph, SRE)
Fonte: habr.com
