доработки в файлах раскатки

This commit is contained in:
mi
2026-07-13 13:44:06 +03:00
parent 8c7b4074c4
commit 4d8e856fd8
5 changed files with 1506 additions and 0 deletions
@@ -0,0 +1,769 @@
# Подробная инструкция по развертыванию и запуску HAN Chat
Эта инструкция описывает первый запуск текущего проекта на одной виртуальной
машине с Ubuntu 24.04. Все команды на VM предполагают, что проект расположен в
`/opt/han-chat/backend`, а команды Docker Compose выполняются из этого каталога.
PostgreSQL и Selectel S3 не запускаются в Docker Compose: их необходимо создать
заранее как внешние управляемые сервисы. Из интернета должны быть доступны только
порты 80 и 443 виртуальной машины.
## 1. Что потребуется до начала работы
Подготовьте:
1. Виртуальную машину с Ubuntu 24.04 и минимум 4 vCPU, 8 ГБ RAM и 40 ГБ диска.
2. SSH-доступ к VM пользователем с правом `sudo`.
3. Домен, например `chat.example.ru`, и возможность изменить его DNS.
4. Управляемый PostgreSQL, доступный VM по приватной сети.
5. Три приватных бакета Selectel S3.
6. Учетные данные приложения Bitrix24.
7. При необходимости — удаленный OTLP-бэкенд для телеметрии.
8. Локальную копию каталога `HAN_chat_specification/codebase/backend` либо URL
Git-репозитория, из которого его можно получить.
Для первого тестового запуска допустимы mock OTP, заглушка Message Safety и
заглушка bitrix-sync. Они не являются полноценными production-реализациями.
## 2. Первичный вход на VM
Подключитесь к созданной VM облачным пользователем:
```sh
ssh <cloud-user>@<VM_IP>
```
Проверьте версию ОС:
```sh
cat /etc/os-release
```
Должна использоваться Ubuntu 24.04 или более новая версия.
## 3. Передача и запуск скрипта настройки VM
Сначала передайте на VM только подготовительный скрипт. Например, с локального
компьютера:
```sh
scp deployment/scripts/setup-vm.sh <cloud-user>@<VM_IP>:/tmp/setup-vm.sh
```
На VM выполните:
```sh
chmod +x /tmp/setup-vm.sh
sudo /tmp/setup-vm.sh
```
Скрипт:
- обновит Ubuntu и установит базовые пакеты;
- создаст пользователя `deploy`;
- установит Docker Engine и Docker Compose;
- настроит UFW, fail2ban и цепочку `DOCKER-USER`;
- откроет только SSH, HTTP и HTTPS;
- создаст `/opt/han-chat/backend`;
- создаст swap;
- включит автоматические обновления безопасности.
По умолчанию скрипт не отключает парольный SSH-вход. Не используйте
`HARDEN_SSH=true`, пока не проверили вход пользователем `deploy` по ключу в
отдельной сессии.
Если SSH работает на нестандартном порту или имя внешнего интерфейса известно
заранее, передайте параметры:
```sh
sudo SSH_PORT=2222 EXTERNAL_IF=ens3 /tmp/setup-vm.sh
```
После завершения выйдите из SSH-сессии: членство `deploy` в группе `docker`
начинает действовать только после нового входа.
```sh
exit
ssh deploy@<VM_IP>
docker version
docker compose version
```
## 4. Копирование проекта на VM
### Вариант A — через Git
Это предпочтительный вариант: Git применит правило LF для shell-скриптов.
```sh
git clone <URL_РЕПОЗИТОРИЯ> /tmp/han-chat-source
cp -a /tmp/han-chat-source/HAN_chat_specification/codebase/backend/. \
/opt/han-chat/backend/
cd /opt/han-chat/backend
```
Если `HAN_chat_specification` является корнем репозитория:
```sh
cp -a /tmp/han-chat-source/codebase/backend/. /opt/han-chat/backend/
```
### Вариант B — архивом с локального компьютера
Создайте архив именно из содержимого каталога `backend`, включая скрытые файлы:
```sh
tar -C HAN_chat_specification/codebase/backend -czf han-chat-backend.tar.gz .
scp han-chat-backend.tar.gz deploy@<VM_IP>:/tmp/
```
На VM:
```sh
cd /opt/han-chat/backend
tar -xzf /tmp/han-chat-backend.tar.gz
# Обязательно при копировании с Windows:
find . -type f \( -name '*.sh' -o -name 'validate-env' \) -exec dos2unix {} +
chmod +x scripts/validate-env deployment/scripts/*.sh redis/scripts/*.sh nginx/scripts/*.sh
```
Проверьте наличие точки запуска:
```sh
test -f /opt/han-chat/backend/docker-compose.yml
test -f /opt/han-chat/backend/.env.example
```
## 5. Настройка DNS и сетевого доступа
Создайте DNS-запись:
```text
chat.example.ru A <ПУБЛИЧНЫЙ_IP_VM>
```
Дождитесь обновления DNS:
```sh
getent ahostsv4 chat.example.ru
```
В облачной группе безопасности VM разрешите входящие подключения:
- TCP 80 из интернета;
- TCP 443 из интернета;
- SSH только из доверенной сети или с административного IP.
Не открывайте наружу порты 6379, 4317, 4318, 8000, 8080 и 9000.
В группе безопасности PostgreSQL разрешите входящий трафик на порт PostgreSQL
только от приватного адреса или группы безопасности VM.
## 6. Подготовка управляемого PostgreSQL
Создайте одну базу данных:
```text
han_chat
```
В ней нужны схемы:
```text
han_app
bitrix_local
bitrix_sync
message_safety
keycloak
```
Для текущей MVP-реализации используются следующие пользователи:
```text
han_app
bitrix_local_app
bitrix_sync_user
message_safety_app
keycloak_user
```
Создать пользователей и схемы можно через панель провайдера либо от имени
администратора PostgreSQL. Пример SQL:
```sql
CREATE ROLE han_app LOGIN PASSWORD '<HAN_APP_PASSWORD>';
CREATE ROLE bitrix_local_app LOGIN PASSWORD '<BITRIX_LOCAL_PASSWORD>';
CREATE ROLE bitrix_sync_user LOGIN PASSWORD '<BITRIX_SYNC_PASSWORD>';
CREATE ROLE message_safety_app LOGIN PASSWORD '<SAFETY_PASSWORD>';
CREATE ROLE keycloak_user LOGIN PASSWORD '<KEYCLOAK_PASSWORD>';
CREATE SCHEMA IF NOT EXISTS han_app AUTHORIZATION han_app;
CREATE SCHEMA IF NOT EXISTS bitrix_local AUTHORIZATION bitrix_local_app;
CREATE SCHEMA IF NOT EXISTS bitrix_sync AUTHORIZATION bitrix_sync_user;
CREATE SCHEMA IF NOT EXISTS message_safety AUTHORIZATION message_safety_app;
CREATE SCHEMA IF NOT EXISTS keycloak AUTHORIZATION keycloak_user;
GRANT CONNECT ON DATABASE han_chat TO
han_app, bitrix_local_app, bitrix_sync_user, message_safety_app, keycloak_user;
```
Текущие migration jobs используют те же DSN, что и сервисы. Поэтому владельцы
схем должны иметь право создавать таблицы в своих схемах. Для более строгого
production-разделения migration/runtime ролей потребуется отдельная настройка
DSN и прав, которой в текущем `.env.example` нет.
Скачайте CA-сертификат PostgreSQL у провайдера и поместите его на VM:
```sh
mkdir -p /opt/han-chat/backend/secrets/pg
cp /путь/к/ca.pem /opt/han-chat/backend/secrets/pg/ca.pem
chmod 644 /opt/han-chat/backend/secrets/pg/ca.pem
```
CA-сертификат не является секретом. Права `644` нужны, чтобы его могли прочитать
контейнеры, работающие не от root.
Проверьте сетевую доступность:
```sh
nc -vz <PG_HOST> 6432
```
Замените `6432` на фактический порт провайдера.
## 7. Подготовка Selectel S3
Создайте три приватных бакета:
```text
han-chat-quarantine
han-chat-attachments
han-chat-documents
```
Создайте две пары ключей:
1. Ключ API с правом чтения и записи в бакеты.
2. Отдельный ключ Message Safety только с правом чтения карантина.
Для бакетов запретите публичный доступ. Для браузерной загрузки настройте CORS:
- Allowed origin: `https://chat.example.ru`;
- Methods: `PUT`, `GET`, `HEAD`;
- Headers: `Content-Type`, `x-amz-*`;
- Expose header: `ETag`.
Для карантина задайте lifecycle удаления объектов с запасом относительно
`MESSAGE_SAFETY_TASK_TTL_SEC`.
## 8. Создание файла окружения
На VM:
```sh
cd /opt/han-chat/backend
umask 077
cp .env.example .env
chmod 600 .env
nano .env
```
Замените все `change-me` и адреса `example.*`.
### 8.1. Основные адреса
Для домена `chat.example.ru`:
```dotenv
APP_ENV=production-like
RELEASE_VERSION=2026-07-13-1
PUBLIC_HOST=chat.example.ru
PUBLIC_WEB_URL=https://chat.example.ru
PUBLIC_API_URL=https://chat.example.ru/api
PUBLIC_AUTH_URL=https://chat.example.ru/auth
KEYCLOAK_PUBLIC_URL=https://chat.example.ru/auth
KEYCLOAK_INTERNAL_URL=http://keycloak:8080/auth
KEYCLOAK_REALM=han-chat
KEYCLOAK_AUDIENCE=han-chat-api
```
### 8.2. PostgreSQL
Укажите фактические host, port, database, пользователей и пароли:
```dotenv
HAN_PG_HOST=<PG_HOST>
HAN_PG_PORT=6432
HAN_PG_DATABASE=han_chat
PG_CA_HOST_PATH=/opt/han-chat/backend/secrets/pg/ca.pem
DATABASE_URL=postgresql+asyncpg://han_app:<PASSWORD>@<PG_HOST>:6432/han_chat?sslmode=verify-full&sslrootcert=/run/secrets/pg-ca.pem&options=-csearch_path%3Dhan_app
BITRIX_DATABASE_URL=postgresql://bitrix_local_app:<PASSWORD>@<PG_HOST>:6432/han_chat?sslmode=verify-full&sslrootcert=/run/secrets/pg-ca.pem&options=-csearch_path%3Dbitrix_local
BITRIX_SYNC_DATABASE_URL=postgresql://bitrix_sync_user:<PASSWORD>@<PG_HOST>:6432/han_chat?sslmode=verify-full&sslrootcert=/run/secrets/pg-ca.pem&options=-csearch_path%3Dbitrix_sync
MESSAGE_SAFETY_DATABASE_URL=postgresql://message_safety_app:<PASSWORD>@<PG_HOST>:6432/han_chat?sslmode=verify-full&sslrootcert=/run/secrets/pg-ca.pem&options=-csearch_path%3Dmessage_safety
KEYCLOAK_DB_URL=jdbc:postgresql://<PG_HOST>:6432/han_chat?currentSchema=keycloak&sslmode=verify-full&sslrootcert=/run/secrets/pg-ca.pem
KEYCLOAK_DB_USERNAME=keycloak_user
KEYCLOAK_DB_PASSWORD=<PASSWORD>
```
Если пароль содержит `@`, `:`, `/`, `?`, `#` или `%`, его необходимо
URL-кодировать внутри PostgreSQL URL.
### 8.3. Генерация секретов
Для обычных токенов используйте:
```sh
openssl rand -hex 32
```
Для `BITRIX_TOKEN_ENCRYPTION_KEY` нужен URL-safe Base64 ключ ровно из 32 байт:
```sh
python3 -c 'import base64,secrets; print(base64.urlsafe_b64encode(secrets.token_bytes(32)).decode())'
```
Заполните все секреты. Две пары должны совпадать:
```dotenv
BITRIX_LOCAL_APP_INTERNAL_TOKEN=<TOKEN_A>
BITRIX_INTERNAL_API_TOKEN=<TOKEN_A>
BITRIX_API_FORWARD_TOKEN=<TOKEN_B>
BITRIX_API_INBOX_TOKEN=<TOKEN_B>
```
Остальные токены должны быть разными:
```dotenv
MESSAGE_SAFETY_SERVICE_TOKEN=<UNIQUE_TOKEN>
BITRIX_SYNC_SERVICE_TOKEN=<UNIQUE_TOKEN>
KEYCLOAK_SETTINGS_BRIDGE_TOKEN=<UNIQUE_TOKEN>
CURSOR_HMAC_SECRET=<UNIQUE_TOKEN>
KEYCLOAK_OTP_HMAC_KEY=<UNIQUE_TOKEN_НЕ_КОРОЧЕ_32_БАЙТ>
BITRIX_TOKEN_ENCRYPTION_KEY=<URLSAFE_BASE64_KEY>
KEYCLOAK_ADMIN_PASSWORD=<UNIQUE_ADMIN_PASSWORD>
```
### 8.4. Redis
Создайте три разных пароля и продублируйте их в URL:
```dotenv
REDIS_API_PASSWORD=<REDIS_API_PASSWORD>
REDIS_SAFETY_PASSWORD=<REDIS_SAFETY_PASSWORD>
REDIS_HEALTH_PASSWORD=<REDIS_HEALTH_PASSWORD>
REDIS_URL=redis://api_backend:<REDIS_API_PASSWORD>@redis:6379/0
REDIS_REALTIME_URL=redis://api_backend:<REDIS_API_PASSWORD>@redis:6379/1
MESSAGE_SAFETY_REDIS_URL=redis://message_safety:<REDIS_SAFETY_PASSWORD>@redis:6379/2
```
### 8.5. Mock OTP
В MVP реализован только mock OTP. Для запуска:
```dotenv
KEYCLOAK_OTP_MOCK_ENABLED=true
KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=true
KEYCLOAK_OTP_MOCK_CODE=<ТЕСТОВЫЙ_КОД_НЕ_КОРОЧЕ_16_СИМВОЛОВ>
```
Этот код будет вводиться пользователем при тестовой авторизации. Не используйте
его как production-механизм доставки OTP.
### 8.6. S3
```dotenv
SELECTEL_S3_ENDPOINT_URL=https://s3.storage.selcloud.ru
SELECTEL_S3_BUCKET_QUARANTINE=han-chat-quarantine
SELECTEL_S3_BUCKET_ATTACHMENTS=han-chat-attachments
SELECTEL_S3_BUCKET_DOCUMENTS=han-chat-documents
SELECTEL_S3_ACCESS_KEY=<API_ACCESS_KEY>
SELECTEL_S3_SECRET_KEY=<API_SECRET_KEY>
SELECTEL_S3_QUARANTINE_READ_ACCESS_KEY=<SAFETY_READ_ACCESS_KEY>
SELECTEL_S3_QUARANTINE_READ_SECRET_KEY=<SAFETY_READ_SECRET_KEY>
```
### 8.7. Bitrix24
До установки локального приложения заполните:
```dotenv
BITRIX_CLIENT_ID=<BITRIX_CLIENT_ID>
BITRIX_CLIENT_SECRET=<BITRIX_CLIENT_SECRET>
BITRIX_CONNECTOR_ID=han_mobile_app
BITRIX_OPEN_LINE_ID=8
BITRIX_PUBLIC_BASE_URL=https://chat.example.ru/bitrix
BITRIX_TOKEN_ENCRYPTION_KEY=<URLSAFE_BASE64_KEY>
```
`BITRIX_APPLICATION_TOKEN` можно окончательно задать после создания приложения в
Bitrix24. До этого используйте отдельное случайное значение, проходящее
валидацию окружения.
### 8.8. TLS и наблюдаемость
До выпуска сертификата оставьте в `.env` целевые значения:
```dotenv
NGINX_TLS_ENABLED=true
NGINX_TLS_CERTIFICATE=/etc/letsencrypt/live/chat.example.ru/fullchain.pem
NGINX_TLS_CERTIFICATE_KEY=/etc/letsencrypt/live/chat.example.ru/privkey.pem
ACME_EMAIL=<ADMIN_EMAIL>
```
Если удаленный OTLP-бэкенд пока не выбран, укажите временные непубличные значения
и примите ограничение: Collector будет пытаться отправлять телеметрию и сохранять
ее в ограниченной очереди. Перед production-запуском задайте реальный endpoint.
## 9. Проверка окружения и конфигурации Compose
Выполните:
```sh
cd /opt/han-chat/backend
./scripts/validate-env .env
docker compose --env-file .env config --quiet
docker compose --env-file .env config --services
python3 -m unittest discover -s tests -v
```
Не переходите к следующему шагу, пока все команды не завершатся успешно.
Посмотрите итоговую конфигурацию портов:
```sh
docker compose --env-file .env config | grep -n 'published:'
```
Публиковаться должны только 80 и 443 у nginx.
## 10. Сборка образов
Соберите все локальные образы:
```sh
cd /opt/han-chat/backend
docker compose --env-file .env build --pull
```
Проверьте список:
```sh
docker compose --env-file .env images
```
Сборка Keycloak включает Java OTP SPI, а сборка `frontend-static` экспортирует
тестовый Expo Web frontend.
## 11. Миграции БД и начальные настройки
Перед миграциями создайте backup/PITR marker в панели провайдера PostgreSQL.
Затем:
```sh
cd /opt/han-chat/backend
PITR_MARKER_CONFIRMED=true deployment/scripts/migrate.sh
deployment/scripts/seed.sh
```
Скрипт применит миграции `han_app`, `bitrix_local` и baseline `bitrix_sync`, после
чего загрузит `deployment/app-settings.production-like.yaml`.
Повторный запуск seed должен быть безопасным:
```sh
deployment/scripts/seed.sh
```
## 12. Запуск внутренних сервисов
Сначала запустите Redis:
```sh
docker compose --env-file .env up -d redis
docker compose --env-file .env ps redis
```
Затем Keycloak и OpenTelemetry:
```sh
docker compose --env-file .env up -d keycloak otel-collector
docker compose --env-file .env ps keycloak otel-collector
```
Первый запуск Keycloak может занять несколько минут: он создаст свои таблицы и
импортирует realm `han-chat`.
После готовности Keycloak:
```sh
docker compose --env-file .env up -d message-safety
docker compose --env-file .env up -d api-backend
docker compose --env-file .env up -d bitrix-local-app bitrix-sync
docker compose --env-file .env up -d \
delivery-worker safety-recovery-worker cleanup-worker
docker compose --env-file .env ps
```
Если сервис не становится healthy:
```sh
docker compose --env-file .env logs --tail=200 <SERVICE_NAME>
docker inspect "$(docker compose --env-file .env ps -q <SERVICE_NAME>)"
```
## 13. Первоначальный выпуск TLS-сертификата
Для ACME требуется работающий nginx по HTTP. В `.env` оставьте
`NGINX_TLS_ENABLED=true`, но первый nginx запустите с временным переопределением:
```sh
NGINX_TLS_ENABLED=false \
docker compose --env-file .env up -d frontend-static nginx
```
Проверьте HTTP:
```sh
curl -I http://chat.example.ru/
```
Сначала рекомендуется проверить Certbot через staging:
```sh
docker compose --env-file .env --profile certbot run --rm certbot certonly \
--staging \
--webroot -w /var/www/certbot \
-d chat.example.ru \
--cert-name chat.example.ru-staging \
--email <ADMIN_EMAIL> \
--agree-tos --no-eff-email --non-interactive
```
После успешного staging-теста выпустите рабочий сертификат с основным cert-name
без `--staging`:
```sh
docker compose --env-file .env --profile certbot run --rm certbot certonly \
--webroot -w /var/www/certbot \
-d chat.example.ru \
--cert-name chat.example.ru \
--email <ADMIN_EMAIL> \
--agree-tos --no-eff-email --non-interactive
```
Пересоздайте nginx уже с TLS:
```sh
docker compose --env-file .env up -d --force-recreate nginx
docker compose --env-file .env exec -T nginx nginx -t -c /tmp/nginx.conf
curl -I https://chat.example.ru/
```
Повторно запустите VM setup, чтобы он обнаружил проект и установил systemd-таймер
продления сертификата:
```sh
sudo /opt/han-chat/backend/deployment/scripts/setup-vm.sh
systemctl status han-chat-ssl-renew.timer
```
## 14. Запуск всего контура
Теперь можно привести весь проект к состоянию, описанному Compose:
```sh
cd /opt/han-chat/backend
docker compose --env-file .env up -d
docker compose --env-file .env ps
```
Проверьте, что контейнеры не перезапускаются:
```sh
docker compose --env-file .env ps
docker compose --env-file .env logs --since=10m
```
## 15. Публичная проверка
Запустите smoke-тест:
```sh
cd /opt/han-chat/backend
deployment/scripts/smoke.sh
```
Также вручную проверьте:
```sh
curl -fsS https://chat.example.ru/api/v1/public/app-config | jq
curl -fsS https://chat.example.ru/api/v1/public/content | jq
curl -fsS \
https://chat.example.ru/auth/realms/han-chat/.well-known/openid-configuration | jq
```
Внутренний API не должен быть опубликован:
```sh
curl -i https://chat.example.ru/internal/safety/v1/messages/check
```
Ожидаемый статус — `404`.
Откройте в браузере:
```text
https://chat.example.ru/
```
Для тестовой авторизации используйте значение `KEYCLOAK_OTP_MOCK_CODE` из `.env`.
## 16. Подключение Bitrix24
В настройках локального приложения Bitrix24 задайте HTTPS-адреса:
```text
Установка: https://chat.example.ru/bitrix/install
Обработчик: https://chat.example.ru/bitrix/handler
Placement: https://chat.example.ru/bitrix/placement
```
После установки:
1. Получите и сохраните application token.
2. Запишите его в `BITRIX_APPLICATION_TOKEN` файла `.env`.
3. Пересоздайте сервис:
```sh
docker compose --env-file .env up -d --force-recreate bitrix-local-app
docker compose --env-file .env logs --tail=200 bitrix-local-app
```
Проверьте коннектор `han_mobile_app` и Открытую линию 8.
## 17. Включение SSH hardening
Только после успешного входа пользователем `deploy` по ключу в отдельной сессии:
```sh
sudo HARDEN_SSH=true \
/opt/han-chat/backend/deployment/scripts/setup-vm.sh
```
Не закрывайте текущую SSH-сессию, пока не проверили новый вход.
## 18. Обычный перезапуск проекта
Для штатного запуска после перезагрузки VM:
```sh
cd /opt/han-chat/backend
docker compose --env-file .env up -d
docker compose --env-file .env ps
```
Для перезапуска одного сервиса:
```sh
docker compose --env-file .env restart api-backend
```
После изменения `.env` используйте пересоздание, а не `restart`:
```sh
docker compose --env-file .env up -d --force-recreate <SERVICE_NAME>
```
## 19. Обновление версии проекта
Перед обновлением:
1. Создайте backup/PITR marker PostgreSQL.
2. Сохраните текущие image digests.
3. Получите новый код.
4. Проверьте `.env`.
5. Пересоберите образы.
6. Примените миграции и seed.
7. Пересоздайте сервисы.
Команды:
```sh
cd /opt/han-chat/backend
./scripts/validate-env .env
docker compose --env-file .env build --pull
PITR_MARKER_CONFIRMED=true deployment/scripts/migrate.sh
deployment/scripts/seed.sh
docker compose --env-file .env up -d
deployment/scripts/smoke.sh
```
## 20. Диагностика
Состояние сервисов:
```sh
docker compose --env-file .env ps
```
Все логи:
```sh
docker compose --env-file .env logs --tail=300
```
Логи конкретного сервиса:
```sh
docker compose --env-file .env logs -f api-backend
```
Проверка firewall:
```sh
sudo ufw status verbose
sudo iptables -L HAN-CHAT-DOCKER -n -v
```
Проверка сертификата:
```sh
openssl s_client -connect chat.example.ru:443 -servername chat.example.ru \
</dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates
```
Проверка свободного места:
```sh
df -h
docker system df
```
Не выполняйте `docker compose down -v`: эта команда удалит именованные volumes.
Не выполняйте Alembic downgrade. Для отката используйте
`deployment/scripts/rollback.sh` и инструкции из `RUNBOOK.ru.md`.
## 21. Когда развертывание можно считать завершенным
Проект запущен корректно, если:
- `docker compose ps` показывает healthy для критических сервисов;
- `deployment/scripts/smoke.sh` завершается успешно;
- открывается тестовый frontend;
- проходит авторизация через mock OTP;
- отправляются текстовые и файловые сообщения;
- внутренние URL возвращают 404 снаружи;
- TLS-сертификат действителен;
- логи не содержат токены, PII и тексты сообщений;
- настроены резервное копирование PostgreSQL и продление TLS.
Для формальной production-like приемки после этого пройдите контрольные этапы
из `deployment/RUNBOOK.ru.md`.
+9
View File
@@ -20,6 +20,15 @@ downgrade, or a mutable image tag during deployment.
## Gate 2 — VM hardening
On a fresh Ubuntu 24.04 VM, run:
```sh
sudo deployment/scripts/setup-vm.sh
```
Before setting `HARDEN_SSH=true`, verify key-based access in a separate SSH
session. The script header documents its parameters and safe defaults.
- [ ] Ubuntu 24.04, NTP, unattended security updates and disk alerts are active.
- [ ] Key-only deploy account works in a second session; root/password SSH is off.
- [ ] UFW/cloud SG and `DOCKER-USER` policy survive reboot.
+222
View File
@@ -0,0 +1,222 @@
# Инструкция по развертыванию HAN Chat в production-like окружении
Это исполняемый чек-лист для контура на одной виртуальной машине. PostgreSQL и S3
используются как внешние управляемые сервисы. Во время развертывания запрещено
использовать `docker compose down -v`, откат миграций Alembic и изменяемые теги образов.
Подробная пошаговая инструкция для первого запуска находится в
`deployment/DEPLOYMENT_GUIDE.ru.md`.
## Этап 0 — решения и зоны ответственности
- [ ] Зафиксированы SHA/дайджесты релиза, окно обслуживания, дежурный и ответственный за откат.
- [ ] Согласованы RPO/RTO; начальные цели: RPO PostgreSQL не более 15 минут и RTO не более 4 часов.
- [ ] Выбран удаленный OTLP-бэкенд либо принято ограничение на использование только отладочного контура.
- [ ] Явно приняты риски mock OTP, заглушки Safety и заглушки bitrix-sync.
## Этап 1 — VPC, DNS и группы безопасности
- [ ] Управляемый PostgreSQL имеет только приватную точку доступа и принимает трафик от группы безопасности VM.
- [ ] Из интернета доступны только TCP-порты VM 80/443; SSH ограничен VPN или CIDR администраторов.
- [ ] Порты 6379, 4317/4318, 8000, 8080 и 9000 закрыты для внешнего доступа.
- [ ] DNS-запись `A` для `PUBLIC_HOST` указывает на VM; исходящий HTTPS доступен.
## Этап 2 — защита виртуальной машины
На новой Ubuntu 24.04 можно выполнить подготовительный скрипт:
```sh
sudo deployment/scripts/setup-vm.sh
```
Перед включением `HARDEN_SSH=true` обязательно проверьте вход по ключу в отдельной
SSH-сессии. Параметры запуска и безопасные значения по умолчанию описаны в начале скрипта.
- [ ] Установлена Ubuntu 24.04; работают NTP, автоматические обновления безопасности и оповещения о заполнении диска.
- [ ] Вход учетной записью развертывания по ключу проверен во второй сессии; вход root и SSH по паролю отключены.
- [ ] Правила UFW/облачной группы безопасности и политика `DOCKER-USER` сохраняются после перезагрузки.
- [ ] Docker Engine и Compose поддерживают `include` и полную форму `env_file`.
## Этап 3 — управляемый PostgreSQL
- [ ] Включены ежедневные резервные копии, PITR, защита от удаления, шифрование и оповещения.
- [ ] CA-сертификат провайдера установлен по пути `PG_CA_HOST_PATH`; все DSN используют `verify-full`.
- [ ] Для схем `han_app`, `bitrix_local`, `bitrix_sync`, `message_safety`, `keycloak`
созданы отдельные роли миграций и выполнения; запрет лишних прав проверен тестами.
- [ ] Миграции проверены на пустой БД и на клоне БД предыдущего релиза.
## Этап 4 — Selectel S3
- [ ] Бакеты карантина, вложений и документов закрыты от публичного доступа и зашифрованы.
- [ ] Права API ограничены префиксами; учетные данные Safety имеют доступ к карантину только на чтение.
- [ ] CORS бакетов разрешает только точный HTTPS-origin браузера и необходимые заголовки PUT.
- [ ] Срок хранения карантина превышает время Safety polling/recovery; политика хранения данных согласована.
## Этап 5 — неизменяемый релиз
- [ ] Репозиторий переключен на утвержденный SHA в detached-режиме; рабочее дерево чистое.
- [ ] Образы сервисов неизменяемы и просканированы; нерешенных критических и высоких уязвимостей нет.
- [ ] Корневой `docker-compose.yml` является единственной точкой запуска.
## Этап 6 — окружение и секреты
```sh
umask 077
cp .env.example .env
chmod 600 .env
# Замените заглушки через защищенный редактор или менеджер секретов.
./scripts/validate-env .env
docker compose --env-file .env config --quiet
```
- [ ] Парные токены совпадают, PostgreSQL проверяет TLS, публичные URL используют HTTPS.
- [ ] Риск mock OTP принят; все секреты уникальны и содержат не менее 128 бит энтропии.
- [ ] Установлено `FRONTEND_DEV_PROXY_ENABLED=false`; таймауты Safety и nginx согласованы.
## Этап 7 — образы и статический frontend
```sh
docker compose --env-file .env pull
docker compose --env-file .env build --pull frontend-static nginx redis
docker compose --env-file .env run --rm frontend-static
```
- [ ] Экспорт frontend проверен и просканирован, затем скопирован сервисом `frontend-static` в именованный volume.
- [ ] Артефакты сборки не содержат секретов и непредусмотренных source map.
- [ ] На диске VM остается не менее 30% свободного места.
## Этап 8 — топология
```sh
docker compose --env-file .env config --services
python3 -m unittest discover -s tests -v
```
- [ ] Только nginx публикует `80:80` и `443:443`; сервиса PostgreSQL в Compose нет.
- [ ] Присутствуют volumes Redis AOF/RDB/ACL и постоянной очереди OTEL.
- [ ] Сети `backend` и `observability` являются внутренними.
## Этап 9 — первоначальная настройка ACME/TLS
Установите `NGINX_TLS_ENABLED=false` только для команды первоначального запуска:
```sh
NGINX_TLS_ENABLED=false docker compose --env-file .env up -d nginx
docker compose --profile certbot run --rm certbot certonly \
--webroot -w /var/www/certbot -d "$PUBLIC_HOST" \
--cert-name "$PUBLIC_HOST" --email "$ACME_EMAIL" \
--agree-tos --no-eff-email --non-interactive
docker compose --env-file .env up -d --force-recreate nginx
docker compose exec -T nginx nginx -t -c /tmp/nginx.conf
```
Сначала выполните проверку с параметром Certbot `--staging`. Установите systemd-таймер,
запускающий `deployment/scripts/ssl-renew.sh` дважды в сутки, и проверьте
`certbot renew --dry-run`. Включайте HSTS только после проверки цепочки сертификатов,
имени хоста, перенаправления и поддержки TLS 1.2/1.3.
## Этап 10 — миграции и начальные данные
Создайте у провайдера точку восстановления PITR, затем выполните:
```sh
PITR_MARKER_CONFIRMED=true deployment/scripts/migrate.sh
deployment/scripts/seed.sh
```
- [ ] Активны ожидаемые ревизии Alembic; runtime-пользователи не выполняли DDL.
- [ ] Повторный seed завершается успешно; обязательные настройки не содержат секретов.
- [ ] Схема остается обратно совместимой с образами предыдущего релиза.
## Этап 11 — Keycloak
```sh
docker compose up -d keycloak
docker compose ps keycloak
```
- [ ] Issuer discovery/JWKS точно совпадает с публичным HTTPS URL `/auth`.
- [ ] Frontend-клиент является публичным PKCE S256; implicit, password и social flows отключены.
- [ ] Неверный или повторно использованный OTP и превышение лимитов безопасно отклоняются; settings bridge работает fail-closed.
- [ ] Временный администратор удален либо его пароль изменен; для именного администратора включена MFA.
## Этап 12 — последовательный запуск и готовность
```sh
docker compose up -d redis
docker compose up -d keycloak otel-collector
docker compose up -d message-safety
docker compose up -d api-backend
docker compose up -d bitrix-local-app bitrix-sync
docker compose up -d nginx
docker compose ps
```
- [ ] Нет циклических перезапусков и OOM; критические readiness-проверки успешны.
- [ ] Сохраняется только документированная деградация: Bitrix не установлен и bitrix-sync работает как заглушка.
- [ ] Внешний запрос `/internal/*` возвращает 404; OTEL принимает телеметрию.
## Этап 13 — Bitrix24
- [ ] URL установки, обработчика и placement используют точные публичные HTTPS-пути.
- [ ] Коннектор `han_mobile_app` активен в Открытой линии 8; события привязаны однократно.
- [ ] OAuth зашифрован; callback-, application- и service-токены не попадают в логи.
- [ ] Исходящие сообщения и ответы оператора идемпотентны; внутренний статус не опубликован наружу.
## Этап 14 — smoke- и E2E-тесты
```sh
deployment/scripts/smoke.sh
```
- [ ] Успешны сценарии гостя, OTP/PKCE/bootstrap/session, обновления токена и выхода.
- [ ] Проверены Safety allow/deny/pending/timeout и один параллельный медленный poll.
- [ ] Проверены карантин, перенос и удаление файлов, скачивание только владельцем и аудит.
- [ ] Проверены переподключение WS с REST-сверкой, 404 при обращении к чужому ресурсу, идемпотентность и 429.
- [ ] Логи не содержат PII, текстов сообщений, токенов и query-параметров presigned URL.
## Этап 15 — наблюдаемость
- [ ] Известный request ID связывает трассировку nginx, API и downstream-сервисов; UX ID не используется как label.
- [ ] Все три сигнала поступают в выбранный бэкенд; SLO-запросы и оповещения проверены.
- [ ] При недоступности удаленного сервиса ограниченная постоянная очередь заполняется и опустошается без остановки бизнес-функций.
- [ ] Тестовые секреты и PII отсутствуют; проверены метрики перезапуска, потерь, отказов и очереди Collector.
Только для локальной приемки запустите отладочный Collector с удалением чувствительных данных:
`docker compose --profile observability-local up -d otel-collector-local`.
## Этап 16 — открытие трафика
- [ ] Этапы 0–15 подписаны; имеются свежие подтверждения backup/PITR и предыдущие образы.
- [ ] HSTS включен; дайджесты релиза, версии схем и realm зафиксированы.
- [ ] Активных инцидентов нет; дежурный и владелец продукта приняли ограничения заглушек.
- [ ] В течение 60 минут контролируются 5xx, auth, доставка, БД, Redis, OOM, очередь OTEL и Bitrix.
## Резервное копирование и восстановление
Основным механизмом являются backup/PITR провайдера. Дополнительный проверенный логический дамп:
```sh
PG_BACKUP_DSN='postgresql://...?...sslmode=verify-full&sslrootcert=...' \
deployment/scripts/backup.sh /opt/han-chat/backups
```
Ежеквартально восстанавливайте PostgreSQL и S3 в изолированной VPC, развертывайте те же
дайджесты образов, не направляйте туда production DNS и callbacks Bitrix, выполняйте
smoke-тесты и фиксируйте фактические RPO/RTO. Redis можно восстановить пустым:
его AOF/RDB не является резервной копией бизнес-данных.
## Откат
Откатывайтесь только на образы, совместимые с текущей схемой:
```sh
SCHEMA_BACKWARD_COMPATIBLE_CONFIRMED=true \
deployment/scripts/rollback.sh /secure/path/previous-release.env
ENV_FILE=/secure/path/previous-release.env deployment/scripts/smoke.sh
```
Никогда не выполняйте downgrade Alembic. После обратно несовместимой миграции используйте
исправление вперед либо согласованный PITR с восстановлением S3 и сверкой Bitrix во время
технического обслуживания. Всегда проверяйте outbox, inbox и recovery, чтобы сообщение
с неопределенным статусом не было отправлено повторно.
@@ -0,0 +1,492 @@
#!/usr/bin/env bash
# Первичная подготовка Ubuntu 24.04 для HAN Chat.
#
# Скрипт настраивает только VM: пользователя развертывания, базовые пакеты,
# Docker/Compose, UFW, fail2ban, DOCKER-USER, swap и каталоги проекта.
# PostgreSQL и S3 остаются внешними управляемыми сервисами. Скрипт не создает
# .env, секреты, DNS, S3-бакеты, схемы БД и TLS-сертификаты.
#
# Запуск на свежей VM:
# chmod +x deployment/scripts/setup-vm.sh
# sudo deployment/scripts/setup-vm.sh
#
# Основные параметры:
# DEPLOY_USER=deploy
# DEPLOY_DIR=/opt/han-chat/backend
# SSH_PORT=22
# TIMEZONE=Europe/Moscow
# SWAP_SIZE_GB=4
# EXTERNAL_IF=ens3
# PUBLIC_DOCKER_PORTS=80,443
# COPY_SSH_KEYS=true
# HARDEN_SSH=false
# RESET_UFW=false
# SKIP_APT_UPGRADE=false
#
# HARDEN_SSH=true разрешено использовать только после проверки входа по ключу
# в отдельной SSH-сессии. По умолчанию парольный вход не отключается.
set -Eeuo pipefail
IFS=$'\n\t'
DEPLOY_USER="${DEPLOY_USER:-deploy}"
DEPLOY_DIR="${DEPLOY_DIR:-/opt/han-chat/backend}"
SSH_PORT="${SSH_PORT:-22}"
TIMEZONE="${TIMEZONE:-Europe/Moscow}"
SWAP_SIZE_GB="${SWAP_SIZE_GB:-4}"
EXTERNAL_IF="${EXTERNAL_IF:-}"
PUBLIC_DOCKER_PORTS="${PUBLIC_DOCKER_PORTS:-80,443}"
COPY_SSH_KEYS="${COPY_SSH_KEYS:-true}"
HARDEN_SSH="${HARDEN_SSH:-false}"
RESET_UFW="${RESET_UFW:-false}"
SKIP_APT_UPGRADE="${SKIP_APT_UPGRADE:-false}"
LOG_FILE="${LOG_FILE:-/var/log/han-chat-vm-setup.log}"
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE"
}
step() {
log ""
log "==> $*"
}
die() {
log "ОШИБКА: $*"
exit 1
}
on_error() {
local exit_code=$?
log "ОШИБКА: команда завершилась с кодом ${exit_code}, строка ${BASH_LINENO[0]}"
exit "$exit_code"
}
trap on_error ERR
require_root() {
[[ "${EUID:-$(id -u)}" -eq 0 ]] || die "Запустите скрипт через sudo"
}
validate_parameters() {
[[ "$DEPLOY_USER" =~ ^[a-z_][a-z0-9_-]*$ ]] || die "Некорректный DEPLOY_USER"
[[ "$DEPLOY_DIR" == /* ]] || die "DEPLOY_DIR должен быть абсолютным путем"
[[ "$SSH_PORT" =~ ^[0-9]+$ ]] || die "SSH_PORT должен быть числом"
((SSH_PORT >= 1 && SSH_PORT <= 65535)) || die "SSH_PORT вне диапазона"
[[ "$SWAP_SIZE_GB" =~ ^[0-9]+$ ]] || die "SWAP_SIZE_GB должен быть целым числом"
[[ "$PUBLIC_DOCKER_PORTS" =~ ^[0-9]+(,[0-9]+)*$ ]] \
|| die "PUBLIC_DOCKER_PORTS должен иметь вид 80,443"
}
check_os() {
step "Проверка операционной системы"
[[ -r /etc/os-release ]] || die "Не найден /etc/os-release"
# shellcheck disable=SC1091
source /etc/os-release
[[ "${ID:-}" == "ubuntu" ]] || die "Поддерживается только Ubuntu"
local major="${VERSION_ID%%.*}"
((major >= 24)) || die "Требуется Ubuntu 24.04 или новее"
log "Обнаружена ${PRETTY_NAME}"
}
update_system() {
step "Обновление системы и установка пакетов"
export DEBIAN_FRONTEND=noninteractive
apt-get update
if [[ "$SKIP_APT_UPGRADE" != "true" ]]; then
apt-get dist-upgrade -y
fi
apt-get install -y \
ca-certificates \
curl \
dos2unix \
fail2ban \
git \
gnupg \
iptables \
jq \
logrotate \
netcat-openbsd \
openssl \
python3 \
python3-venv \
rsync \
unattended-upgrades \
ufw
apt-get autoremove -y
}
configure_time() {
step "Настройка времени"
timedatectl set-timezone "$TIMEZONE"
timedatectl set-ntp true
}
create_deploy_user() {
step "Пользователь развертывания"
if ! id "$DEPLOY_USER" >/dev/null 2>&1; then
useradd --create-home --shell /bin/bash "$DEPLOY_USER"
log "Создан пользователь ${DEPLOY_USER}"
else
log "Пользователь ${DEPLOY_USER} уже существует"
fi
install -d -m 700 -o "$DEPLOY_USER" -g "$DEPLOY_USER" \
"/home/${DEPLOY_USER}/.ssh"
local source_user="${SUDO_USER:-}"
local source_keys=""
local target_keys="/home/${DEPLOY_USER}/.ssh/authorized_keys"
if [[ -n "$source_user" && "$source_user" != "root" ]]; then
source_keys="/home/${source_user}/.ssh/authorized_keys"
fi
if [[ "$COPY_SSH_KEYS" == "true" && ! -s "$target_keys" && -s "$source_keys" ]]; then
install -m 600 -o "$DEPLOY_USER" -g "$DEPLOY_USER" "$source_keys" "$target_keys"
log "SSH-ключи скопированы от ${source_user}"
fi
if [[ ! -s "$target_keys" ]]; then
log "ПРЕДУПРЕЖДЕНИЕ: у ${DEPLOY_USER} отсутствует authorized_keys"
fi
}
configure_layout() {
step "Каталоги HAN Chat"
install -d -m 755 -o "$DEPLOY_USER" -g "$DEPLOY_USER" "$DEPLOY_DIR"
install -d -m 700 -o "$DEPLOY_USER" -g "$DEPLOY_USER" \
"${DEPLOY_DIR}/secrets" \
"${DEPLOY_DIR}/secrets/pg" \
"${DEPLOY_DIR}/backups"
local env_file="${DEPLOY_DIR}/.env"
if [[ -f "$env_file" ]]; then
chown "$DEPLOY_USER:$DEPLOY_USER" "$env_file"
chmod 600 "$env_file"
fi
}
configure_swap() {
step "Настройка swap"
if ((SWAP_SIZE_GB == 0)); then
log "Создание swap отключено"
return
fi
if swapon --show=NAME --noheadings | grep -qx '/swapfile'; then
log "Swap уже подключен"
return
fi
if [[ ! -f /swapfile ]]; then
fallocate -l "${SWAP_SIZE_GB}G" /swapfile
chmod 600 /swapfile
mkswap /swapfile
fi
swapon /swapfile
grep -q '^/swapfile ' /etc/fstab \
|| printf '/swapfile none swap sw 0 0\n' >>/etc/fstab
printf 'vm.swappiness = 10\n' >/etc/sysctl.d/99-han-chat-swappiness.conf
sysctl --system >/dev/null
}
configure_sysctl() {
step "Настройка сетевого стека"
cat >/etc/sysctl.d/99-han-chat-hardening.conf <<'EOF'
net.ipv4.ip_forward = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.tcp_fin_timeout = 30
EOF
sysctl --system >/dev/null
}
install_docker() {
step "Установка Docker Engine и Compose"
if ! command -v docker >/dev/null 2>&1; then
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
# shellcheck disable=SC1091
source /etc/os-release
printf '%s\n' \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu ${VERSION_CODENAME} stable" \
>/etc/apt/sources.list.d/docker.list
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
fi
install -d -m 755 /etc/docker
cat >/etc/docker/daemon.json <<'EOF'
{
"live-restore": true,
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "5"
},
"userland-proxy": false
}
EOF
systemctl enable --now docker
systemctl restart docker
usermod -aG docker "$DEPLOY_USER"
docker compose version >/dev/null \
|| die "Docker Compose plugin не установлен"
log "$(docker --version)"
log "$(docker compose version)"
}
configure_ufw() {
step "Настройка UFW"
if [[ "$RESET_UFW" == "true" ]]; then
ufw --force reset
fi
ufw default deny incoming
ufw default allow outgoing
ufw allow "${SSH_PORT}/tcp" comment 'HAN Chat SSH'
ufw limit "${SSH_PORT}/tcp" comment 'HAN Chat SSH rate limit'
ufw allow 80/tcp comment 'HAN Chat HTTP'
ufw allow 443/tcp comment 'HAN Chat HTTPS'
ufw logging medium
ufw --force enable
}
configure_fail2ban() {
step "Настройка fail2ban для SSH"
cat >/etc/fail2ban/jail.d/han-chat.local <<EOF
[DEFAULT]
bantime = 2h
findtime = 10m
maxretry = 5
backend = systemd
banaction = ufw
[sshd]
enabled = true
port = ${SSH_PORT}
maxretry = 3
EOF
systemctl enable --now fail2ban
systemctl restart fail2ban
}
configure_unattended_upgrades() {
step "Автоматические обновления безопасности"
cat >/etc/apt/apt.conf.d/51han-chat-unattended <<'EOF'
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
EOF
dpkg-reconfigure -f noninteractive unattended-upgrades
systemctl enable --now unattended-upgrades
}
configure_docker_firewall() {
step "Фильтрация опубликованных Docker-портов"
cat >/etc/default/han-chat-docker-firewall <<EOF
EXTERNAL_IF=${EXTERNAL_IF}
PUBLIC_DOCKER_PORTS=${PUBLIC_DOCKER_PORTS}
EOF
cat >/usr/local/sbin/han-chat-docker-firewall <<'FIREWALL'
#!/usr/bin/env bash
set -Eeuo pipefail
# shellcheck disable=SC1091
source /etc/default/han-chat-docker-firewall
external_if="${EXTERNAL_IF:-}"
if [[ -z "$external_if" ]]; then
external_if="$(ip -4 route show default | awk '{print $5; exit}')"
fi
[[ -n "$external_if" ]] || {
echo "Не удалось определить внешний интерфейс; задайте EXTERNAL_IF" >&2
exit 1
}
iptables -N HAN-CHAT-DOCKER 2>/dev/null || true
iptables -F HAN-CHAT-DOCKER
iptables -A HAN-CHAT-DOCKER -m conntrack --ctstate RELATED,ESTABLISHED -j RETURN
iptables -A HAN-CHAT-DOCKER -i lo -j RETURN
IFS=',' read -ra ports <<<"$PUBLIC_DOCKER_PORTS"
for port in "${ports[@]}"; do
[[ "$port" =~ ^[0-9]+$ ]] || {
echo "Некорректный порт: $port" >&2
exit 1
}
iptables -A HAN-CHAT-DOCKER -i "$external_if" -p tcp --dport "$port" -j RETURN
done
# Блокируется только новый входящий трафик с внешнего интерфейса в Docker bridge.
# Исходящий и межконтейнерный трафик этой цепочкой не затрагивается.
iptables -A HAN-CHAT-DOCKER -i "$external_if" -o docker+ -j DROP
iptables -A HAN-CHAT-DOCKER -i "$external_if" -o br+ -j DROP
iptables -A HAN-CHAT-DOCKER -j RETURN
while iptables -C DOCKER-USER -j HAN-CHAT-DOCKER 2>/dev/null; do
iptables -D DOCKER-USER -j HAN-CHAT-DOCKER
done
iptables -I DOCKER-USER 1 -j HAN-CHAT-DOCKER
FIREWALL
chmod 750 /usr/local/sbin/han-chat-docker-firewall
cat >/etc/systemd/system/han-chat-docker-firewall.service <<'EOF'
[Unit]
Description=HAN Chat firewall for Docker published ports
After=docker.service network-online.target
Wants=docker.service network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/han-chat-docker-firewall
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
install -d -m 755 /etc/systemd/system/docker.service.d
cat >/etc/systemd/system/docker.service.d/han-chat-firewall.conf <<'EOF'
[Service]
ExecStartPost=-/usr/local/sbin/han-chat-docker-firewall
EOF
systemctl daemon-reload
systemctl enable --now han-chat-docker-firewall.service
}
configure_ssh() {
step "Проверка SSH hardening"
if [[ "$HARDEN_SSH" != "true" ]]; then
log "HARDEN_SSH=false: парольный вход не изменен"
return
fi
[[ -s "/home/${DEPLOY_USER}/.ssh/authorized_keys" ]] \
|| die "Нельзя включить HARDEN_SSH: authorized_keys пользователя пуст"
cat >/etc/ssh/sshd_config.d/99-han-chat.conf <<EOF
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
X11Forwarding no
AllowTcpForwarding no
MaxAuthTries 3
ClientAliveInterval 120
ClientAliveCountMax 2
Port ${SSH_PORT}
EOF
sshd -t || die "Проверка конфигурации sshd не пройдена"
systemctl reload ssh
log "SSH hardening включен"
}
install_ssl_timer_if_possible() {
step "Таймер продления TLS"
local renew_script="${DEPLOY_DIR}/deployment/scripts/ssl-renew.sh"
if [[ ! -x "$renew_script" ]]; then
log "Проект еще не скопирован: таймер TLS будет установлен при повторном запуске"
return
fi
cat >/etc/systemd/system/han-chat-ssl-renew.service <<EOF
[Unit]
Description=Renew HAN Chat TLS certificate
After=docker.service
[Service]
Type=oneshot
User=${DEPLOY_USER}
WorkingDirectory=${DEPLOY_DIR}
ExecStart=${renew_script}
EOF
cat >/etc/systemd/system/han-chat-ssl-renew.timer <<'EOF'
[Unit]
Description=Run HAN Chat TLS renewal twice daily
[Timer]
OnCalendar=*-*-* 03,15:20:00
RandomizedDelaySec=30m
Persistent=true
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now han-chat-ssl-renew.timer
}
verify() {
step "Проверка результата"
local failed=0
systemctl is-active --quiet docker || { log "FAIL: Docker не активен"; failed=1; }
systemctl is-active --quiet fail2ban || { log "FAIL: fail2ban не активен"; failed=1; }
ufw status | grep -q 'Status: active' || { log "FAIL: UFW не активен"; failed=1; }
iptables -C DOCKER-USER -j HAN-CHAT-DOCKER 2>/dev/null \
|| { log "FAIL: цепочка HAN-CHAT-DOCKER не подключена"; failed=1; }
docker compose version >/dev/null || { log "FAIL: Compose недоступен"; failed=1; }
[[ -d "$DEPLOY_DIR" ]] || { log "FAIL: отсутствует ${DEPLOY_DIR}"; failed=1; }
((failed == 0)) || die "Базовая проверка VM не пройдена"
log "Базовая проверка VM пройдена"
}
summary() {
step "Настройка VM завершена"
cat <<EOF | tee -a "$LOG_FILE"
Пользователь развертывания: ${DEPLOY_USER}
Каталог Compose: ${DEPLOY_DIR}
Открытые порты: ${SSH_PORT}, 80, 443
Лог настройки: ${LOG_FILE}
Следующие действия:
1. Проверьте вход в новой SSH-сессии:
ssh ${DEPLOY_USER}@<VM_IP>
2. Скопируйте содержимое codebase/backend в:
${DEPLOY_DIR}
3. Поместите CA PostgreSQL:
${DEPLOY_DIR}/secrets/pg/ca.pem
4. Создайте и заполните окружение:
cd ${DEPLOY_DIR}
cp .env.example .env
chmod 600 .env
./scripts/validate-env .env
5. Продолжите с Gate 7 в:
deployment/RUNBOOK.ru.md
6. После копирования проекта повторно запустите этот скрипт для установки TLS-таймера.
Важно: членство в группе docker начнет действовать после нового входа в систему.
EOF
}
main() {
require_root
install -d -m 755 "$(dirname "$LOG_FILE")"
touch "$LOG_FILE"
chmod 600 "$LOG_FILE"
validate_parameters
check_os
update_system
configure_time
create_deploy_user
configure_layout
configure_swap
configure_sysctl
install_docker
configure_ufw
configure_fail2ban
configure_unattended_upgrades
configure_docker_firewall
configure_ssh
install_ssl_timer_if_possible
verify
summary
}
main "$@"