Розміщення Keycloak в інфраструктурі Beanstalk: Технічна подорож в Alan

pic

Фото: iMattSmart на Unsplash

Створення безпечних додатків є пріоритетом для будь-якої організації. У Alan ми використовували власне рішення для аутентифікації, яке ефективно задовольняло наші потреби. Однак, з ускладненням наших вимог, ми зрозуміли, що потрібно більш комплексне рішення. Після дослідження різних варіантів, ми вирішили інтегрувати Keycloak для управління аутентифікацією та авторизацією для наших додатків.

Незважаючи на численні переваги, розгортання Keycloak на нашій існуючій інфраструктурі, яка використовує AWS Elastic Beanstalk і Docker контейнери, мало свої виклики. З обмеженою документацією на цю тему, інтеграція виявилася складним завданням.

У цій статті я спробую провести вас через наш процес розгортання Keycloak на інфраструктурі Alan, труднощі, з якими ми зіткнулися, та рішення, які ми знайшли. Сподіваюся, ця стаття допоможе іншим, хто стикається з подібними проблемами і потребує допомоги у підключенні Keycloak до платформи AWS Elastic Beanstalk.

Key... що?

pic

Ця стаття не буде занурюватися в деталі Keycloak, але ми коротко його представимо.

Keycloak — це відкрите рішення для управління ідентифікацією та доступом (Identity and Access Management, IAM), яке надає централізовану аутентифікацію та авторизацію для веб- і мобільних додатків. Підтримка стандартних протоколів, таких як OAuth 2.0, OpenID Connect та SAML, а також різноманітні варіанти аутентифікації, як-от аутентифікація за допомогою пароля, соціальний логін через Google або Facebook, та багатофакторна аутентифікація через SMS або електронну пошту, роблять його універсальним вибором.

У Alan ми приділяємо велику увагу дизайну та забезпеченню безперебійного досвіду для користувачів. Ми також серйозно ставимося до безпеки і впровадили заходи для забезпечення кінцевого шифрування, зокрема передхешування паролів користувачів у браузері. Повністю налаштовувані форми входу та реєстрації в Keycloak зробили його ідеальним вибором для наших потреб.

pic

Форма входу Alan

Keycloak також підтримує грант для обміну паролем власника ресурсу (Resource Owner Password Credentials Grant, ROPC), що дозволяє працювати поступово. Ця функція дозволяє обміняти облікові дані користувача на токен доступу на стороні сервера, минаючи необхідність у складних процесах аутентифікації. Це дозволило нам спочатку здійснити перехід лише на бекенд, а потім оновити наші додатки для інтеграції стандартних редиректів OAuth2.

Крім того, Keycloak побудований на основі серверу додатків WildFly, який працює на Java, зберігає інформацію про користувачів та облікові дані у власній базі даних (підтримує різні механізми, такі як PostgreSQL) і може бути розширений через розробку власних постачальників. Це Java-розширення, що реалізують інтерфейси Keycloak, дозволяють підключати платформу та розширювати її можливості.

Налаштування Keycloak для локальної розробки

Щоб почати працювати з Keycloak, можна скористатися офіційним Docker-образом.
Ми використаємо Docker Compose для оркестрації контейнера Keycloak та бази даних PostgreSQL.

Ось приклад файлу docker-compose.yml для налаштування цих сервісів:

version: '3.7'  

services:  
 postgres:  
 image: postgres  
 environment:  
 POSTGRES_DB: keycloak  
 POSTGRES_USER: keycloak  
 POSTGRES_PASSWORD: keycloak  
 ports:  
 - "5433:5432"  
 healthcheck:  
 test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]  
 interval: 10s  
 timeout: 5s  
 retries: 5  
 volumes:  
 - db-data:/var/lib/postgresql/data  
 keycloak:  
 depends_on:  
 postgres:  
 condition: service_healthy  
 image: quay.io/keycloak/keycloak:21.0.1  
 env_file:  
 - .env.local  
 environment:  
 KC_DB: postgres  
 KC_DB_URL: jdbc:postgresql://postgres/keycloak  
 KC_DB_USERNAME: keycloak  
 KC_DB_PASSWORD: keycloak  
 KEYCLOAK_ADMIN: admin  
 KEYCLOAK_ADMIN_PASSWORD: admin  
 ports:  
 - "8080:8080"  
 volumes:  
 - ./providers:/opt/keycloak/providers  
 command: start-dev

