Внедрение KESL на ВМ2 + замена CLAMAV на KESL

This commit is contained in:
mi
2026-09-08 01:39:37 +03:00
parent 85df788f2d
commit fdfdeaffb4
43 changed files with 2210 additions and 329 deletions
+1 -1
View File
@@ -82,7 +82,7 @@
## Каноническое размещение production-контуров
- **ВМ1 HAN Chat** — самостоятельная публичная точка входа приложения: nginx, `api-backend`, Keycloak, `bitrix-local-app`, SMS-контур, Redis DB0/DB1 и локальный OTEL Collector.
- **ВМ2 Processing** — самостоятельная service VM с отдельным public webhook host, private Message Safety ingress и постоянным ограниченным egress: `message-safety`, `bitrix-sync`, `clamd`/`freshclam`, отдельный Redis Safety, nginx и локальный OTEL Collector.
- **ВМ2 Processing** — самостоятельная service VM с отдельным public webhook host и private Message Safety ingress: в root Compose работают `message-safety`, `bitrix-sync`, отдельный Redis Safety, nginx и локальный OTEL Collector; на host работают KESL 12.4 standalone и root-owned fail-closed broker с Unix socket `/run/han-kesl/scan.sock`. `clamd`/`freshclam` в Compose отсутствуют.
- На каждой VM действует один root Compose project и отдельный root-owned systemd deployment unit. «Единый Compose» означает один проект **на VM**, а не один общий project через несколько хостов.
- Bitrix24 вызывает CRM webhook напрямую на nginx ВМ2; ВМ1 в route не участвует. ВМ1 вызывает только Message Safety по private HTTPS.
- При росте нагрузки `bitrix-sync` может быть перенесён на ВМ3 без изменения API и границ схем PostgreSQL.
+4 -3
View File
@@ -63,7 +63,7 @@ HAN Chat - приложение для мигрантов, где стартов
Production-like backend разделён на два контура в одной private network/VPC:
- **ВМ1 HAN Chat**: edge `nginx`, `api-backend`, `keycloak`, `sms-service`/worker, `bitrix-local-app`, Redis DB0/DB1 и локальный `otel-collector`;
- **ВМ2 Processing**: собственный public/private `nginx`, `message-safety` API/worker, `clamd`/`freshclam`, `bitrix-sync`, отдельный Redis Safety и локальный `otel-collector`;
- **ВМ2 Processing**: в root Compose — собственный public/private `nginx`, `message-safety` API/worker, `bitrix-sync`, отдельный Redis Safety и локальный `otel-collector`; на host — KESL 12.4 standalone и root-owned fail-closed broker с `/run/han-kesl/scan.sock`;
- каждая VM имеет один root Compose project и отдельный root-owned systemd deployment unit;
- ВМ1 и ВМ2 имеют независимые public DNS/TLS ingress на своих nginx; ВМ2 публикует только exact CRM webhook;
- ВМ1 вызывает ВМ2 по private HTTPS с проверкой internal CA, service token, cloud SG и host firewall;
@@ -97,7 +97,8 @@ flowchart LR
api -->|"HTTPS 8443 + service token"| privateGateway[VM2_PrivateListener]
privateGateway --> safety[MessageSafetyApi]
safety --> worker[SafetyWorker]
worker --> clamd[Clamd]
worker -->|"Unix socket /run/han-kesl/scan.sock"| keslBroker[KESLBroker]
keslBroker --> hostKesl[HostKESL12_4]
worker --> s3q[S3Quarantine]
worker --> pg[ManagedPostgreSQL]
sync --> pg
@@ -590,7 +591,7 @@ App DB — **локальный кэш** для UI. Двусторонний syn
Минимальный целевой real-SMS контур разделён на два stack:
- ВМ1: `nginx`, `api-backend`, `keycloak`, `sms-service`/worker, `bitrix-local-app`, Redis DB0/DB1, `otel-collector`;
- ВМ2: nginx с public webhook/private internal server blocks, `message-safety` API/worker, `clamd`/`freshclam`, `bitrix-sync`, Redis Safety, `otel-collector`.
- ВМ2: nginx с public webhook/private internal server blocks, `message-safety` API/worker, `bitrix-sync`, Redis Safety, `otel-collector` в Compose; KESL 12.4 standalone и root-owned broker на host. `clamd`/`freshclam` удалены из Compose.
До SMS rollout сервисы SMS могут отсутствовать, но Keycloak обязан оставаться в mock mode.
@@ -64,7 +64,6 @@ processing/
nginx-internal/docker-compose.yml
message-safety/docker-compose.yml
bitrix-sync/docker-compose.yml
clamav/docker-compose.yml
redis/docker-compose.yml
observability/docker-compose.yml
```
@@ -93,7 +92,7 @@ volumes:
redis-data:
```
Root Compose ВМ2 включает собственный nginx с public/private server blocks, Message Safety API/worker, `clamd`/`freshclam`, `bitrix-sync`, Redis Safety и локальный OTEL Collector. Секреты, сети и volumes двух projects не общие.
Root Compose ВМ2 включает собственный nginx с public/private server blocks, Message Safety API/worker, `bitrix-sync`, Redis Safety и локальный OTEL Collector. `clamd`/`freshclam` удалены из Compose; KESL 12.4 standalone и root-owned fail-closed broker работают на host VM2. Секреты, сети и volumes двух projects не общие.
### Правила для сервисных compose-файлов
@@ -254,6 +253,7 @@ Python FastAPI backend.
- runtime role читает immutable active `message_safety.config_versions`; создавать/активировать config может только отдельный migration/config-admin job;
- использует локальный Redis Safety только для hot cache/rate/wakeup; PostgreSQL владеет task queue/leases;
- запускает async workers для file scan из S3-quarantine;
- worker монтирует только Unix socket `/run/han-kesl/scan.sock` root-owned broker; KESL и `kesl-control` остаются на host, TCP scanner port отсутствует;
- API container получает read-only
`/etc/han-chat/message-safety-mode.env` с host contract
`root:han-message-safety 0640` и GID `10001`; менять его и перезапускать
@@ -262,29 +262,26 @@ Python FastAPI backend.
- экспортирует traces/logs в `otel-collector`;
- таймауты: POST check 5 s, GET task 2 s, file scan 60 s (см. [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), переменные `MESSAGE_SAFETY_*`).
### ClamAV на ВМ2
### Host KESL и broker на ВМ2
`clamd` и `freshclam` используют один immutable image digest, но разные
security-профили:
KESL 12.4 работает standalone на host и не является Compose service. Отдельный
root-owned custom broker слушает только `/run/han-kesl/scan.sock` и вызывает
фиксированный `kesl-control --scan-file --action Inform`.
- оба запускаются через vendor `init-unprivileged`, а не root entrypoint;
- `clamd` читает volume signatures read-only, не подключён к signature CDN и
имеет healthcheck daemon socket;
- `freshclam` один пишет в signatures и имеет только разрешённый egress к CDN;
- `/run/clamav` — отдельный runtime volume, `/var/log/clamav` и `/tmp`
ограниченные tmpfs с UID/GID ClamAV;
- `freshclam` работает как foreground daemon с заданным interval; inherited
healthcheck `clamd` отключён, потому что updater не поднимает daemon socket;
- работоспособность updater подтверждается состоянием `Up`, отсутствием
restart loop и отдельным контролем возраста/signature version, а не
искусственным container healthcheck.
- Архитектурная верхняя граница допустимого возраста signatures — `720` часов
(30 дней); активный seed-порог `max_signature_age_hours``240` часов
(10 дней), то есть строже предельного значения.
- socket монтируется только в Message Safety worker и доступен выделенной
группе; worker не получает host binary, shell, Docker socket или host root;
- broker принимает только bounded scan request и fail-closed сопоставляет
`clean → allow`, `infected → deny`, а timeout, stale database, неизвестный
output/exit code и недоступность KESL → retry/`503`;
- `scanner_engine=kesl`; `signatures_version` — hash KESL version + database
date;
- KESL обновляет database ежечасно на host; update egress не подключает
контейнеры к internet;
- schema допускает `max_signature_age_hours` до `720`, seed — `240`.
Смена digest ClamAV требует повторной проверки entrypoint, UID/GID, writable
paths, `clamd` health и фактического обновления signatures. Нельзя менять
только tag/digest, считая security contract image неизменным.
Формат результата `kesl-control`, socket permissions, cleanup и throughput
этой custom integration подтверждаются gates на target VM2 с фактическим KESL
12.4; repository-only/Compose health не считается достаточным evidence.
### bitrix-sync
@@ -400,9 +397,9 @@ Identity provider. **Обязателен** в compose-контуре с пер
- ВМ1 `public`: edge nginx, Keycloak proxy и frontend entrypoint.
- ВМ1 `backend`: `api-backend`, `bitrix-local-app`, Keycloak, SMS API и Redis DB0/DB1.
- ВМ2 `backend`: nginx, Safety API/worker, `bitrix-sync`, `clamd` и Redis Safety.
- ВМ2 `backend`: nginx, Safety API/worker, `bitrix-sync` и Redis Safety; broker доступен worker только через host Unix socket.
- ВМ1 `egress` подключается только к процессам с назначением: `sms-worker` → i-Digital Direct; Keycloak → SmartCaptcha только при включённом feature flag; `api-backend` → S3 и private PG, а вызов Safety идёт к `processing.internal:8443`; local collector → private SigNoz. `sms-service` без совмещённого worker, `bitrix-local-app` и Redis не получают общий internet egress.
- ВМ2 `egress` подключается только к процессам с назначением: `freshclam` → signature CDN; `bitrix-sync` → утверждённый Bitrix portal; Safety worker → S3/PG/DNS; collector → private SigNoz. Общего internet egress у Safety API/clamd/Redis нет.
- ВМ2 `egress` подключается только к процессам с назначением: `bitrix-sync` → утверждённый Bitrix portal; Safety worker → S3/PG/DNS; collector → private SigNoz. Общего internet egress у Safety API/Redis нет; KESL update egress задаётся отдельно на host.
- `observability` существует отдельно на каждой VM и ведёт в её local collector.
Базы данных, Redis, OTLP receivers и internal service ports не публикуются. Cross-host calls идут через private network, точные SG и TLS.
@@ -412,8 +409,8 @@ Identity provider. **Обязателен** в compose-контуре с пер
Минимальные persistent volumes:
- ВМ1: Redis DB0/DB1 data, local OTEL queue;
- ВМ2: Redis Safety data (rebuildable), ClamAV signatures и runtime,
internal TLS secrets, local OTEL queue.
- ВМ2: Redis Safety data (rebuildable), internal TLS secrets, local OTEL queue.
KESL database/runtime и `/run/han-kesl` принадлежат host, не Compose volumes.
Public TLS и ACME на обеих VM — не named volumes. Root-only ACME state остаётся
на host в `/etc/letsencrypt`; root hook атомарно копирует только нужные
@@ -561,7 +558,7 @@ WAF не заменяет обязательные лимиты, валидац
- `nginx`: на веб-домене — `308` с `:80` на HTTPS; на API-домене (если выделен) — `:80` не слушает; `:443` — успешная TLS handshake и ожидаемый route response;
- `api-backend`: `/health/live` проверяет процесс; `/health/ready` проверяет PostgreSQL `han_app`, Redis DB0/DB1, JWKS/discovery Keycloak и S3 permissions. Недоступность remote Message Safety отражается как degraded dependency и блокирует только send path, но не readiness read API;
- `message-safety`: `/health/ready` возвращает process/core status и capability map `text|links|files|worker`; ClamAV/S3 не выключают text, DNS не выключает text без ссылок, Redis hot cache не является core gate;
- `message-safety`: `/health/ready` возвращает process/core status и capability map `text|links|files|worker`; KESL broker/stale database/S3 не выключают text, DNS не выключает text без ссылок, Redis hot cache не является core gate;
- `bitrix-sync`: `/health/live` проверяет процесс; `/health/ready` проверяет validated config/secrets, PostgreSQL/grants, worker/limiter state и CRM webhook config; invalid credential/config даёт not-ready, краткая CRM outage — degraded по stale policy; при `BITRIX_SYNC_ENABLED=false` ready возвращает not-ready `sync_disabled`;
- `bitrix-local-app`: `/health/live` проверяет процесс; `/health/ready` показывает PostgreSQL, OAuth-токены после установки приложения, connector activation и возможность forward в API при включённом `BITRIX_API_FORWARD_URL`;
- `keycloak`: health endpoint Keycloak; readiness — подключение к managed PostgreSQL;
@@ -591,8 +588,8 @@ host-side secrets/TLS materialized, controlled migrations и seed заверше
ВМ2 запускается в порядке:
1. `redis-safety`, `otel-queue-init`, затем local `otel-collector`;
2. `freshclam`, затем `clamd` до состояния healthy;
1. operator KESL runbook: KESL 12.4/database update, затем enable/start broker socket и проверка status/permissions;
2. `redis-safety`, `otel-queue-init`, затем local `otel-collector`;
3. Message Safety API/worker и `bitrix-sync`;
4. nginx — последним, после успешного config test;
5. private HTTPS ВМ1→ВМ2 и capability health проверяются до cutover.
+3 -4
View File
@@ -199,7 +199,7 @@ worker.lease_seconds=90
## Service-owned настройки `message-safety`
Runtime policy хранится в версионированной `message_safety.config_versions`, а не в `.env` и не в `han_app.app_settings`. Сюда входят task lease/deadline/attempts, internal rate/pending limits, retention/cache TTL, URL/DNS pipeline limits, ClamAV policy timeout/signature age и enabled file MIME/size policy. Для ClamAV schema допускает возраст сигнатур не более `720` часов (30 дней), seed `max_signature_age_hours` равен `240` часам (10 дней). Полный schema/seed/activation contract — module-05 §10.1 и §15.
Runtime policy хранится в версионированной `message_safety.config_versions`, а не в `.env` и не в `han_app.app_settings`. Сюда входят task lease/deadline/attempts, internal rate/pending limits, retention/cache TTL, URL/DNS pipeline limits, KESL broker timeout/database age и enabled file MIME/size policy. Schema допускает возраст базы KESL не более `720` часов (30 дней), seed `max_signature_age_hours` равен `240` часам (10 дней). Полный schema/seed/activation contract — module-05 §10.1 и §15.
Seed, JSON Schema и referenced artifacts входят в immutable Message Safety
image. Их изменение требует одновременно нового pinned image digest и новой
@@ -211,7 +211,7 @@ active row на месте. Rollback выполняется новой config ve
`han_app.app_settings:chat.attachments.*` остаётся бизнес-настройкой api-backend. Message Safety не получает cross-schema read к `han_app`; файл допускается только при пересечении business allow-list, active safety policy и immutable detector manifest. Active policy может сузить manifest, но не добавить parser и не увеличить hard limit.
В env Message Safety остаются только bootstrap/topology/capacity (`APP_ENV`, worker concurrency, DNS resolver, ClamAV/S3/OTLP endpoints); credentials доставляются secret files. Rules/detector versions вычисляются/проверяются по immutable artifacts. Emergency MOCK остаётся в отдельном read-only bind file `root:han-message-safety 0640` с dedicated GID контейнера и намеренно не переносится в БД.
В env Message Safety остаются только bootstrap/topology/capacity (`APP_ENV`, worker concurrency, DNS resolver, Unix socket KESL broker, S3/OTLP endpoints); credentials доставляются secret files. Rules/detector versions вычисляются/проверяются по immutable artifacts. `scanner_engine=kesl`; `signatures_version` вычисляется как hash KESL version + database date. Emergency MOCK остаётся в отдельном read-only bind file `root:han-message-safety 0640` с dedicated GID контейнера и намеренно не переносится в БД.
---
@@ -379,8 +379,7 @@ HAN_APP_SAFETY_RECOVERY_MAX_SEC=1200
# /etc/han-chat/message-safety-mode.env и меняется approved helper-ом.
MESSAGE_SAFETY_WORKER_CONCURRENCY=5
MESSAGE_SAFETY_DNS_RESOLVERS=<VPC-resolver-IP>
MESSAGE_SAFETY_CLAMAV_HOST=clamd
MESSAGE_SAFETY_CLAMAV_PORT=3310
MESSAGE_SAFETY_KESL_SOCKET=/run/han-kesl/scan.sock
# =============================================================================
# Frontend (nginx)
@@ -62,7 +62,7 @@ SigNoz относится к этому классу, если его UI, OTLP
- public `443` разрешает только exact `/bitrix/sync/webhook/contact` и `/bitrix/sync/webhook/alert`; остальные paths закрыты;
- private ingress `8443/tcp` разрешён только от security group ВМ1 и утверждённого ops path для Message Safety/internal API;
- ни один public запрос ВМ2 не проходит через nginx ВМ1;
- `freshclam` имеет egress только к утверждённым источникам сигнатур;
- host KESL имеет egress только к утверждённым источникам обновления из operator KESL runbook; Message Safety и broker не получают общий internet egress;
- `bitrix-sync` имеет HTTPS egress только к утверждённому порталу Bitrix24;
- Safety worker имеет доступ только к managed PostgreSQL, S3-quarantine и доверенному DNS resolver;
- локальный OTEL Collector имеет private egress к SigNoz;
@@ -513,23 +513,28 @@ certificate/key проверяются preflight. Успешный hook обяз
при ненулевом exit code. Иначе Certbot/оркестратор может пометить успешный
renewal как hook error.
### Daemon и updater как разные security-профили
### Host KESL и root-owned broker
Если один vendor image используется для daemon и updater, им задаются разные
сети, mounts и health semantics. Проверенный паттерн ClamAV:
KESL 12.4 standalone и custom integration broker работают на host VM2, вне
Compose. Граница между non-root Message Safety worker и privileged host:
- `clamd` не имеет signature-CDN egress, читает signatures read-only и имеет
healthcheck реального daemon socket;
- `freshclam` один получает ограниченный egress и write к signatures;
- оба используют vendor `init-unprivileged` и только выделенные writable
`/run/clamav`, `/var/log/clamav` и `/tmp`;
- updater запускается как постоянный foreground daemon, чтобы restart policy
не превращала успешный one-shot exit в download loop/rate limit;
- унаследованный healthcheck, проверяющий отсутствующий в updater-контейнере
daemon, отключается; updater контролируется по `Up`, restart count, логам и
возрасту сигнатур;
- security policy разрешает настроить порог возраста не выше `720` часов
(30 дней); production-like seed использует более строгие `240` часов.
- root-owned broker слушает только Unix socket `/run/han-kesl/scan.sock`;
TCP listener и Docker socket запрещены;
- socket доступен только выделенной группе worker; owner/group/mode
проверяются после каждого restart/reboot;
- worker не получает `kesl-control`, shell или произвольный host path;
- broker принимает bounded request, создаёт контролируемый временный файл,
вызывает только `kesl-control --scan-file --action Inform` и гарантированно
очищает временные данные;
- только однозначный `clean` допускает allow; `infected` даёт deny; timeout,
stale database, неизвестный output/exit code и недоступность scanner дают
retry/`503`;
- KESL обновляет database ежечасно; update egress принадлежит host KESL и
ограничен approved sources;
- policy допускает `max_signature_age_hours` не выше `720`, seed — `240`.
Формат `kesl-control`, socket permissions, cleanup и throughput этой custom
integration подтверждаются gates на target VM2 с фактическим KESL 12.4.
Ошибки `read-only file system` устраняются точечным writable mount. Запрещено
лечить их глобальным `read_only: false`, root, `privileged` или broad
@@ -638,8 +643,8 @@ Fail2ban обязателен для SSH, временно или постоян
содержимому, а не только по декларации Compose;
- runtime/migration/config-admin DB roles разделены, временные cross-schema
grants выданы и отозваны владельцем;
- healthcheck проверяет процесс, реально присутствующий в контейнере, а
updater freshness контролируется отдельным сигналом;
- healthcheck проверяет процесс, реально присутствующий в контейнере, а KESL
broker/database freshness контролируется отдельным host-сигналом;
- healthcheck-команда и все её binaries подтверждены внутри exact pinned
digest; отсутствие `curl`/`wget` не обнаруживается впервые в production;
- внутренние ports недоступны извне;
+16 -7
View File
@@ -39,7 +39,7 @@
- **Safety Service Owner**: API/data contract, capacity result и v2 cutover/rollback sign-off.
- **Rule Pack Owner**: rules bundle, corpus, monitor report и version release.
- **Product Owner**: mnemonic `safety.chat.blocked` и business acceptance chat flow.
- **Operations Owner**: VM2 alerts, ClamAV signatures, incident/reprovision/restore rehearsal.
- **Operations Owner**: VM2 alerts, host KESL/broker и database updates, incident/reprovision/restore rehearsal.
```text
<PUBLIC_HOST> например chat.example.ru
@@ -93,7 +93,7 @@ Sizing и load gates конкретной машины — профильный
| ВМ2 collector | private SigNoz | TCP 4317 | allow |
| ВМ2 workers | S3 endpoints | TCP 443 | allow |
| ВМ2 `bitrix-sync` | approved Bitrix portal | TCP 443 | allow |
| ВМ2 `freshclam` | approved signature CDN | TCP 443/80 по vendor manifest | allow |
| host KESL ВМ2 | approved update sources по operator KESL runbook | vendor-required destinations/ports | allow |
| ВМ2 | trusted DNS/NTP | UDP/TCP 53, UDP 123 | allow |
| internet | managed PG | any | deny |
| internet | ВМ2 | any кроме nginx 80/443 | deny ingress |
@@ -237,11 +237,20 @@ Host ВМ1 — `<PUBLIC_HOST>`, ВМ2 — `<PROCESSING_PUBLIC_HOST>`; private `8
## 10. Сквозной порядок startup и cutover
1. На ВМ2 unit поднимает Redis Safety и local Collector.
2. Затем `clamd`/`freshclam`, Safety API/worker и `bitrix-sync`.
3. Последним на ВМ2 — nginx public `80/443` и private `8443`.
4. На ВМ1Redis/Collector, API, SMS, Keycloak, local app, edge nginx.
5. Только private `MESSAGE_SAFETY_URL` ВМ1 переключается на ВМ2 после Safety gates. Public CRM webhook DNS/routes ВМ2 не требуют изменения ВМ1.
1. На ВМ2 оператор выполняет KESL runbook: KESL 12.4 standalone, database update/canary.
2. Затем enable/start root-owned broker socket/service и проверяет status/permissions `/run/han-kesl/scan.sock`.
3. Unit ВМ2 поднимает Redis Safety и local Collector, затем Safety API/worker и `bitrix-sync`.
4. Последним на ВМ2nginx public `80/443` и private `8443`.
5. На ВМ1 — Redis/Collector, API, SMS, Keycloak, local app, edge nginx.
6. Только private `MESSAGE_SAFETY_URL` ВМ1 переключается на ВМ2 после Safety gates. Public CRM webhook DNS/routes ВМ2 не требуют изменения ВМ1.
Broker — custom integration: до cutover target VM2 обязана подтвердить точный
формат/exit semantics `kesl-control --scan-file --action Inform`, socket
permissions/cleanup и throughput. Только `clean` допускает allow; `infected`
даёт deny; scanner error или stale database остаются retryable и завершаются
`503`, не allow. Runtime фиксирует `scanner_engine=kesl` и
`signatures_version=hash(KESL version + database date)`; KESL update выполняется
ежечасно.
Legacy single-VM `docker compose up` не является evidence готовности target ВМ2. Один `message_id` нельзя одновременно отправлять в v1 и v2. После cutover ВМ1 не содержит local Safety/Redis DB2.