Перейти к содержанию

Вход людей — Keycloak

Keycloak — identity provider людей для панели платформы: форма входа, пароли, роли realm и атрибут tenant_id, по которому platform-api определяет организацию пользователя. Статья описывает realm platform, его клиенты и мапперы, заведение пользователей и связь Keycloak с IAM. Для администраторов инсталляции.

Замороженный периметр

Keycloak входит в профиль compose platform, который заморожен (TAI-ADR-0034, TAI-ADR-0039). Агенты и операторский харнесс Keycloak не используют: их identity — Platform Access Token IAM.

Роль Keycloak в цепочке identity

Keycloak отвечает только на вопрос «кто этот человек». Решения о доступе к Control Plane и сервисам пакетов принимаются дальше по цепочке:

sequenceDiagram
    participant U as Браузер
    participant W as platform-web
    participant A as platform-api
    participant K as Keycloak
    participant I as iam-service
    participant CP as control-plane-api
    U->>W: e-mail и пароль (форма входа)
    W->>A: POST /api/v1/auth/login
    A->>K: password grant (клиент platform-api)
    K-->>A: access token (iss realm, aud platform-api + iam-service, tenant_id)
    A-->>W: access + refresh token
    W-->>U: httpOnly cookie
    U->>W: экран консоли
    W->>A: /api/v1/services/control-plane/... (Bearer Keycloak)
    A->>I: federation:exchange (token Keycloak → audience control-plane)
    I-->>A: RS256 access token IAM
    A->>CP: запрос с токеном IAM

Keycloak-токен проверяют двое:

Кто Что проверяет
platform-api подпись по JWKS realm, iss = ${KEYCLOAK_URL}/realms/platform, aud содержит platform-api, непустой tenant_id
iam-service (federation:exchange) подпись, iss и aud = iam-service по записи identity provider'а tenant'а

Развёртывание

Параметры контейнера

Сервис keycloak в compose.yml запускается командой start --import-realm со следующими настройками:

Переменная Значение Смысл
KC_HOSTNAME ${TAIMEN_PUBLIC_URL}/auth публичный адрес с префиксом; issuer realm = ${TAIMEN_PUBLIC_URL}/auth/realms/platform
KC_HTTP_RELATIVE_PATH /auth Keycloak обслуживает всё под /auth
KC_HOSTNAME_STRICT ${KEYCLOAK_HOSTNAME_STRICT:-true} локально по http выключают (false), в промышленной инсталляции — true
KC_PROXY_HEADERS xforwarded доверять X-Forwarded-* от Caddy
KC_HTTP_ENABLED true TLS терминирует Caddy
KC_HEALTH_ENABLED true healthcheck GET /auth/health/ready на management-порту 9000
KC_DB_URL jdbc:postgresql://platform-db:5432/keycloak отдельная база keycloak в platform-db
KC_BOOTSTRAP_ADMIN_USERNAME / _PASSWORD KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD администратор realm master

База keycloak и роль keycloak создаются init-скриптом deploy/staging/platform/postgres/init-keycloak-db.sh только при первой инициализации тома platform_db. Если том уже существовал без этой базы, создайте её вручную (CREATE ROLE keycloak …; CREATE DATABASE keycloak OWNER keycloak;).

Шаблон realm и его подстановки

Realm описан в git как шаблон deploy/staging/platform/keycloak/platform-realm.json. Keycloak при импорте не разворачивает переменные окружения, поэтому перед стартом одноразовый контейнер realm-render подставляет значения и кладёт результат в том realm_import:

Плейсхолдер Чем заменяется
__PLATFORM_API_CLIENT_SECRET__ KEYCLOAK_CLIENT_SECRET
__WEB_BASE_URL__ TAIMEN_PUBLIC_URL

В git остаётся только шаблон; секрет живёт в .env.

Импорт realm выполняется только при первом старте

--import-realm создаёт realm, если его ещё нет. Правка шаблона и перезапуск Keycloak на уже импортированный realm не действуют: живой realm хранится в базе keycloak. Изменения в существующем realm вносятся через Admin API или админ-консоль (примеры ниже). Смена KEYCLOAK_CLIENT_SECRET в .env тоже не меняет секрет клиента в живом realm — поменяйте его в Keycloak и в .env одновременно, иначе password grant platform-api перестанет работать.

Realm platform

Основные настройки

