Files
han-app/releases/#1 SMS OTP deploy.md
T

13 KiB
Raw Blame History

#1 SMS OTP deploy

Безопасный порядок развёртывания sms-service/worker на существующей ВМ и включения реальной OTP-доставки: сначала подготовить PostgreSQL и секреты, затем запустить новый контур при mock=true, проверить i-Digital и только после этого переключить Keycloak.

Старую сборку Keycloak после expand-миграции возвращать нельзя. Аварийный откат выполняется переключением новой сборки обратно в mock-режим.

0. До начала

  • Получить у i-Digital:
    • TOKEN_1;
    • согласованное имя отправителя;
    • согласованный текст auth_otp;
    • подтверждённый source IP для callback;
    • регистрацию статического egress IP ВМ.
  • Создать PITR marker/backup managed PostgreSQL.
  • Скопировать /opt/han-chat/backend/.env в защищённое место вне каталога релиза.
  • Оставить KEYCLOAK_OTP_MOCK_ENABLED=true до последнего этапа.
  • На production-like при mock-режиме оставить KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=true.
  • Подтвердить у Direct актуальность IP 185.203.96.7, указанного в codebase/backend/nginx/templates/site-tls.conf.template. Если IP другой — обновить allowlist до сборки nginx.

1. Создать пользователя и схему PostgreSQL

1.1. Создать пользователя

В интерфейсе Selectel создать отдельного пользователя:

sms_user

Использовать случайный пароль не короче 32 символов.

1.2. Создать схему

Подключиться к han_chat под dbAdmin и выполнить:

GRANT CONNECT ON DATABASE han_chat TO sms_user;
GRANT CREATE ON DATABASE han_chat TO sms_user;

CREATE SCHEMA IF NOT EXISTS sms AUTHORIZATION sms_user;
REVOKE ALL ON SCHEMA sms FROM PUBLIC;

ALTER ROLE sms_user IN DATABASE han_chat SET search_path TO sms, public;

1.3. Проверить

Переподключиться к БД как sms_user:

SELECT current_user;
SHOW search_path;

SELECT
    nspname,
    pg_get_userbyid(nspowner) AS owner
FROM pg_namespace
WHERE nspname = 'sms';

Ожидаемый результат:

  • current_user = sms_user;
  • search_path = sms, public;
  • владелец схемы smssms_user.

Не выдавать sms_user права на схемы han_app и keycloak.

2. Заполнить .env на ВМ

Файл:

/opt/han-chat/backend/.env

Добавить или обновить:

SMS_SERVICE_IMAGE=han-chat-sms-service:local
SMS_DATABASE_URL=postgresql+asyncpg://sms_user:<URL_ENCODED_PASSWORD>@<PG_HOST>:<PG_PORT>/han_chat?sslmode=verify-full&sslrootcert=/run/secrets/pg-ca.pem

KEYCLOAK_SMS_SERVICE_URL=http://sms-service:8080
SMS_SERVICE_TOKEN=<openssl rand -hex 32>
KEYCLOAK_SMS_SERVICE_TOKEN=<ТОЧНО ТО ЖЕ ЗНАЧЕНИЕ>

IDGTL_SMS_BASE_URL=https://direct.i-dgtl.ru
IDGTL_SMS_API_KEY=<ГОТОВЫЙ TOKEN_1 БЕЗ ПОВТОРНОГО BASE64>
IDGTL_SMS_CALLBACK_PUBLIC_URL=https://<PUBLIC_HOST>/callbacks/idgtl/sms
IDGTL_SMS_CALLBACK_USERNAME=<openssl rand -hex 16>
IDGTL_SMS_CALLBACK_PASSWORD=<openssl rand -hex 32>

NGINX_RATE_LIMIT_SMS_CALLBACK=120r/m

KEYCLOAK_OTP_MOCK_ENABLED=true
KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=true

