# module-08. Проектная спецификация `keycloak` > Статус: целевая production-спецификация OTP; mock действует до controlled rollout, real mode интегрируется только через `sms-service` по [`module-11-idgtl-sms.md`](module-11-idgtl-sms.md). > Источники: [`README.md`](README.md), [`arch-00-glossary.md`](../../architectory/arch-00-glossary.md), [`arch-01-system-architecture.md`](../../architectory/arch-01-system-architecture.md), [`arch-02-api-contracts.md`](../../architectory/arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](../../architectory/arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](../../architectory/arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](../../architectory/arch-05-agent-development-process.md), [`arch-06-service-hosting-security.md`](../../architectory/arch-06-service-hosting-security.md), [`module-01-api-backend.md`](module-01-api-backend.md), [`module-02-frontend-test-site.md`](module-02-frontend-test-site.md), [`module-03-nginx-vm1.md`](module-03-nginx-vm1.md). ## 1. Назначение и границы Keycloak — единственный IdP HAN Chat. MVP предоставляет регистрацию/вход только по подтверждённому номеру телефона и OTP, OIDC tokens, refresh/logout, discovery/JWKS и защиту auth flow. Keycloak отвечает за: - realm, users, credentials, auth sessions и token lifecycle; - Authorization Code Flow with PKCE для Expo web/iOS/Android; - нормализацию/уникальность телефона и claims; - OTP authenticator/SPI, генерацию и локальную проверку OTP, challenge lifecycle, продуктовые limits и verify audit; - brute-force, sessions, logout/revocation; - keys/JWKS rotation и health/metrics. Не отвечает за: - `api-backend` bootstrap/consents/UserIdentity; - App DB/profile/chat и CRM sync; - API service-to-service tokens; - пользовательскую UX-сессию; - шаблоны, отправку и provider delivery journal (это `sms-service`); - прямой вызов i-Digital Direct и обработку delivery callback. Mock code является секретом окружения, не контентом UI и не логируется. В real mode Keycloak вызывает только закрытый durable-order API `sms-service`; API Direct Verifier не используется. ## 2. Топология и публичный URL Keycloak работает за единственным root nginx: ```text Client HTTPS https://tohin.ru/auth/* → nginx TLS termination → HTTP keycloak:8080 в закрытой Docker network → managed PostgreSQL schema keycloak по TLS ``` Публичный issuer обязан быть стабильным: ```text https://tohin.ru/auth/realms/han-chat ``` OIDC discovery: ```text https://tohin.ru/auth/realms/han-chat/.well-known/openid-configuration ``` JWKS — URI из discovery. `api-backend` проверяет `iss`, audience, signature, `exp/nbf` и `sub`, не вызывает Admin API в hot path. ## 3. Версия, image и providers - Keycloak Quarkus distribution, поддерживаемая LTS/stable версия, закреплённая image digest. - PostgreSQL JDBC driver из image. - Custom Java provider JAR для phone OTP authenticator/settings bridge/counters. - Сборка provider reproducible, зависимости pinned, SBOM/signature/security scan. - Build-stage выполняет `kc.sh build`; runtime image immutable/non-root. - Перед upgrade читаются Keycloak migration notes и SPI compatibility. Версия Keycloak фиксируется в deployment manifest; `latest` запрещён. ## 4. Realm и clients Realm: `han-chat`. Master realm не используется приложением. ### 4.1. Public frontend client Канонический client id: ```text han-chat-frontend ``` Настройки: - public client; client authentication off; - standard flow on; - Authorization Code + PKCE `S256` обязательно; - implicit flow off; - direct access grants/password grant off; - service accounts off; - device flow off, если не нужен; - consent screen Keycloak не заменяет продуктовые согласия API; - exact redirect URIs и web origins; - full scope allowed off; только назначенные scopes/mappers. Примеры redirect URI должны перечисляться отдельно: ```text https://tohin.ru/auth/callback han-chat://auth/callback ``` Production redirect URI задаются exact; wildcard не используется до отдельного security review. Development localhost origins/redirects находятся в отдельном dev realm/client либо profile и запрещены production. ### 4.2. API audience Audience: ```text han-chat-api ``` Client scope/audience mapper добавляет `aud=han-chat-api` в access token frontend. `api-backend` не принимает token только по `azp` без audience. ### 4.3. Optional confidential client `han-chat-backend` можно импортировать disabled/optional для будущих admin/ops S2S: - client authentication on, service account only при явном включении; - secret не хранится в realm export; - минимальные roles; - не используется между текущими сервисами и не требуется для JWT validation; - не участвует в пользовательском hot path. Internal API по-прежнему используют service tokens из arch-02. ## 5. OTP-only phone flow ### 5.1. Browser flow Отдельный flow `han-phone-otp-browser`: 1. Cookie/SSO authenticator проверяет действующую Keycloak session. 2. При отсутствии session показывается форма телефона. 3. `Phone Identity Authenticator` нормализует номер. 4. Проверяются realm brute-force и product send limits. 5. Создаётся/находится user по canonical phone identity. 6. `Phone OTP Challenge` создаёт `ordering`: mock активирует его локально, real mode заказывает SMS через `sms-service`. 7. Показывается форма OTP. 8. Проверяются только локальные status/TTL/attempt limits и constant-time HMAC/mock compare; provider status не читается. 9. При успехе user enabled/phone verified, flow завершается code. 10. Frontend меняет code+verifier на tokens. Password form, registration password, reset password, email OTP, social login и magic link отсутствуют. ### 5.2. Регистрация/find-or-create До выдачи OTP новый user может существовать как short-lived pending identity либо создаваться после успешной проверки. Предпочтительное решение: - normalized phone reservation/counter создаётся в SPI store; - permanent Keycloak user создаётся/активируется только после успешного OTP; - concurrent flow защищён unique phone index/transaction; - abandoned pending challenges очищаются TTL. Если Keycloak storage не позволяет безопасный custom unique index в managed schema, user создаётся disabled с deterministic username и очищается job; точная реализация покрывается concurrency tests. ### 5.3. Required actions Используются только при реальной необходимости: - `VERIFY_PHONE` — если user импортирован/номер изменён вне текущего verified flow; - `UPDATE_PHONE` — будущий controlled flow с повторной OTP; - terms/product consents не required action: версии и факт согласия хранит `api-backend`. Required actions не должны предлагать пароль/email. После phone OTP обычный вход завершается без лишнего profile screen. ## 6. Нормализация и уникальная identity Телефон парсится libphonenumber: - Unicode digits/NFKC input normalization; - default region `RU` допустим только для национального ввода; международные номера поддерживаются по product policy; - canonical storage/claim — E.164, например `+79001234567`; - invalid/impossible number отклоняется до send; - отображение только masked; - canonical phone comparison exact. Рекомендуемая модель: - `username` = canonical E.164 либо irreversible deterministic identifier; - user attribute `phone_number` = E.164; - `phone_number_verified=true`; - unique phone enforced storage-level, не только pre-check; - email nullable/не используется. Утечка существования номера запрещена: initiate/challenge возвращают одинаковый внешний текст/timing class для нового/существующего пользователя. Один verified phone соответствует одному active `sub`. Merge/reassignment — отдельная administrative policy, не автоматический side effect login. Изменение телефона требует re-auth + OTP нового номера и invalidation sessions/tokens по policy. `api-backend` обновляет cached phone при следующем bootstrap/login claim; прямого вызова Keycloak DB нет. ## 7. Mock и real delivery mode Env: ```text KEYCLOAK_OTP_MOCK_ENABLED=true KEYCLOAK_OTP_MOCK_CODE= ``` Правила: - mock временно разрешён до controlled SMS rollout только как явно принятый риск; - пустой/default `1234` запрещён startup policy для production-like, если не согласован secret; - code не входит в realm import, frontend config, HTML hint, API response, logs, metrics, traces или audit; - сравнение constant-time; - challenge всё равно имеет TTL, max verify attempts и counters, чтобы flow был близок production; - code не сохраняется per-user в открытом виде; - UI сообщает только «тестовый режим», без кода; - `KEYCLOAK_OTP_MOCK_ENABLED=false` при недоступном/неконфигурированном `sms-service` завершает новый order generic unavailable; уже active challenges продолжают локальный verify до TTL. Реальный delivery interface: ```java interface OtpDeliveryProvider { SmsOrderResult order(E164Phone phone, String otp, Duration ttl, String challengeId); } ``` Реализация real mode — `SmsOrderClient` к `POST /internal/sms/v1/send`. Успех — только `200/202` с `sms_message_id`; один HTTP retry использует тот же challenge и `idempotency_key=keycloak:challenge:{challenge_id}`. Keycloak не получает `provider_message_id`, template/sender/status/callback и не хранит vendor credentials. ## 8. OTP challenge и counters Даже в mock: - challenge id random ≥128 bit; - OTP не хранится raw; production-generated code — keyed hash/HMAC с challenge salt/pepper; - TTL — snapshot `app_settings["otp.phone.ttl_seconds"]`, диапазон `60..900`, кратен 60; real mode считается от `ordered_at`; - one-time use; success atomically consumes challenge; - max verification attempts per challenge; - resend всегда переводит предыдущий `active`/`ordering` challenge в `superseded`; - replay/parallel verify безопасны; - destination stored masked/hash where possible. Audit хранит `sms_message_id` (nullable для mock/order_failed), `ordered_at`, destination masked/HMAC, attempts, outcome и device context. Provider send/delivery status и полный SMS journal в schema `keycloak` запрещены. ### 8.1. Product send limits bridge SPI вызывает: ```text GET http://api-backend:8000/internal/settings/v1/otp Authorization: Bearer ${KEYCLOAK_SETTINGS_BRIDGE_TOKEN} ``` Ответ: ```json { "max_send_attempts_per_24h": 3, "min_seconds_between_attempts": 30, "max_verify_attempts": 5, "code_length": 6, "ttl_seconds": 60, "sms_order_timeout_ms": 3000, "version": "2026-07-10T08:00:00Z", "cache_ttl_seconds": 60 } ``` Это единственный путь к `otp.phone.*`; Keycloak не получает GRANT на `han_app`. SPI поддерживает ETag/cache, single-flight refresh. Bridge down: - использовать last-known-good до bounded max stale; - если cache пуст/слишком стар — fail-closed для send; - verify уже выданного challenge может продолжаться по snapshot, с которым challenge создан. Token name точно `KEYCLOAK_SETTINGS_BRIDGE_TOKEN`, endpoint точно `/internal/settings/v1/otp`. ### 8.2. Где хранятся counters Решение MVP: counters/challenges хранятся в Keycloak-owned PostgreSQL tables/provider storage в схеме `keycloak`, а не в Redis API и не в `han_app`. Причины: - durable across restart; - одна transaction для reserve/send-attempt/consume; - не добавляет Keycloak credentials к общему Redis; - соответствует границе «счётчики в зоне Keycloak/SPI». Используются phone HMAC, не E.164 в key/index для rate data. Tables provider-owned создаются versioned migration provider-а, не ручным DDL-on-start. Минимальные records: - `han_otp_challenge`: id, phone_hmac, destination_masked, otp_hash, `sms_message_id`, `delivery_mode`, `challenge_status`, `ordered_at`, `expires_at`, `otp_ttl_sec`, `otp_code_length`, verify attempts, settings version; - `han_otp_send_counter`: phone_hmac, window_start, count, last_sent_at; - `han_otp_security_event`: append-only событие на каждую send/verify попытку, `sms_message_id`, outcome/details и device context. Indexes: unique active challenge policy, `(phone_hmac,window_start)`, `(challenge_status,expires_at)`, partial `sms_message_id` и event `sms_message_id`. Periodic expiry переводит active в `expired`; автоматическое удаление SMS journal выполняться здесь не может. Доступ только `keycloak_user`. ### 8.3. Lifecycle и границы транзакций 1. После limits/counter reservation прежние `active`/`ordering` становятся `superseded`; создаётся новый `ordering` с crypto-random numeric OTP и immutable settings snapshot. 2. В real mode HTTP order выполняется вне transaction с DB locks. Потерянный ответ повторяется с тем же challenge/idempotency key, без нового OTP/counter. 3. `200/202` + `sms_message_id` → короткая transaction устанавливает `ordered_at`, `expires_at=ordered_at+otp_ttl_sec`, status `active` и event `otp_send/ordered`. 4. Невозможность durable order → `order_failed`; прежний challenge не восстанавливается. В mock mode challenge сразу `active`, `sms_message_id=null`. 5. Verify разрешён только для `active`: success → `consumed`, неверный код увеличивает attempts/event, лимит → `limited`, TTL → `expired`. Никакой переход не зависит от Direct `send_status`/`delivery_status`. Миграция существующих mock rows: дождаться прежнего max TTL либо истечь незавершённые challenges; установить `delivery_mode=mock`, `sms_message_id=null`, `ordered_at=created_at`, consumed rows → `consumed`, остальные → `expired`, backfill TTL/length текущими seed. Прежние `provider_id`/`provider_status` сначала nullable/неиспользуемые и удаляются только отдельной backward-incompatible migration после стабилизации. ### 8.4. Device context и verify events `han_otp_security_event` содержит `client_ip`, `user_agent`, `device_id`, `fingerprint`, `os_name`, `os_version`, `platform`, `app_version`; `sms_message_id` копируется для корреляции. Событие `otp_verify` пишется на каждую попытку с outcome `success|failure|limited|expired|already_used`. Frontend передаёт необязательные `han_device_id`, `han_fingerprint`, `han_platform`, `han_os_name`, `han_os_version`, `han_app_version` в OIDC request/hidden fields. Значения недоверенные audit metadata: id/fingerprint ≤256, OS/app ≤64, platform только `web|ios|android`, control characters запрещены. IP берётся только из trusted nginx chain, UA — из текущего запроса. Query/form/OTP/device identifiers редактируются в access logs. ## 9. Brute-force и abuse Слои: 1. nginx `/auth` IP rate limit (`NGINX_RATE_LIMIT_AUTH`); 2. Keycloak realm brute-force detection; 3. SPI product send limits per phone HMAC; 4. verify-attempt limit per challenge/phone/IP hash; 5. cooldown after repeated failures; 6. невидимая Yandex SmartCaptcha перед каждым первичным и повторным заказом OTP SMS. Realm включает brute-force protection с temporary lockout и bounded wait. Permanent lockout для consumer phone login без recovery runbook нежелателен. Error messages не различают unknown phone/wrong code/locked account сверх безопасной UX причины. `Retry-After`/remaining time выдаётся только если не помогает enumeration. IP берётся только из trusted proxy chain; Keycloak настроен доверять forwarded headers от root nginx. SmartCaptcha включается только через `KEYCLOAK_YANDEX_CAPTCHA_ENABLED`; client/server keys обязательны при `true`. Одноразовый token проверяется server-side до `OtpFlow.start()`/counter reservation. `status=failed`, отсутствующий token и non-temporary HTTP 4xx блокируют SMS; timeout, I/O, HTTP 408/429/5xx и malformed response работают fail-open с безопасным логом. Сложность и traffic rules принадлежат одной CAPTCHA в Yandex Cloud. CSP с доменами SmartCaptcha задаётся точечно в nginx только для login endpoints; custom realm CSP запрещён из-за риска поломки Admin Console/`3p-cookies`. ## 10. Claims и token contract Access token минимум: | Claim | Значение | |---|---| | `iss` | `https://tohin.ru/auth/realms/han-chat` | | `sub` | immutable Keycloak user id | | `aud` | включает `han-chat-api` | | `azp` | `han-chat-frontend` | | `exp`, `iat`, `nbf` | стандартные | | `sid` | session id, если поддерживается | | `auth_time` | время auth | | `acr`/`amr` | отражает phone OTP | | `phone_number` | canonical E.164 | | `phone_number_verified` | `true` | | `scope` | only allowed scopes | `preferred_username` может совпадать с phone для compatibility, но канонический claim API — `phone_number`; module-01 допускает fallback только если E.164. ID token предназначен client login state; API принимает access token, не ID token. Refresh token непрозрачен для приложения и хранится frontend secure storage. PII minimization: full phone нужен API bootstrap по зафиксированному контракту, но не добавляется в service tokens/metrics/logs. Roles/groups выдаются только если используются authorization policy. ## 11. Signing keys, JWKS и rotation - asymmetric signing, RS256 MVP; `none`/HS algorithms запрещены; - active signing key + passive previous keys до истечения всех выпущенных tokens/grace; - keys генерируются/хранятся Keycloak, private material не в realm export/repo; - JWKS публичен через issuer; - rotation rehearsed; `kid` меняется, API controlled-refresh cache; - emergency compromise: disable key, revoke sessions, force re-login, alert/runbook; - backup/restore учитывает realm keys. Rotation interval и HSM/keystore — ops TBD. Изменение algorithm требует совместного rollout API verifier. ## 12. Token и session lifecycle Предлагаемые MVP значения, окончательно принять security/product review: - access token lifespan: 5 минут; - SSO session idle: 30 дней; - SSO session max: 90 дней; - refresh token следует session limits; - authorization code: 1 минута; - login action: 5 минут; - client session idle/max согласованы с SSO; - clock skew минимальный. Refresh: - revoke refresh token on use / refresh token rotation включены; - max reuse `0` или минимально поддерживаемое значение; - frontend применяет single-flight, поэтому parallel refresh не требуется; - reuse старого refresh token → `invalid_grant`, возможная session revocation/security event; - offline tokens не выдаются. Access token не хранится server-side и живёт до exp; критическая блокировка пользователя сопровождается logout/revocation/not-before policy. ## 13. Logout, revocation и browser cookies Frontend вызывает OIDC end-session/logout с valid post-logout redirect, затем всегда очищает local tokens. Back-channel logout можно включить для clients, которые его поддержат; API JWT hot path не хранит browser session. Cookies Keycloak: - `Secure`, `HttpOnly`; - SameSite согласно redirect/iframe requirements, по умолчанию `Lax`; - domain/path минимальны (`/auth`/host); - third-party cookie dependency не закладывается; - session fixation предотвращается Keycloak; - admin console cookies не расширяются на frontend origins. Front-channel iframe checks не должны заставлять ослабить CSP всего сайта. Native logout использует system browser и app-link/custom scheme validation. ## 14. CORS, origins и redirects - exact `Web Origins`: `https://tohin.ru`; - no wildcard `*` with credentials; - native apps не получают произвольные web origins; - valid redirects exact/safely scoped; - redirect URI comparison не допускает open redirect; - post-logout redirects отдельно allow-listed; - nginx и Keycloak CORS не должны дублировать противоречащие headers; - token endpoint используется PKCE client без client secret; - admin endpoints не CORS-доступны приложению. Любой новый environment имеет отдельный host/client config, а не production wildcard. ## 15. Reverse proxy и hostname Ключевые настройки (точные CLI names проверяются по закреплённой версии): ```text KC_HTTP_ENABLED=true KC_HTTP_PORT=8080 KC_PROXY_HEADERS=xforwarded KC_HOSTNAME=https://tohin.ru/auth KC_HTTP_RELATIVE_PATH=/auth KC_HOSTNAME_STRICT=true KC_HOSTNAME_STRICT_HTTPS=true ``` Если выбран другой поддержанный pattern (`hostname` без path + relative path), итоговые issuer/endpoints обязаны совпасть с `KEYCLOAK_PUBLIC_URL`. Nginx передаёт trusted `Host`, `X-Forwarded-Proto=https`, `X-Forwarded-Host`, `X-Forwarded-Port=443`, real IP. Keycloak не доступен напрямую с host/public network, поэтому spoofed forwarded headers не принимаются извне. Admin hostname/path рекомендуется ограничить ops network/VPN; публично нужны только realm/OIDC/login assets. Если разделить admin hostname невозможно MVP, admin console защищается network allow-list и сильным admin auth. ## 16. PostgreSQL `keycloak` Используется: ```text KEYCLOAK_DB_URL=jdbc:postgresql://.../han_chat?...¤tSchema=keycloak KC_DB_URL_PROPERTIES=currentSchema=keycloak ``` Role `keycloak_user` имеет доступ только к schema `keycloak`; нет доступа `han_app`, `bitrix_*`, `message_safety`. Connection только private VPC + TLS verify. Pool: - bounded initial/min/max; - acquisition/query/connect timeout; - leak detection/metrics; - pool max определяется load test и managed PG limit; - `application_name=keycloak`. Keycloak управляет своей стандартной schema migration. Custom provider tables имеют отдельную versioned migration strategy, совместимую с startup/rolling upgrade; DDL не выполняется бесконтрольно каждым replica. Нельзя редактировать стандартные Keycloak tables вручную или Alembic-миграциями Python-сервисов. ## 17. Admin bootstrap и realm import ### 17.1. Bootstrap admin - `KC_BOOTSTRAP_ADMIN_USERNAME`/password или актуальный bootstrap mechanism только на первом запуске; - password генерируется strong secret, не коммитится и после bootstrap ротируется/удаляется из runtime env; - admin user не используется приложением; - отдельные named admin accounts/least privilege для ops; - MFA для admin обязательно до production, независимо от consumer phone flow; - admin events audit включён. ### 17.2. Realm import Репозиторий: ```text keycloak/ realm/han-chat-realm.json.template providers/han-phone-otp-provider.jar themes/han-phone/ migrations/ scripts/{render-realm,validate-realm,export-realm}.sh tests/ Dockerfile docker-compose.yml ``` Export/template содержит realm/client/flow/scopes/policies, но не: - client/admin/provider secrets; - mock code; - private signing keys; - environment-specific production credentials. Import автоматически допустим для clean local/test. Production changes применяются controlled declarative job/Admin API procedure с diff/backup, не `--import-realm` поверх живого realm без проверки. Drift detection сравнивает безопасный desired subset. ## 18. Settings и secrets Канонические: ```text KEYCLOAK_PUBLIC_URL=https://tohin.ru/auth KEYCLOAK_INTERNAL_URL=http://keycloak:8080 KEYCLOAK_REALM=han-chat KEYCLOAK_AUDIENCE=han-chat-api KEYCLOAK_DB_URL=jdbc:postgresql://... KC_DB_URL_PROPERTIES=currentSchema=keycloak KEYCLOAK_OTP_MOCK_ENABLED=true KEYCLOAK_OTP_MOCK_CODE= KEYCLOAK_SETTINGS_BRIDGE_TOKEN= KEYCLOAK_SMS_SERVICE_URL=http://sms-service:8080 KEYCLOAK_SMS_SERVICE_TOKEN= OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 ``` Дополнительные Keycloak-standard env version-specific (`KC_DB`, hostname/proxy/health/metrics/pool) фиксируются в `.env.example` после выбора image. Секреты только root `.env`/secret mounts с минимальными permissions. Все изменяемые OTP-параметры, включая limits, длину кода, TTL и timeout durable SMS order, не дублируются в env и поступают через settings bridge: ```text otp.phone.code_length otp.phone.ttl_seconds otp.phone.sms_order_timeout_ms ``` Challenge сохраняет snapshot этих значений и `settings_version`; изменение настроек влияет только на новые challenges. В env остаются только secret/bootstrap-параметры: ```text KEYCLOAK_OTP_SETTINGS_MAX_STALE_SEC=300 KEYCLOAK_OTP_HMAC_KEY= ``` ## 19. Health, readiness и startup Keycloak management health endpoints включены. Compose проверяет liveness/startup; readiness требует: - server started; - DB reachable/schema migration complete; - realm/client/auth flow/provider loaded; - active signing key; - settings bridge last-known-good для OTP send; - mock enabled с valid secret либо real-mode `sms-service` URL/token configured. Общая readiness Keycloak не зависит от Direct/provider status; недоступность `sms-service` отражается отдельным degraded dependency indicator и блокирует только новый real order. Стандартный Keycloak health сам не знает business provider state; custom provider readiness check/sidecar/synthetic internal check дополняет его. Public synthetic проверяет discovery/JWKS и authorization endpoint без отправки OTP. DB/settings failure не должен приводить к выдаче tokens без OTP. OTEL/metrics outage не блокирует login. ## 20. Logging, metrics, tracing и audit ### Logs JSON/stdout: - service/version/environment, event/category; - request/trace id, realm/client, safe flow step; - result/error code, duration; - phone только HMAC/masked при необходимости. Запрещены raw OTP/mock code, phone, access/refresh/code, cookies, Authorization, client/admin secret, password, form body, redirect query с `code`, DB URL. Keycloak access log должен редактировать sensitive query. TRACE/DEBUG production выключены. ### Events/audit Включаются login/login_error, logout, refresh/revoke, user create/disable, phone verify/change, brute-force/OTP limit, admin config changes. Retention/consumer определяется ops/legal; event payload минимален. ### Metrics - login/OTP send/verify success/failure/latency; - limit/lockout rejects; - settings cache age/refresh failures; - active sessions/token refresh/error; - DB pool/JVM/GC/HTTP; - JWKS/key age; - delivery mode info (`mock`, `sms`) и provider dependency `idgtl`, без phone labels. ### Tracing OTEL support зависит от версии; HTTP/provider/settings bridge spans добавляются instrumentation без secrets. Если native tracing недостаточно, сохраняются request/trace correlation headers. Наблюдаемость не меняет auth outcome. ## 21. Backup, restore и disaster recovery - managed PostgreSQL daily backup + PITR; - realm config export хранится versioned и secret-free; - signing key/private realm state входит в protected DB backup; - provider JAR/theme/image reproducible из repo/artifacts; - restore rehearsal в isolated environment; - после restore проверяются issuer, keys/JWKS, clients/flows, user/session consistency, provider tables; - RPO/RTO фиксируются ops до production. При восстановлении в другой host нельзя случайно выдать production tokens с неверным issuer. DNS/TLS/hostname проверяются до открытия traffic. Backup encrypted/access-controlled; OTP expired rows очищаются по TTL. ## 22. Миграции и upgrades Порядок: 1. прочитать release notes и supported DB upgrade path; 2. backup/PITR checkpoint; 3. проверить provider SPI/API compatibility и пересобрать JAR; 4. прогнать upgrade clone БД; 5. contract/E2E login+refresh+logout; 6. staged maintenance/rolling rollout только если версия поддерживает cluster compatibility; 7. проверить schema migration, realm drift, JWKS; 8. rollback приложения возможен только если DB schema backward-compatible; иначе restore/forward-fix runbook. Нельзя пропускать major versions произвольно. Realm changes versioned отдельно. Custom provider migration имеет собственный version table/compatibility matrix. ## 23. Docker/runtime hardening - service `keycloak`, `expose: 8080` и management port только internal; - networks `public` (только nginx access при необходимости), `backend`, `observability`; при `KEYCLOAK_YANDEX_CAPTCHA_ENABLED=true` дополнительно ограниченная `egress` только к `smartcaptcha.cloud.yandex.ru:443` для server-side `/validate`, при `false` egress у Keycloak отсутствует; - без host `ports`; - non-root, read-only rootfs где совместимо, tmpfs для temp; - no-new-privileges/drop capabilities; - memory/CPU/JVM heap limits, graceful termination; - startup/readiness probes с достаточным initial period; - immutable provider/theme mounts/image; - no local persistent DB volume. TLS client→nginx; Keycloak→managed PG TLS. Internal nginx→Keycloak HTTP допустим только на закрытой сети одной VM. ## 24. Failure semantics | Сбой | Поведение | |---|---| | DB down | not-ready; login/refresh fail; existing access tokens проверяются API до exp по cached JWKS | | settings bridge down, cache valid | send limits по last-known-good | | settings bridge down, cache empty/stale | new OTP send fail-closed | | mock secret missing/invalid | startup/not-ready; OTP не bypass | | real mode без URL/token `sms-service` | новый OTP order fail-closed `otp_provider_unconfigured`; startup/config gate не пройден | | `sms-service` timeout/5xx | один retry с тем же idempotency key; затем `order_failed`, generic unavailable | | Direct reject/timeout после durable order | active challenge не меняется; Keycloak provider status не читает | | wrong OTP | generic error, increment counter | | too many sends/verifies | temporary reject/lockout, safe UX | | token signing key rotation | old keys passive в JWKS grace | | OTEL down | auth работает, telemetry drop metric/local log | | provider exception | flow fail-closed, generic error/request id | Не должно быть fallback на password или «успешный OTP» при инфраструктурной ошибке. ## 25. Тестовая матрица ### Unit provider - E.164 normalization across RU/international/Unicode; - invalid/impossible phone; - unique concurrent reservation; - mock constant-time compare/redaction; - challenge TTL/one-time/replay/concurrent verify; - send/verify limits and window boundaries; - settings cache/ETag/stale/fail-closed; - phone HMAC/counter cleanup; - provider SPI error mapping. - durable order `200/202`, idempotent retry, `409` reuse и `order_failed`; - lifecycle `ordering/active/superseded/expired/limited/consumed`, periodic/lazy expiry; - device metadata validation и append-only event на каждую verify. ### Realm/config contract - only standard code+PKCE S256; - password/direct/implicit/social disabled; - exact origins/redirect/logout URIs; - audience/claims/issuer; - token/session TTL and refresh rotation; - browser flow executions/required actions; - no secrets/private keys in realm export. ### Integration - managed/test PostgreSQL schema/currentSchema/TLS; - restart preserves counters/challenges; - Keycloak upgrade/provider migration; - settings bridge token/path with module-01; - JWKS rotation and API validation; - disabled user/revocation/not-before; - brute-force lockout/recovery; - proxy hostname/path builds correct external URLs. ### E2E - new phone → mock OTP → PKCE tokens → API bootstrap; - real mode: durable order открывает OTP form до ответа Direct; `sms_message_id` совпадает в обеих БД; - resend отклоняет старый код; provider reject/timeout не меняет active challenge; - existing user login; valid refresh without OTP; - expired/revoked/rotated refresh → re-auth; - wrong/expired/replayed code; - max sends/min interval/max verifies; - concurrent tabs/refresh single-flight assumptions; - logout web/native; - DB/settings outage; - no phone/OTP/token in logs, URLs or metrics; - admin endpoint inaccessible publicly. ### Security - redirect/open redirect, PKCE downgrade, state/nonce; - user enumeration/timing; - cookie flags/CSRF on auth forms; - forwarded header spoofing; - brute-force/IP/phone distributed attempts; - JWT alg/aud/iss/kid attacks; - secret scanning/image/SBOM/provider dependency review. ## 26. Definition of Done - Keycloak доступен за `/auth`, issuer/discovery/JWKS стабильны; - realm/client topology и PKCE S256 зафиксированы declaratively; - только phone OTP; password/implicit/direct/social отключены; - phone canonical E.164 и storage-level unique; - claims соответствуют module-01 (`sub`, `phone_number`, audience); - mock secret only env, не логируется/не отдаётся; - OTP challenges/counters/verify events durable в Keycloak schema; SMS journal/template/provider statuses там отсутствуют; - product limits читаются только через canonical settings bridge/token; - brute-force, TTL, verify attempts и enumeration protection работают; - refresh rotation/reuse detection/logout/revocation покрыты; - proxy/redirect/origin/CORS/cookies/TLS boundaries проверены; - DB role/schema/backup/restore/upgrade runbooks готовы; - health/metrics/logging/tracing не раскрывают secrets/PII; - container hardening/root Compose без published port; - test matrix зелёная; - mock и real mutually exclusive; real mode вызывает только durable-order API `sms-service`, Direct/Verifier/status polling отсутствуют. ## 27. Решения, допущения и TBD **Решения:** - K1: realm `han-chat`, public client `han-chat-frontend`, audience `han-chat-api`. - K2: Authorization Code + PKCE S256; остальные user grants выключены. - K3: canonical identity/claim — E.164 `phone_number`; `sub` immutable. - K4: OTP authenticator/provider SPI; mock code только secret env. - K5: counters/challenges в provider-owned PostgreSQL schema `keycloak`, не API Redis. - K6: product limits только `/internal/settings/v1/otp` + `KEYCLOAK_SETTINGS_BRIDGE_TOKEN`. - K7: refresh rotation/revoke-on-use; frontend single-flight. - K8: real delivery — `Keycloak → sms-service → i-Digital Direct`; verify остаётся локальным. **Допущения:** - A1: единый public host `tohin.ru` и relative path `/auth`. - A2: Keycloak version поддерживает нужные hostname/proxy/health options; точные names pin после выбора image. - A3: product допускает mock OTP до прохождения controlled real-SMS rollout как временный риск. - A4: телефон в access token необходим API bootstrap и защищён TLS/short token TTL. **TBD:** - K-TBD1: выбрать/pin Keycloak version и проверить custom SPI compatibility. - K-TBD2: окончательные redirect URI для Expo iOS/Android и universal/app links. - K-TBD3: финальные token/session TTL и brute-force thresholds после security review. - K-TBD4: exact schema/migration mechanism provider tables без вмешательства в standard schema. - K-TBD5: admin MFA/ops access topology и отдельный admin hostname. - K-TBD6: signing-key rotation interval/HSM и emergency revocation. - K-TBD7: RPO/RTO/event retention/legal deletion. - K-TBD8 закрыт module-11 для v1: vendor i-Digital Direct, credentials/template/sender/callback принадлежат `sms-service`; failover вне v1. - K-TBD9 закрыт для v1: одна невидимая Yandex SmartCaptcha защищает initial send и resend; динамический risk scoring остаётся вне scope.