Фото: iMattSmart на Unsplash
Створення безпечних додатків є пріоритетом для будь-якої організації. У Alan ми використовували власне рішення для аутентифікації, яке ефективно задовольняло наші потреби. Однак, з ускладненням наших вимог, ми зрозуміли, що потрібно більш комплексне рішення. Після дослідження різних варіантів, ми вирішили інтегрувати Keycloak для управління аутентифікацією та авторизацією для наших додатків.
Незважаючи на численні переваги, розгортання Keycloak на нашій існуючій інфраструктурі, яка використовує AWS Elastic Beanstalk і Docker контейнери, мало свої виклики. З обмеженою документацією на цю тему, інтеграція виявилася складним завданням.
У цій статті я спробую провести вас через наш процес розгортання Keycloak на інфраструктурі Alan, труднощі, з якими ми зіткнулися, та рішення, які ми знайшли. Сподіваюся, ця стаття допоможе іншим, хто стикається з подібними проблемами і потребує допомоги у підключенні Keycloak до платформи AWS Elastic Beanstalk.
Key... що?
Ця стаття не буде занурюватися в деталі Keycloak, але ми коротко його представимо.
Keycloak — це відкрите рішення для управління ідентифікацією та доступом (Identity and Access Management, IAM), яке надає централізовану аутентифікацію та авторизацію для веб- і мобільних додатків. Підтримка стандартних протоколів, таких як OAuth 2.0, OpenID Connect та SAML, а також різноманітні варіанти аутентифікації, як-от аутентифікація за допомогою пароля, соціальний логін через Google або Facebook, та багатофакторна аутентифікація через SMS або електронну пошту, роблять його універсальним вибором.
У Alan ми приділяємо велику увагу дизайну та забезпеченню безперебійного досвіду для користувачів. Ми також серйозно ставимося до безпеки і впровадили заходи для забезпечення кінцевого шифрування, зокрема передхешування паролів користувачів у браузері. Повністю налаштовувані форми входу та реєстрації в Keycloak зробили його ідеальним вибором для наших потреб.
Форма входу 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
.
Веб-інтерфейс адміністратора Keycloak
Розгортання Keycloak у продакшн
Наше рішення полягає в розгортанні Keycloak на AWS Elastic Beanstalk.
Ми використаємо архів zip, який містить наш файл Docker Compose, усі файли для спільного використання з контейнером через томи, а також потенційні файли конфігурації для AWS.
Далі ми завантажуємо цей архів на Elastic Beanstalk за допомогою AWS Management Console або CLI.
Ми не будемо вдаватися в деталі (більше інформації можна знайти в спеціалізованих статтях на нашому блозі), але надамо деякий контекст щодо інфраструктури Alan:
- додатки розгортаються через Elastic Beanstalk,
- сервіси визначені через Docker Compose файли,
- чутливі дані керуються через Secrets Manager,
- все керується за допомогою Terraform.
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 і його залежності до списку постачальників:
- aws-secretsmanager-jdbc
- jackson-core
- jackson-databind
- aws-java-sdk-secretsmanager
- aws-secretsmanager-caching-java
Після цього необхідно оновити змінні середовища для бази даних:
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