Важно:

  • пароль БД необходимо URL-encode, если он содержит специальные символы;
  • SMS_SERVICE_TOKEN и KEYCLOAK_SMS_SERVICE_TOKEN должны совпадать;
  • IDGTL_SMS_API_KEY — уже готовое значение Basic API key TOKEN_1, повторно кодировать его нельзя;
  • KEYCLOAK_OTP_HMAC_KEY во время rollout не менять.

2.1. Проверить egress IP

Из каталога /opt/han-chat/backend:

docker compose --env-file .env --profile ops run --rm \
  --entrypoint curl toolbox -fsS https://api.ipify.org

Полученный IP передать Direct для allowlist. При динамическом IP сначала настроить статический IP/NAT.

2.2. Проверить конфигурацию

cd /opt/han-chat/backend

./scripts/validate-env .env
docker compose --env-file .env config --quiet
docker compose --env-file .env config --services

3. Скопировать и собрать release

Копирование проекта выполняется по инструкции deploy-steps.md.

Сначала выполнить rsync с флагом -n и проверить список изменений. Убедиться, что исключены:

.env
secrets/
*.crt
*.pem
*.key

После проверки повторить rsync без -n.

На ВМ:

cd /opt/han-chat/backend

find . -type f \( -name '*.sh' -o -name 'validate-env' \) -exec dos2unix {} +
chmod +x scripts/validate-env deployment/scripts/*.sh nginx/scripts/*.sh

./scripts/validate-env .env

docker compose --env-file .env build --pull \
  api-backend sms-service keycloak frontend-static nginx

На этом этапе KEYCLOAK_OTP_MOCK_ENABLED всё ещё должен быть true.

4. Применить миграции

Перед миграцией создать PITR marker у провайдера БД.

cd /opt/han-chat/backend

PITR_MARKER_CONFIRMED=true deployment/scripts/migrate.sh

Команда применит:

  • migration 0005_otp_settings для han_app;
  • migration 0001_initial для схемы sms;
  • migration 0002_seed для схемы sms;
  • остальные штатные migrations проекта.

При необходимости применить production-like settings:

docker compose --env-file .env --profile ops run --rm seed-settings

Проверить версии:

SELECT version_num FROM han_app.alembic_version;
SELECT version_num FROM sms.alembic_version;

Ожидается:

han_app: 0005_otp_settings
sms:     0002_seed

5. Записать согласованные sender и SMS-шаблон

Миграция намеренно создаёт placeholder. Пока он не заменён, sms-service будет возвращать not_ready.

Подключиться как sms_user и выполнить, подставив согласованные значения:

UPDATE sms.sms_setting
SET setting_value = to_jsonb('<APPROVED_SENDER>'::text),
    updated_at = now()
WHERE setting_key = 'provider.idgtl.default_sender_name';

UPDATE sms.sms_template
SET body_template = 'Код входа в HAN Chat: {code}. Действителен {ttl_min} мин.',
    sender_name = NULL,
    approved_at = now(),
    updated_at = now(),
    created_by = 'ops-approved'
WHERE code = 'auth_otp'
  AND channel = 'SMS'
  AND locale = 'ru'
  AND version = 1;

Если оператор согласовал другой текст, использовать именно его. В тексте должны остаться ровно два placeholders:

{code}
{ttl_min}

Для OTP должно сохраняться:

max_parts = 1

Проверить:

SELECT
    code,
    channel,
    locale,
    version,
    body_template,
    sender_name,
    max_parts,
    is_active,
    approved_at
FROM sms.sms_template
WHERE code = 'auth_otp';

SELECT setting_key, setting_value
FROM sms.sms_setting
ORDER BY setting_key;

Должна существовать ровно одна active+approved версия auth_otp, а placeholder имени отправителя должен быть заменён.

6. Запустить SMS-контур при mock=true

cd /opt/han-chat/backend

docker compose --env-file .env up -d sms-service sms-worker
docker compose --env-file .env ps sms-service sms-worker
docker compose --env-file .env logs --since=10m sms-service sms-worker

Ожидается:

  • sms-service — healthy;
  • worker запущен;
  • отсутствуют ошибки Direct 401/402;
  • отсутствуют contract errors;
  • отсутствуют необъяснённые uncertain.

Затем запустить обновлённые смежные сервисы, не выключая mock:

docker compose --env-file .env up -d --force-recreate \
  api-backend keycloak frontend-static nginx

docker compose --env-file .env exec -T nginx nginx -t -c /tmp/nginx.conf
deployment/scripts/smoke.sh

Первый запуск новой сборки Keycloak применит Liquibase expand migration. Старые незавершённые OTP challenges будут помечены истёкшими, поэтому запускать Keycloak лучше в период низкой активности.

7. Проверить i-Digital до включения real mode

Через внутренний endpoint:

POST /internal/sms/v1/send

заказать одну SMS на контролируемый номер.

Требования к тесту:

  • использовать уникальный idempotency_key;
  • не записывать service token и OTP в shell history;
  • JSON body создать во временном файле с правами 600;
  • после теста удалить временный файл.

Проверить журнал:

SELECT
    id,
    created_at,
    phone_masked,
    send_status,
    delivery_status,
    provider_message_id,
    provider_error_code,
    attempt_count,
    callback_last_at
FROM sms.sms_outbound_message
ORDER BY created_at DESC
LIMIT 10;

Ожидается:

  1. После заказа создана одна строка.
  2. send_status переходит в accepted.
  3. provider_message_id заполнен.
  4. Callback меняет delivery_status на sent/delivered.
  5. Повтор идентичного запроса возвращает тот же sms_message_id и не создаёт вторую SMS.

Проверить edge:

  • публичный /internal/sms/* возвращает 404;
  • callback не с IP Direct возвращает 403;
  • реальный callback Direct проходит IP allowlist и Basic auth.

8. Включить реальные SMS

Только после успешной тестовой отправки изменить:

KEYCLOAK_OTP_MOCK_ENABLED=false
KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=false
KEYCLOAK_OTP_MOCK_CODE=

Применить:

cd /opt/han-chat/backend

./scripts/validate-env .env

docker compose --env-file .env up -d \
  --no-deps \
  --force-recreate keycloak

docker compose --env-file .env ps keycloak
docker compose --env-file .env logs --since=10m \
  keycloak sms-service sms-worker

Проверить полный пользовательский сценарий:

  1. Ввод номера телефона.
  2. Получение реальной SMS.
  3. Неверный OTP отклоняется.
  4. Верный OTP авторизует пользователя.
  5. Resend создаёт новый challenge.
  6. Старый challenge получает superseded.
  7. Старый код больше не принимается.
  8. OTP истекает через 60 секунд.
  9. Работают лимиты отправок и проверок.
  10. Уже active challenge продолжает локально проверяться при временно остановленном worker.

9. Аварийный откат

Не выполнять:

  • downgrade Alembic;
  • downgrade Liquibase;
  • возврат старой сборки Keycloak.

После expand migration старая сборка Keycloak несовместима с новыми обязательными полями challenge.

Безопасный rollback — оставить новую сборку и вернуть mock:

KEYCLOAK_OTP_MOCK_ENABLED=true
KEYCLOAK_OTP_MOCK_CODE=<НЕПУБЛИЧНЫЙ 6-ЗНАЧНЫЙ КОД>
KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=true
cd /opt/han-chat/backend

./scripts/validate-env .env

docker compose --env-file .env up -d \
  --no-deps \
  --force-recreate keycloak

sms-service и worker можно оставить запущенными для обработки callback и reconciliation. Новые SMS-заказы от Keycloak прекратятся.

Записи со статусом uncertain автоматически не переотправлять — их необходимо разбирать вручную.