Ми налаштували необхідні змінні середовища для використання бази даних PostgreSQL та відкрили порт 8080, на якому буде доступний веб-інтерфейс Keycloak.

Крім того, ми монтуємо том до директорії /opt/keycloak/providers, що дозволяє додавати власні постачальники до Keycloak. Ці постачальники є файлами JAR, які використовуються для розширення функціональності Keycloak за межі його стандартних можливостей.

Наприклад, у Alan ми взяли участь у розробці та використовуємо два різних постачальники:

  • keycloak-bcrypt: дозволяє нам використовувати вже існуючі паролі, захешовані за допомогою BCrypt, у Keycloak.
  • keycloak-sentry-logger: додає прослуховувач подій (Event Listener), який відправляє помилки до Sentry.

Після запуску docker-compose up, ми можемо увійти до адміністративної консолі Keycloak за допомогою облікових даних admin/admin на адресу http://localhost:8080/admin.

pic

Веб-інтерфейс адміністратора Keycloak

Розгортання Keycloak у продакшн

Наше рішення полягає в розгортанні Keycloak на AWS Elastic Beanstalk.

Ми використаємо архів zip, який містить наш файл Docker Compose, усі файли для спільного використання з контейнером через томи, а також потенційні файли конфігурації для AWS.
Далі ми завантажуємо цей архів на Elastic Beanstalk за допомогою AWS Management Console або CLI.

Ми не будемо вдаватися в деталі (більше інформації можна знайти в спеціалізованих статтях на нашому блозі), але надамо деякий контекст щодо інфраструктури Alan:

Elastic Beanstalk оркеструє створення EC2 інстансів та здійснює балансування навантаження між ними, що дозволяє масштабувати середовище, додаючи більше інстансів за потреби.

Основна конфігурація

Ми зосередимося лише на інтеграції Keycloak та візьмемо за початкову точку:

  • два EC2 інстанси,
  • безпекова група та балансувальник навантаження поверх них,
  • інстанс PostgreSQL,
  • облікові дані бази даних та адміністратора зберігаються в Secrets Manager.

Для розгортання нашого сервісу Keycloak на Elastic Beanstalk, нам потрібно створити архів zip, який містить:

  • наш Docker Compose файл,
  • усі файли, які потрібно поділити з контейнером через томи,
  • потенційні конфігураційні файли для AWS.

Далі ми завантажуємо цей архів на Elastic Beanstalk через AWS Management Console або CLI для розгортання наших сервісів на EC2 інстансах.

Ось спрощений приклад сумісного файлу docker-compose (наш також має Nginx-проксі, як рекомендується в документації AWS):

version: '3.7'  
services:  
 main:  
 image: quay.io/keycloak/keycloak:21.0.1  
 environment:  
 KC_HEALTH_ENABLED: "true"  
 KC_HOSTNAME_STRICT: "false"  
 KC_PROXY: edge  
 KC_LOG_LEVEL: info  
 KC_LOG_CONSOLE_OUTPUT: "json"  
 SENTRY_ERRORS_ONLY: "true"  
 env_file:  
 - .env  
 ports:  
 - "80:8080"  
 volumes:  
 - ./providers:/opt/keycloak/providers  
 command: start  
 labels:  
 com.datadoghq.ad.logs: '[{"source": "docker/keycloak", "service": "<>", "tags": ["env:<>", "instance-id:<>", "beanstalk-env-name:<>"]}]'  
 restart: on-failure

