Вход людей — 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).
Пользователи¶
Пользователь панели существует в двух местах, и оба должны быть согласованы:
- Keycloak — учётная запись с паролем, ролями realm и атрибутом
tenant_id. - База 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¶
- Сгенерировать новый секрет в Keycloak (Clients →
platform-api→ Credentials → Regenerate) или задать через Admin API. - Записать его в
.envкакKEYCLOAK_CLIENT_SECRET. - Пересоздать
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.