Запускаємо Keycloak в режимі HA на Kubernetes

Запускаємо Keycloak в режимі HA на Kubernetes

TL, д-р: буде опис Keycloak, системи контролю доступу з відкритим вихідним кодом, розбір внутрішнього пристрою, деталі налаштування.

Введення та основні ідеї

У цій статті ми побачимо основні ідеї, які слід пам'ятати під час розгортання кластера Keycloak поверх Kubernetes.

Якщо хочете знати детальніше про Keycloak — зверніться до посилань наприкінці статті. Для того, щоб сильніше поринути в практику, можете вивчити наш репозиторій з модулем, який реалізує основні ідеї цієї статті (посібник із запуску там же, у цій статті буде огляд пристрою та налаштувань, прим. перекладача).

Keycloak - це комплексна система, написана на Java і побудована поверх сервера додатків Дика муха. Якщо коротко, це framework для авторизації, що дає користувачам додатків федеративність та можливість SSO (single sign-on).

Запрошуємо почитати офіційний сайт або Вікіпедію для детального розуміння.

Запуск Keycloak

Для Keycloak необхідно два постійно зберігаються джерела даних для запуску:

  • База даних, що використовується для зберігання усталених даних, наприклад інформації про користувачів
  • Datagrid cache, який застосовується для кешування даних з бази, а також для зберігання деяких короткоживучих і часто змінюваних метаданих, наприклад сесій користувача. Релізується Інфініспанщо зазвичай значно швидше бази даних. Але в будь-якому випадку дані, що зберігаються в 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.

Також треба увімкнути липкі сеанси у 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 (припустимо один в namespace production, другий - staging) - вузли одного кластера Keycloak можуть приєднатися до іншого кластера. Обов'язково використовуйте унікальну multicast адресу для кожного кластера шляхом встановлення зміннихjboss.default.multicast.address и jboss.modcluster.multicast.address в JAVA_OPTS.

Реплікація між датацентрами

Запускаємо Keycloak в режимі HA на Kubernetes

Зв'язок

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 потрібно додати визначення кешів у файл налаштувань:

<replicated-cache-configuration name="keycloak-sessions" mode="ASYNC" start="EAGER" batching="false">
</replicated-cache-configuration>

<replicated-cache name="work" configuration="keycloak-sessions" />
<replicated-cache name="sessions" configuration="keycloak-sessions" />
<replicated-cache name="offlineSessions" configuration="keycloak-sessions" />
<replicated-cache name="actionTokens" configuration="keycloak-sessions" />
<replicated-cache name="loginFailures" configuration="keycloak-sessions" />
<replicated-cache name="clientSessions" configuration="keycloak-sessions" />
<replicated-cache name="offlineClientSessions" configuration="keycloak-sessions" />

Необхідно налаштувати та запустити кластер 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)

Джерело: habr.com

Додати коментар або відгук