Параметр Значение
sslRequired external
loginWithEmailAllowed true (вход по e-mail)
duplicateEmailsAllowed false
registrationAllowed false — самостоятельной регистрации нет
resetPasswordAllowed true
rememberMe true
bruteForceProtected true
accessTokenLifespan 300 с
ssoSessionIdleTimeout 1800 с
ssoSessionMaxLifespan 36000 с
defaultSignatureAlgorithm RS256

Срок жизни access token (5 минут) определяет частоту silent refresh в веб-консоли; cookie с refresh token живёт 1800 с — столько же, сколько простой SSO-сессии (см. Веб-консоль).

Клиенты

Клиент Тип Потоки Назначение
platform-api confidential (секрет KEYCLOAK_CLIENT_SECRET) standard flow, direct access grants (password grant), service account вход по форме через platform-api; redirect ${TAIMEN_PUBLIC_URL}/platform/*
platform-web public, PKCE S256 standard flow (Authorization Code + PKCE) вход в браузере с редиректом; redirect ${TAIMEN_PUBLIC_URL}/* и http://localhost:3001/* (dev-сервер)
iam-service bearer-only — только audience: IAM принимает токены, адресованные ему

Уберите dev-адрес в промышленной инсталляции

Шаблон разрешает клиенту platform-web редиректы на http://localhost:3001/* — это адрес dev-сервера веб-консоли. В промышленном realm удалите его из Valid Redirect URIs, Web Origins и post-logout redirect URIs.

Мапперы

Оба клиента (platform-api и platform-web) несут одинаковый набор мапперов, поэтому токены, выпущенные любым из них, равнозначны для platform-api и IAM:

Маппер Тип Что попадает в токен
tenant_id oidc-usermodel-attribute-mapper claim tenant_id из атрибута пользователя tenant_id (access, id, userinfo, introspection)
platform-api-audience oidc-audience-mapper platform-api в aud access token
iam-service-audience oidc-audience-mapper iam-service в aud access token
groups oidc-group-membership-mapper claim groups (короткие имена групп)

Роли realm попадают в стандартный claim realm_access.roles; platform-api берёт роли пользователя именно оттуда.

Роли realm

Роль Где используется
org_admin администратор организации; видит модули консоли (с включённым флагом), может удалять чужие документы tenant'а
platform_admin администратор платформы; видит модули консоли
billing_admin биллинг панели
editor, member, viewer уровни доступа внутри панели

Роли realm управляют только панелью. Права в Control Plane задаёт binding IAM-principal'а человека в самом Control Plane (см. platform-api → Подключение человека).

User profile и атрибут tenant_id

Keycloak 24+ работает с декларативным user profile: атрибуты, не объявленные в профиле, молча отбрасываются. Без объявления tenant_id маппер кладёт в токен пустой claim, и platform-api отвечает 403 MISSING_TENANT_CLAIM.

Шаблон realm объявляет профиль через компонент org.keycloak.userprofile.UserProfileProvider:

Атрибут Правила
username длина 3–255
email формат e-mail, до 255; обязателен для роли user
firstName, lastName до 255, запрещённые символы имени
tenant_id ровно 36 символов (UUID); видят admin и user, редактирует только admin

unmanagedAttributePolicy: ADMIN_EDIT — необъявленные атрибуты может менять только администратор. Пользователь не может сам сменить себе tenant_id.

Если realm импортирован из старого шаблона без tenant_id в профиле, объявите атрибут через Admin API:

KC=https://platform.example.com/auth
ADMIN_TOKEN=$(curl -s "$KC/realms/master/protocol/openid-connect/token" \
  -d grant_type=password -d client_id=admin-cli \
  -d username=admin --data-urlencode "password=$KEYCLOAK_ADMIN_PASSWORD" | jq -r .access_token)

curl -s "$KC/admin/realms/platform/users/profile" -H "Authorization: Bearer $ADMIN_TOKEN" > profile.json
jq '.attributes += [{"name":"tenant_id","displayName":"Tenant ID","multivalued":false,
      "permissions":{"view":["admin","user"],"edit":["admin"]},
      "validations":{"length":{"min":36,"max":36}}}]
    | .unmanagedAttributePolicy = "ADMIN_EDIT"' profile.json > profile.new.json
curl -s -X PUT "$KC/admin/realms/platform/users/profile" \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
  --data @profile.new.json

Тот же шаг выполняет служебный скрипт deploy/staging/platform/keycloak-profile.py (идемпотентно, запускается внутри контейнера platform-api).

Пользователи

Пользователь панели существует в двух местах, и оба должны быть согласованы:

  1. Keycloak — учётная запись с паролем, ролями realm и атрибутом tenant_id.
  2. База platform-api — строка пользователя с external_id = sub из Keycloak и активным членством (memberships) в tenant'е с тем же id, что в атрибуте tenant_id.

Если записи в базе platform-api нет, любой запрос с валидным токеном получает 401 USER_NOT_FOUND; если пользователь деактивирован — 401 USER_DEACTIVATED.

Создание пользователя через Admin API

TENANT_ID=<platform-tenant-id>   # id tenant'а в базе platform-api

# 1. пользователь с заполненным профилем и атрибутом tenant_id
curl -s -X POST "$KC/admin/realms/platform/users" \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d '{
    "username": "alice",
    "email": "alice@example.com",
    "firstName": "Alice",
    "lastName": "Example",
    "enabled": true,
    "emailVerified": true,
    "attributes": {"tenant_id": ["'"$TENANT_ID"'"]},
    "credentials": [{"type": "password", "value": "<пароль>", "temporary": false}]
  }'

# 2. sub нового пользователя
USER_ID=$(curl -s "$KC/admin/realms/platform/users?username=alice&exact=true" \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq -r '.[0].id')

# 3. роли realm
ROLE=$(curl -s "$KC/admin/realms/platform/roles/org_admin" -H "Authorization: Bearer $ADMIN_TOKEN")
curl -s -X POST "$KC/admin/realms/platform/users/$USER_ID/role-mappings/realm" \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d "[$ROLE]"

Идемпотентный вариант того же (создание или обновление атрибутов, ролей и пароля по списку пользователей из окружения) — служебный скрипт deploy/staging/platform/keycloak-users.py. Он запускается внутри контейнера platform-api, пароли принимает только через переменные окружения и печатает лишь sub.

Запись пользователя в базе platform-api создаётся потоком приглашений (POST /api/v1/invites → POST /api/v1/invites/accept) либо сидом. Служебный сид deploy/staging/platform/seed_platform.py идемпотентно создаёт tenant, организацию, команду, пользователя с привязкой к sub и членство с ролью (по умолчанию org_admin):

docker compose cp deploy/staging/platform/seed_platform.py platform-api:/app/seed_platform.py
docker compose exec \
  -e SEED_TENANT_ID=<platform-tenant-id> -e SEED_TENANT_SLUG=acme -e SEED_TENANT_NAME="Acme" \
  -e SEED_KEYCLOAK_SUB="$USER_ID" -e SEED_USER_EMAIL=alice@example.com \
  platform-api python /app/seed_platform.py

Поток приглашений в поставке compose

В compose.yml platform-api настроен с EMAIL_TRANSPORT: log: письма приглашений не отправляются, а пишутся в журнал контейнера. Для реальной рассылки настройте почтовый транспорт platform-core.

Password grant и «Account is not fully set up»

Форма входа консоли работает через password grant: platform-web отправляет e-mail и пароль в POST /api/v1/auth/login, platform-api выполняет grant клиентом platform-api. Keycloak отказывает в password grant, если у учётной записи остались незавершённые required actions — ответ invalid_grant с описанием Account is not fully set up. Типичные причины:

Причина Required action Как исправить
Пароль задан как временный (temporary: true) UPDATE_PASSWORD задать пароль с temporary: false
Не заполнены обязательные поля профиля (firstName, lastName, email) UPDATE_PROFILE заполнить поля через Admin API
Не подтверждён e-mail при включённой верификации VERIFY_EMAIL emailVerified: true
Required action назначена вручную любая снять в карточке пользователя

В консоли это выглядит как обычный неверный пароль: platform-api сводит любую ошибку grant к 401 INVALID_CREDENTIALS («Invalid email or password»). Проверить истинную причину можно прямым запросом к Keycloak:

curl -s "$KC/realms/platform/protocol/openid-connect/token" \
  -d grant_type=password -d client_id=platform-api \
  --data-urlencode "client_secret=$KEYCLOAK_CLIENT_SECRET" \
  -d username=alice@example.com --data-urlencode "password=<пароль>" | jq .
# {"error":"invalid_grant","error_description":"Account is not fully set up"}

Заводите пользователей сразу «готовыми»

Временный пароль удобен в интерактивном входе через страницу Keycloak, но форма консоли его не проходит. Создавайте пользователей с temporary: false, заполненными firstName/lastName/email и emailVerified: true; смену пароля пользователь выполняет в профиле.

Связь с IAM

Чтобы platform-api мог обменивать токены Keycloak в IAM, realm регистрируется в tenant'е IAM как identity provider. make bootstrap этого не делает — шаг выполняется один раз bootstrap-токеном IAM:

IAM=https://platform.example.com/iam
curl -s -X POST "$IAM/api/v1/tenants/$IAM_TENANT_ID/identity-providers" \
  -H "X-IAM-Bootstrap-Token: $IAM_BOOTSTRAP_TOKEN" -H "Content-Type: application/json" \
  -d '{
    "key": "keycloak",
    "issuer": "https://platform.example.com/auth/realms/platform",
    "audience": "iam-service",
    "jwksUri": "https://platform.example.com/auth/realms/platform/protocol/openid-connect/certs",
    "lifecycleProfile": "managed"
  }'
Поле Значение Почему
key keycloak совпадает с SERVICE_GATEWAY_IDENTITY_PROVIDER platform-api
issuer issuer realm ровно тот iss, что в токенах Keycloak
audience iam-service токены несут его благодаря маппёру iam-service-audience
subjectClaim, externalIdClaim по умолчанию sub стабильный id пользователя Keycloak
groupClaim по умолчанию groups маппер groups
lifecycleProfile managed позволяет заранее привязать внешнюю identity к существующему principal'у; при read_only ручная привязка отвечает 409 identity_provider_managed

Подробности обмена, проекции групп и снимков authentication context — в статье Федерация identity.

Эксплуатация

Изменения в живом realm

Все изменения после первого импорта — через Admin API (/auth/admin/realms/platform/…) или админ-консоль https://platform.example.com/auth/admin/. Служебные скрипты в deploy/staging/platform/ (keycloak-profile.py, keycloak-mapper.py, keycloak-users.py) идемпотентны и работают по внутреннему адресу http://keycloak:8080/auth изнутри контейнера platform-api. Если меняете realm руками, отразите изменение в шаблоне platform-realm.json, чтобы новая инсталляция получила его при импорте.

Админ-консоль доступна снаружи

Раскладка Caddy проксирует весь /auth/*, включая /auth/admin/. В промышленной инсталляции ограничьте доступ к /auth/admin/* на внешнем контуре (allow-list адресов или отдельный внутренний вход) и используйте стойкий KEYCLOAK_ADMIN_PASSWORD.

Ротация секрета клиента platform-api

  1. Сгенерировать новый секрет в Keycloak (Clients → platform-api → Credentials → Regenerate) или задать через Admin API.
  2. Записать его в .env как KEYCLOAK_CLIENT_SECRET.
  3. Пересоздать platform-api, чтобы он взял новое значение: docker compose --profile platform up -d platform-api.

Между шагами 1 и 3 вход по форме не работает — выполняйте в окно обслуживания.

Резервное копирование

Состояние Keycloak (пользователи, пароли, сессии, живой realm) — в базе keycloak кластера platform-db. Бэкап platform-db целиком сохраняет и данные панели, и Keycloak (см. Резервное копирование).

Типичные проблемы

Симптом Причина Что делать
403 MISSING_TENANT_CLAIM на любой запрос атрибут tenant_id не объявлен в user profile или не заполнен у пользователя объявить атрибут, заполнить значение
401 INVALID_CREDENTIALS при верном пароле «Account is not fully set up» см. Password grant
401 USER_NOT_FOUND после успешного входа в Keycloak нет записи пользователя в базе platform-api сид или приглашение
401 INVALID_TOKEN с ошибкой issuer KEYCLOAK_URL platform-api не совпадает с публичным адресом Keycloak проверить TAIMEN_PUBLIC_URL, KC_HOSTNAME и hairpin
Правка шаблона realm не применилась realm импортируется только при первом старте Admin API
Keycloak не стартует, ошибка подключения к БД база keycloak не создана (том существовал до init-скрипта) создать роль и базу вручную

Больше сценариев — в Диагностика панели и Keycloak.

См. также