Ось деякі важливі моменти щодо цієї конфігурації:

  • Ми включили всі необхідні постачальники в zip-файл і змонтували їх як томи всередині нашого контейнера.
  • Логи нашого додатку виводяться у форматі JSON з мінімальним рівнем логів info. Ми також налаштували Datadog для збору логів з контейнера за допомогою мітки com.datadoghq.ad.logs (це працює лише тому, що ми маємо демона Datadog, що працює на всіх наших EC2 інстансах).
  • Включено логування помилок у Sentry, (див. постачальник sentry-keycloak).
  • Включено маршрути здоров'я Keycloak, (і ми налаштували Beanstalk для використання їх для перевірок стану).
  • Вказано, що наш додаток знаходиться за проксі відповідно до вказівок, наданих у документації Keycloak.
  • Для використання облікових даних, збережених у Secrets Manager, в Keycloak, ми додаємо файл .env, який обробляє необхідні змінні середовища.

Цей файл .env генерується через predeploy hook в Beanstalk, який отримує облікові дані з Secrets Manager за допомогою AWS CLI та форматує їх у файл .env.
Ось приклад такого скрипту:

# .platform/hooks/predeploy/01_augment_env.sh  

#!/bin/bash  
set -euxo pipefail  

db_credentials="$(aws secretsmanager get-secret-value --secret-id my/db/secret | jq -r .SecretString)"  
admin_credentials="$(aws secretsmanager get-secret-value --secret-id my/keycloak/secret | jq -r .SecretString)"  

touch .env  

{  
echo "KEYCLOAK_ADMIN=$(echo "$admin_credentials" | jq .username)"  
echo "KEYCLOAK_ADMIN_PASSWORD=$(echo "$admin_credentials" | jq .password)"  

echo "KC_DB=postgres"  
echo "KC_DB_USERNAME=$(echo "$db_credentials" | jq .username)"  
echo "KC_DB_PASSWORD=$(echo "$db_credentials" | jq .password)"  
echo "KC_DB_URL_HOST=$(echo "$db_credentials" | jq .host)"  
echo "KC_DB_URL_PORT=$(echo "$db_credentials" | jq .port)"  
echo "KC_DB_URL_DATABASE=$(echo "$db_credentials" | jq .dbname)"  
} >> .env

Тепер ми можемо завантажити ZIP-файл на Beanstalk, увійти за адміністративними обліковими даними і…

В нескінченність і далі!

Ми стикаємося з несподіваним циклом нескінченних редиректів і не можемо отримати доступ до консолі Keycloak. Також помічаємо в логах кілька виключень LOGIN_ERROR: invalid_grant.

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

