# module-08. Проектная спецификация `keycloak` > Статус: целевая production-спецификация MVP; реальный SMS provider не входит в scope. > Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.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.md`](module-03-nginx.md), [`../../HAN_chat/deploy/init-managed-postgres.py`](../../HAN_chat/deploy/init-managed-postgres.py). ## 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, mock verification и продуктовые limits; - 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-сессию; - реальную отправку SMS в MVP. Реальный SMS provider — строго extension point/TBD. Mock code является секретом окружения, не контентом UI и не логируется. ## 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` инициирует mock/provider send. 7. Показывается форма OTP. 8. Проверяются TTL/attempt limits/constant-time hash or mock compare. 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 OTP Env: ```text KEYCLOAK_OTP_MOCK_ENABLED=true KEYCLOAK_OTP_MOCK_CODE= ``` Правила: - mock разрешён MVP production-like только как явно принятый риск; - пустой/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` при отсутствии configured provider делает OTP flow fail-closed/not-ready, а не пропускает проверку. Реальный provider interface: ```java interface OtpDeliveryProvider { DeliveryResult send(E164Phone phone, String otp, Duration ttl, Correlation ctx); } ``` Будущий provider обязан вернуть `provider_message_id`; raw OTP не логируется. Выбор provider, template, sender, delivery status webhook и vendor credentials — TBD. ## 8. OTP challenge и counters Даже в mock: - challenge id random ≥128 bit; - OTP не хранится raw; production-generated code — keyed hash/HMAC с challenge salt/pepper; - TTL (предлагается 5 минут) — technical security parameter; - one-time use; success atomically consumes challenge; - max verification attempts per challenge; - resend invalidates либо version-binds предыдущий challenge; - replay/parallel verify безопасны; - destination stored masked/hash where possible. Audit fields по arch-05: provider message id (для mock — synthetic non-secret), sent_at, destination_masked, otp_hash/reference, attempts, outcome. Никогда raw code. ### 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, "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, otp_hash/mock marker, created/expires/consumed, verify attempts, settings version, provider id/status; - `han_otp_send_counter`: phone_hmac, window_start, count, last_sent_at; - `han_otp_security_event`: append-only minimal outcome/retention. Indexes: unique active challenge policy, `(phone_hmac,window_start)`, `(expires_at)`. Cleanup bounded job. Доступ только `keycloak_user`. ## 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. CAPTCHA/risk engine — future extension. 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. ## 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= 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. Product limits `otp.phone.*`, включая `otp.phone.max_verify_attempts`, не дублируются env и поступают через settings bridge. OTP TTL остаётся security technical config provider-а: ```text KEYCLOAK_OTP_TTL_SEC=300 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 либо реальный provider configured. Стандартный 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; - provider mode info (`mock`, later vendor), без 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`; - без 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 | | SMS mode без provider | not-ready `otp_provider_unconfigured` | | 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. ### 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; - 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 durable в Keycloak schema; - 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 зелёная; - реальный SMS явно остаётся extension point, не скрытой заглушкой. ## 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 SMS provider — extension point/TBD. **Допущения:** - A1: единый public host `tohin.ru` и relative path `/auth`. - A2: Keycloak version поддерживает нужные hostname/proxy/health options; точные names pin после выбора image. - A3: product допускает mock OTP в первой production-like среде как временный риск. - 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: SMS vendor, credentials, templates, sender, delivery receipts and failover. - K-TBD9: CAPTCHA/risk scoring после mock. - K-TBD10: добавить proposed OTP technical env в arch-04 до реализации.