[Перевод] Запускаем Keycloak в HA режиме на Kubernetes
TL; DR: будет описание Keycloak, системы контроля доступа с открытым исходным кодом, разбор внутреннего устройства, детали настройки.
Введение и основные идеи
В этой статье мы увидим основные идеи, которые следует помнить при разворачивании кластера Keycloak поверх Kubernetes.
Если желаете знать более детально о Keycloak — обратитесь к ссылкам в конце статьи. Для того, чтобы сильнее погрузиться в практику — можете изучить наш репозиторий с модулем, который реализует основные идеи этой статьи (руководство по запуску там же, в этой статье будет обзор устройства и настроек, прим. переводчика).
Keycloak — это комплексная система, написанная на Java и построенная поверх сервера приложений Wildfly. Если кратко, это framework для авторизации, дающий пользователям приложений федеративность и возможность SSO (single sign-on).
Приглашаем почитать официальный сайт или Википедию для подробного понимания.
Запуск Keycloak
Для Keycloak необходимо два постоянно хранимых источника данных для запуска:
- База данных, применяемая для хранения устоявшихся данных, например информации о пользователях
- Datagrid cache, который применяется для кэширования данных из базы, а также для хранения некоторых короткоживущих и часто изменяемых метаданных, например пользовательских сессий. Релизуется Infinispan, который обычно значительно быстрее базы данных. Но в любом случае сохраняемые в Infinispan данные эфемерны — и их не надо куда-либо сохранять при перезапуске кластера.
Keycloak работает в четырех различных режимах:
- Обычный — один и только один процесс, настраивается через файл standalone.xml
- Обычный кластер (высокодоступный вариант) — все процессы должны использовать одну и ту же конфигурацию, которую надо синхронизировать вручную. Настройки хранятся в файле standalone-ha.xml, дополнительно надо сделать общий доступ к базе данных и балансировщик нагрузки.
- Доменный кластер — запуск кластера в обычном режиме быстро становится рутинным и скучным занятием при росте кластера, поскольку каждый раз при изменении конфигурации надо все изменения внести на каждом узле кластера. Доменный режим работы решает этот вопрос путем настройки некоторого общего места хранения и публикации конфигурации. Эти настройки хранятся в файле domain.xml
- Репликация между датацентрами — в случае, если хотите запустить Keycloak в кластере из нескольких датацентров, чаще всего в разных местах географически. В этом варианте работы каждый датацентр будет иметь собственный кластер Keycloak серверов.
В этой статье мы детально рассмотрим второй вариант, то есть обычный кластер, а также немного затронем тему насчет репликации между датацентрами, так как эти два варианта имеет смысл запускать в Kubernetes. К счастью в Kubernetes нету проблемы с синхронизацией настроек нескольких подов (узлов Keycloak), так что доменный кластер будет не особо сложно сделать.
Также пожалуйста обратите внимание, что слово кластер до конца статьи будет применяться исключительно насчет группы узлов Keycloak, работающих вместе, нет необходимости ссылаться на кластер Kubernetes.
Обычный кластер Keycloak
Для запуска Keycloak в этом режиме нужно:
- настроить внешнюю общую базу данных
- установить балансировщик нагрузки
- иметь внутреннюю сеть с поддержкой ip multicast
Настройку внешней базы мы разбирать не будем, поскольку она не является целью данной статьи. Давайте будем считать, что где-то есть работающая база данных — и у нас к ней есть точка подключения. Мы просто добавим эти данные в переменные окружения.
Для лучшего понимания того, как Keycloak работает в отказоустойчивом (HA) кластере, важно знать, как сильно это все зависит от способностей Wildfly к кластеризации.
Wildfly применяет несколько подсистем, некоторые из них используются в качестве балансировщика нагрузки, некоторые — для отказоустойчивости. Балансировщик нагрузки обеспечивает доступность приложения при перегрузке узла кластера, а отказоустойчивость гарантирует доступность приложения даже в случае отказа части узлов кластера. Некоторые из этих подсистем:
mod_cluster
: работает совместно с Apache в качестве балансировщика HTTP, зависит от TCP multicast для поиска узлов по умолчанию. Может быть заменен внешним балансировщиком.infinispan
: распределенный кэш, использующий каналы JGroups в качестве транспортного уровня. Дополнительно может применять протокол HotRod для свящи с внешним кластером Infinispan для синхронизации содержимого кэша.jgroups
: предоставляет поддержку связи групп для высокодоступных сервисов на основе каналов JGroups. Именованные каналы позволяют экземплярам приложения в кластере соединяться в группы так, что связь обладает такими свойствами, как надежность, упорядоченность, чувствительность к сбоям.
Балансировщик нагрузки
При установке балансировщика в качестве ingress контроллера в кластере Kubernetes важно иметь ввиду следующие вещи:
Работа Keycloak подразумевает, что удаленный адрес клиента, подключаемого по HTTP к серверу аутентификации, является реальным ip-адресом клиентского компьютера. Настройки балансировщика и ingress должны корректно устанавливать заголовки HTTP X-Forwarded-For
и X-Forwarded-Proto
, а также сохранять изначальный заголовок HOST
. Последняя версия ingress-nginx
(> 0.22.0) отключает это по умолчанию
Активация флага proxy-address-forwarding
путем установки переменной окружения PROXY_ADDRESS_FORWARDING
в true
дает Keycloak понимание, что он работает за proxy.
Также надо включить sticky sessions в ingress. Keycloak применяет распределенный кэш Infinispan для сохранения данных, связанных с текущей сессией аутентификации и пользовательской сессией. Кэши работают с одним владельцем по умолчанию, другими словами эта конкретная сессия сохраняется на некотором узле кластера, а другие узлы должны запрашивать ее удаленно, если им понадобится доступ к этой сессии.
Конкретно у нас вопреки документации не сработало прикрепление сессии с именем cookie AUTH_SESSION_ID
. Keycloak зациклил перенаправление, поэтому мы рекомендуем выбрать другое имя cookie для sticky session.
Также Keycloak прикрепляет имя узла, ответившего первым, к AUTH_SESSION_ID
, а поскольку каждый узел в высокодоступном варианте использует одну и ту же базу данных, каждый из них должен иметь отдельный и уникальный идентификатор узла для управления транзакциями. Рекомендуется ставить в JAVA_OPTS
параметры jboss.node.name
и jboss.tx.node.id
уникальными для каждого узла — можно к примеру ставить имя пода. Если будете ставить имя пода — не забывайте про ограничение в 23 символа для переменных jboss, так что лучше использовать StatefulSet, а не Deployment.
Еще одни грабли — если под удаляется или перезапускается, его кэш теряется. С учетом этого стоит установить число владельцев кэша для всех кэшей не менее чем в два, так будет оставаться копия кэша. Решение — запустить скрипт для Wildfly при запуске пода, подложив его в каталог /opt/jboss/startup-scripts
в контейнере:
embed-server --server-config=standalone-ha.xml --std-out=echo
batch
echo * Setting CACHE_OWNERS to "${env.CACHE_OWNERS}" in all cache-containers
/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-server
после чего установить значение переменной окружения CACHE_OWNERS
в требуемое.
Приватная сеть с поддержкой ip multicast
Если применяете Weavenet в качестве CNI, multicast будет работать сразу же — и ваши узлы Keycloak будут видеть друг друга, как только будут запущены.
Если у вас нет поддержки ip multicast в кластере Kubernetes, можно настроить JGroups на работу с другими протоколами для поиска узлов.
Первый вариант — испольльзование KUBE_DNS
, который использует headless service
для поиска узлов Keycloak, вы просто передаете JGroups имя сервиса, которое будет использовано для поиска узлов.
Еще один вариант — применение метода KUBE_PING
, который работает с API для поиска узлов (надо настроить serviceAccount
с правами list
и get
, после чего настроить поды для работы с этой serviceAccount
).
Способ поиска узлов для JGroups настраивается путем выставления переменных окружения JGROUPS_DISCOVERY_PROTOCOL
и JGROUPS_DISCOVERY_PROPERTIES
. Для KUBE_PING
надо выбрать поды задавая namespace
и labels
.
️ Если используете multicast и запускаете два и больше кластеров Keycloak в одном кластере Kubernetes (допустим один в namespaceproduction
, второй —staging
) — узлы одного кластера Keycloak могут присоединиться к другому кластеру. Обязательно используйте уникальный multicast адрес для каждого кластера путем установки переменныхjboss.default.multicast.address
иjboss.modcluster.multicast.address
вJAVA_OPTS
.
Репликация между датацентрами
Связь
Keycloak использует множественные отдельные кластера кэшей Infinispan для каждого датацентра, где расположены кластера Keycloack, составленные из узлов Keycloak. Но при этом нет разницы между узлами Keycloak в разных датацентрах.
Узлы Keycloak используют внешнюю Java Data Grid (сервера Infinispan) для связи между датацентрами. Связь работает по протоколу Infinispan HotRod.
Кэши Infinispan должны быть настроены с атрибутом remoteStore
, для того, чтобы данные могли сохраняться в удаленных (в другом датацентре, прим. переводчика) кэшах. Есть отдельные кластера infinispan среди JDG серверов, так что данные, сохраняемые на JDG1 на площадке site1
будут реплицированы на JDG2 на площадке site2
.
Ну и наконец, принимающий сервер JDG оповещает сервера Keycloak своего кластера через клиентские соединения, что является особенностью протокола HotRod. Узлы Keycloak на site2
обновляют свои кэши Infinispan, и конкретная пользовательская сессия становится также доступной на узлах Keycloak на site2
.
Для некоторых кэшей также возможно не делать резервные копии и полностью отказаться от записи данных через сервер Infinispan. Для этого надо убрать настройку remote-store
конкретному кэшу Infinispan (в файле standalone-ha.xml), после чего некоторый конкретный replicated-cache
также перестанет быть нужным на стороне Infinispan сервера.
Настройка кэшей
Есть два типа кэшей в Keycloak:
Локальный. Он расположен рядом с базой, служит для уменьшения нагрузки на базу данных, а также для снижения задержки ответа. В этом типе кэша хранится realm, клиенты, роли и пользовательские метаданные. Этот тип кэша не реплицируется, даже если этот кэш — часть кластера Keycloak. Если меняется некоторая запись в кэше — остальным серверам в кластере отправляется сообщение об изменении, после чего запись исключается из кэша. См. описание
work
далее, для более детального описания процедуры.Реплицируемый. Обрабатывает пользовательские сессии, offline токены, а также следит за ошибками входа для определения попыток фишинга паролей и других атак. Хранимые данные в этим кэшах — временные, хранятся только в оперативной памяти, но могут быть реплицированы по кластеру.
Кэши Infinispan
Сессии — концепция в Keycloak, отдельные кэши, которые называются authenticationSessions
, применяются для хранения данных конкретных пользователей. Запросы с этих кэшей обычно нужны браузеру и серверам Keycloak, не приложениям. Здесь и проявляется зависимость от sticky sessions, а сами такие кэши не нужно реплицировать, даже и в случае Active-Active режима.
Токены действия. Очередная концепция, обычно применяется для различных сценариев, когда, к примеру, пользователь должен сделать что-то асинхронно по почте. Например, во время процедуры forget password
кэш actionTokens
применяется для отслеживания метаданных связанных токенов — к примеру токен уже использован, и не может быть активирован повторно. Этот тип кэша обычно должен реплицироваться между датацентрами.
Кэширование и устаревание хранимых данных работает для того, чтобы снять нагрузку с базы данных. Подобное кэширование улучшает производительность, но добавляет очевидную проблему. Если один сервер Keycloak обновляет данные, остальные сервера должны быть оповещены об этом, чтобы они могли провести актуализацию данных в своих кэшах. Keycloak использует локальные кэши realms
, users
и authorization
для кэширования данных из базы.
Также есть отдельный кэш work
, который реплицируется по всем датацентрам. Сам он не хранит каких-либо данных из базы, а служит для отправки сообщений об устаревании данных узлам кластера между датацентрами. Другими словами, как только данные обновляются, узел Keycloak посылает сообщение другим узлам в своем датацентре, а также узлам других датацентров. После получения такого сообщения каждый узел проводит чистку соответствующих данных в своих локальных кэшах.
Пользовательские сессии. Кэши с именами sessions
, clientSessions
, offlineSessions
и offlineClientSessions
, обычно реплицируются между датацентрами и служат для хранения данных об пользовательских сессиях, которые активны во время активности пользователя в браузере. Эти кэши работают с приложением, обрабатывающим запросы HTTP от конечных пользователей, так что они связаны с sticky sessions и должны реплицироваться между датацентрами.
Защита от перебора грубой силой. Кэш loginFailures
служит для отслеживания данных ошибок входа, например сколько раз пользователь ввел неверный пароль. Репликация данного кэша — дело администратора. Но для точного подсчета стоит активировать репликацию между датацентрами. Но с другой стороны если не реплицировать эти данные, получится улучшить производительность, и если встает этот вопрос — репликацию можно и не активировать.
При раскатке кластера Infinispan нужно добавить определения кэшей в файл настроек:
Необходимо настроить и запустить кластер Infinispan перед запуском кластера Keycloak
Затем надо настроить remoteStore
для Keycloak кэшей. Для этого достаточно скрипта, который делается аналогично предыдущему, который использоваться для настройки переменной CACHE_OWNERS
, надо сохранить его в файл и положить в каталог /opt/jboss/startup-scripts
:
embed-server --server-config=standalone-ha.xml --std-out=echo
batch
echo *** Update infinispan subsystem ***
/subsystem=infinispan/cache-container=keycloak:write-attribute(name=module, value=org.keycloak.keycloak-model-infinispan)
echo ** Add remote socket binding to infinispan server **
/socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=remote-cache:add(host=${remote.cache.host:localhost}, port=${remote.cache.port:11222})
echo ** Update replicated-cache work element **
/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 ** Update distributed-cache sessions element **
/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 ** Update distributed-cache offlineSessions element **
/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 ** Update distributed-cache clientSessions element **
/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 ** Update distributed-cache offlineClientSessions element **
/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 ** Update distributed-cache loginFailures element **
/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 ** Update distributed-cache actionTokens element **
/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 ** Update distributed-cache authenticationSessions element **
/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:write-attribute(name=statistics-enabled,value=true)
echo *** Update undertow subsystem ***
/subsystem=undertow/server=default-server/http-listener=default:write-attribute(name=proxy-address-forwarding,value=true)
run-batch
stop-embedded-server
Не забывайте установить JAVA_OPTS
для узлов Keycloak для работы HotRod: remote.cache.host
, remote.cache.port
и имя сервиса jboss.site.name
.
Ссылки и дополнительная документация
Статья переведена и подготовлена для Хабра сотрудниками обучающего центра Слёрм — интенсивы, видеокурсы и корпоративное обучение от практикующих специалистов (Kubernetes, DevOps, Docker, Ansible, Ceph, SRE)