Давайте допоможемо вам заощадити час, показавши вам, як ми проаналізували наші логи. Ми додали стовпець з ідентифікатором інстанса (пам'ятаєте? Він був доданий у Datadog через Docker мітку) до таблиці логів і, переглянувши їх, помітили дещо цікаве.

Чи помітили ви це?

Ми бачимо, що дві інстанси по черзі генерують помилку, і поведінка ідентична на обох.

Насправді, запити балансуються між двома інстансами. Keycloak видає новий токен на першій інстансі, зберігає його в кеші та виконує стандартний редирект OAuth2. Потім ми потрапляємо на іншу інстансу, яка не розпізнає токен і робить те саме, що призводить до нескінченного циклу.

Слово про кешування в Keycloak

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

Проте, замість Redis або подібних рішень, він побудований на основі технологій кешування Red Hat, зокрема Infinispan та JGroups.

Список різних кешів Keycloak з типами та описами

Infinispan — це розподілена пам'ятна грід-система, яка надає рішення для кешування. У кластері кожен вузол підтримує свій локальний кеш. Коли вузол потребує даних, яких немає в його локальному кеші, він може запитати їх у інших вузлів в кластері. Якщо вузол оновлює або інвалідовує дані у своєму локальному кеші, він може також поширити зміни на інші вузли в кластері, забезпечуючи консистентність даних.

Вузли використовують багатоточкову платформу JGroups для виявлення та комунікації один з одним у кластері. За замовчуванням вузли виявляються за допомогою IP multicast транспорту, заснованого на UDP, але ми можемо вибрати один з попередньо визначених стеків транспорту або визначити власний.

Однак згідно з документацією, AWS і більшість інших хмарних провайдерів, схоже, не підтримують UDP multicast, оскільки сервери зазвичай не розгортаються на одному комутаторі мережі, і це було б складно реалізувати.

Після прочитання документації JGroups, ми планували використати протокол JDBC_PING, який використовує спільну таблицю для виявлення вузлів. Це здавалося нам ідеальним варіантом, оскільки у нас уже є база даних Keycloak. Однак цей протокол не підтримується за замовчуванням в Keycloak, і нам так і не вдалося змусити його працювати.
Розчарування неймовірне, тож якщо вам коли-небудь вдасться це зробити, будь ласка, поділіться з нами!

Ми повернулися до S3_PING. Цей метод використовує AWS сховище для виявлення вузлів, дозволяючи кожній інстансі редагувати і читати маленький файл в бакеті для визначення інших учасників. Він також здається рекомендованим способом виявлення для Keycloak, розгорнутого в AWS.

Налаштування S3_PING

Нам потрібно буде зробити кілька додаткових налаштувань в нашій інфраструктурі AWS:

  • Спочатку нам потрібно створити S3 бакет для виявлення. Як згадувалося раніше, ми використовуємо Terraform, але ви можете зробити це через консоль AWS.
  • Також необхідно застосувати IAM політику для ролі, яка дозволяє EC2 інстансам доступ до певного S3 бакету. Для цього потрібно, щоб роль IAM була прикріплена до ваших інстансів, а також прикріплена така політика:
{  
 "Version": "2012-10-17",  
 "Statement": [  
 {  
 "Effect": "Allow",  
 "Action": "s3:ListBucket",  
 "Resource": "arn:aws:s3:::your-s3-bucket-name"  
 },  
 {  
 "Effect": "Allow",  
 "Action": [  
 "s3:PutObject",  
 "s3:GetObject",  
 "s3:DeleteObject"  
 ],  
 "Resource": "arn:aws:s3:::your-s3-bucket-name/*"  
 }  
 ]  
}
  • Потім потрібно дозволити інстансам обмінюватися даними між собою. Для цього необхідно налаштувати групи безпеки для інстансів, щоб дозволити вхідний та вихідний трафік на порт TCP 7800.

Тепер налаштуємо наш додаток:

  • Keycloak може обробляти бакет без додаткових налаштувань через Infinispan, якщо ми надамо:
    • Змінну середовища KC_CACHE_STACK=ec2,
    • Системні властивості JGroups jgroups.s3.region_name і jgroups.s3.bucket_name.
  • Однак нам потрібно буде додатково попрацювати через нашу контейнеризовану інфраструктуру.
    Коли ми запускаємо контейнер Docker, внутрішня IP-адреса контейнера і зовнішня IP-адреса хоста різні. За замовчуванням JGroups використовуватиме внутрішню IP-адресу контейнера для комунікації між інстансами Keycloak. Однак у нашому випадку інстанси Keycloak працюють на різних хостах, тому вони повинні використовувати зовнішні IP-адреси для комунікації.
    Щоб вирішити цю проблему, ми можемо встановити системну властивість jgroups.external_addr, яка вказує JGroups використовувати зазначену IP-адресу для міжвузлової комунікації, замість внутрішньої IP-адреси контейнера.
    Також нам потрібно буде відкрити порт 7800 на контейнері Docker, щоб JGroup був доступний ззовні.

Усе це можна зробити, редагуючи:

  • Файл docker-compose.yml:
KC_LOG_CONSOLE_OUTPUT: "json"  
 SENTRY_ERRORS_ONLY: "true"  
+ KC_CACHE_STACK: ec2  
 env_file:  
 - .env  
 ports:  
 - "80:8080"  
+ - "7800:7800"  
 volumes:
  • І скрипт predeploy:
admin_credentials="$(aws secretsmanager get-secret-value --secret-id "$INITIAL_ADMIN_CREDENTIALS_SECRET_NAME" | jq -r .SecretString)"  

+ ip_address=$(hostname --ip-address)  

 touch .env  

...

echo "KC_DB_URL_DATABASE=$(echo "$db_credentials" | jq .dbname)"  

+ echo "JAVA_OPTS_APPEND=\"-Djgroups.s3.region_name=your-s3-region-name -Djgroups.s3.bucket_name=your-s3-bucket-name -Djgroups.external_addr=$ip_address\""  
 echo "########### /augment_env.sh #########"  
 } >> .env

