Разработана первая версия приложений
This commit is contained in:
@@ -0,0 +1,703 @@
|
||||
# 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
|
||||
<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:
|
||||
|
||||
```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=<secret>
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- 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=<secret>
|
||||
KEYCLOAK_SETTINGS_BRIDGE_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.
|
||||
|
||||
Product limits `otp.phone.*` не дублируются env. OTP TTL/max verify attempts — security technical config provider-а; их имена нужно добавить в arch-04 до реализации, например:
|
||||
|
||||
```text
|
||||
KEYCLOAK_OTP_TTL_SEC=300
|
||||
KEYCLOAK_OTP_MAX_VERIFY_ATTEMPTS=5
|
||||
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 либо реальный 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 до реализации.
|
||||
Reference in New Issue
Block a user