Files
han-app/VM1_app/documentation/module-08-keycloak.md
T
2026-08-26 11:05:32 +03:00

41 KiB
Raw Blame History

module-08. Проектная спецификация keycloak

Статус: целевая production-спецификация OTP; mock действует до controlled rollout, real mode интегрируется только через sms-service по module-11-idgtl-sms.md. Источники: README.md, arch-00-glossary.md, arch-01-system-architecture.md, arch-02-api-contracts.md, arch-03-docker-compose-blueprint.md, arch-04-settings-and-content.md, arch-05-agent-development-process.md, arch-06-service-hosting-security.md, module-01-api-backend.md, module-02-frontend-test-site.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:

Client HTTPS https://tohin.ru/auth/*
  → nginx TLS termination
  → HTTP keycloak:8080 в закрытой Docker network
  → managed PostgreSQL schema keycloak по TLS

Публичный issuer обязан быть стабильным:

https://tohin.ru/auth/realms/han-chat

OIDC discovery:

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:

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 должны перечисляться отдельно:

https://tohin.ru/auth/callback
https://tohin.ru/mobile/oidc/callback
han-chat://auth/callback
<Expo native scheme/callback, exact value после сборки>

Production redirect URI задаются exact; wildcard не используется до отдельного security review. Development localhost origins/redirects находятся в отдельном dev realm/client либо profile и запрещены production.

4.2. API audience

Audience:

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:

KEYCLOAK_OTP_MOCK_ENABLED=true
KEYCLOAK_OTP_MOCK_CODE=<secret>

Правила:

  • 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:

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 вызывает:

GET http://api-backend:8000/internal/settings/v1/otp
Authorization: Bearer ${KEYCLOAK_SETTINGS_BRIDGE_TOKEN}

Ответ:

{
  "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 проверяются по закреплённой версии):

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

Используется:

KEYCLOAK_DB_URL=jdbc:postgresql://.../han_chat?...&currentSchema=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

Репозиторий:

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

Канонические:

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=<secret>
KEYCLOAK_SETTINGS_BRIDGE_TOKEN=<secret>
KEYCLOAK_SMS_SERVICE_URL=http://sms-service:8080
KEYCLOAK_SMS_SERVICE_TOKEN=<secret>
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:

otp.phone.code_length
otp.phone.ttl_seconds
otp.phone.sms_order_timeout_ms

Challenge сохраняет snapshot этих значений и settings_version; изменение настроек влияет только на новые challenges. В env остаются только secret/bootstrap-параметры:

KEYCLOAK_OTP_SETTINGS_MAX_STALE_SEC=300
KEYCLOAK_OTP_HMAC_KEY=<secret>

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.