Ми використовуємо змінну середовища JAVA_OPTS_APPEND, яка зазвичай використовується для додавання додаткових параметрів Java або системних властивостей до командного рядка Java при запуску Java-додатка, і передаємо в неї IP-адресу інстансів.

І на завершення, нам потрібно завантажити та додати всі необхідні пакети в папку providers (як .jar файли):

Після виконання зазначених кроків, ви повинні побачити в логах, як Keycloak підключається до інших інстансів під час запуску сервера. Крім того, нові файли повинні з'явитись у вашому бакеті, і ви повинні змогти успішно увійти до майстер-реалму, використовуючи надані облікові дані 🎉.

Покращення секретів бази даних

Ми вже бачили раніше, що наші облікові дані для доступу до бази даних зберігаються в AWS Secrets Manager і отримуються у файл .env під час розгортання додатка.

Цей процес має два недоліки:

  • Секрети записуються у вигляді відкритого тексту на машині
  • Наші секрети періодично змінюються, і це призводить до простою, поки ми не зробимо нове розгортання сервісу. Ми не можемо дозволити собі цього.

Щоб вирішити ці проблеми, ми можемо оновити нашу конфігурацію, щоб отримувати ці облікові дані безпосередньо під час виконання.

Спочатку нам потрібно додати JDBC постачальника AWS Secret Manager і його залежності до списку постачальників:

Після цього необхідно оновити змінні середовища для бази даних:

echo "KC_DB_DRIVER=com.amazonaws.secretsmanager.sql.AWSSecretsManagerPostgreSQLDriver"  
echo "AWS_SECRET_JDBC_REGION=$(ec2-metadata --availability-zone | awk '{print $2}' | sed 's/[a-z]$//')"  


echo "KC_DB=jdbc-secretsmanager:postgresql"  
echo "KC_DB_USERNAME=my/db/secret"  
echo "KC_DB_URL_HOST=$(echo "$db_credentials" | jq .host)"  
echo "KC_DB_URL_PORT=$(echo "$db_credentials" | jq .port)"  
echo "KC_DB_URL_DATABASE=$(echo "$db_credentials" | jq .dbname)"  
echo "KC_TRANSACTION_XA_ENABLED=false"

Тепер секрет буде автоматично отриманий і кешований під час виконання, і замінений, якщо він буде змінений!

Дорога вперед

Інтеграція Keycloak в нашу інфраструктуру в Alan — це перший крок на глобальному шляху. Незважаючи на труднощі, ми віримо, що переваги міцного, централізованого рішення IAM, як Keycloak, варті всіх зусиль.

Ми рухаємося вперед і готуємося до наступної захоплюючої фази — інтеграції кастомної теми за допомогою React в Keycloak. Це дозволить нам створити більш динамічний і інтерактивний інтерфейс, зберігаючи при цьому надійну безпеку, яку надає Keycloak. Ця інтеграція обіцяє нові виклики та нові знання, і ми з нетерпінням чекаємо на можливість поділитися цим досвідом.

Сподіваємося, що цей посібник був корисним, і будемо раді будь-яким відгукам або запитанням щодо нашої інтеграції Keycloak.
Ваші ідеї можуть допомогти нам покращити наші впровадження, а ваші запитання можуть відкрити нові можливості для обговорення та інновацій. Ми з нетерпінням чекаємо на майбутнє та раді поділитися частинами нашої технічної подорожі з вами!

Перекладено з: Hosting Keycloak within a Beanstalk Infrastructure: A Technical Journey at Alan

Leave a Reply

Your email address will not be published. Required fields are marked *