74 KiB
module-05. Проектная спецификация message-safety
Статус: нормативная постановка целевой production-реализации v2, готовая к разработке после прохождения Definition of Ready (§18). Текущий v1 stub остаётся test-only до отдельного cutover.
Канонические источники:README.md,arch-00-glossary.md,arch-01-system-architecture.md,arch-02-api-contracts.md,arch-03-docker-compose-blueprint.md,arch-04-settings-and-content.md,arch-05-agent-development-process.md,module-01-api-backend.md.
1. Назначение и приоритет
message-safety — внутренний сервис, который до отправки сообщения в Bitrix24 проверяет пользовательский текст, содержащиеся в нём ссылки и файлы из S3-quarantine.
Сервис закрывает угрозы, поступающие через пользовательское сообщение:
- semantic prompt-injection конструкции, которые в MVP только наблюдаются и не блокируют сообщение;
- опасные URL-схемы, URL с credentials и ссылки на private/link-local/metadata адреса;
- HTML/script-like payloads, способные стать активным содержимым при небезопасном отображении;
- подмену типа файла, несоответствие заявленного MIME фактическому формату и checksum;
- вредоносные файлы, обнаруживаемые антивирусными сигнатурами.
Спецификация детализирует обновлённую двух-VM архитектуру. При конфликте приоритет имеют arch-00…arch-06. Канонические domain outcomes v2:
200 allow;403 deny;202 AcceptedсLocation/Retry-Afterи последующим sticky200или403.
Test-only правила по первому символу, случайные verdict и terminal 400 stub_final_error в production-контракт не входят.
1.1. Владение решениями
| Роль | Ответственность |
|---|---|
| Product Owner | бизнес-приёмка generic deny UX и мнемоники safety.chat.blocked |
| Safety Service Owner | lifecycle v2, API/data contracts, service-owned config, capacity и cutover sign-off |
| Rule Pack Owner | версия rules bundle, corpus, monitor rollout и release notes |
| Security Owner | approval monitor → deny и config changes, ослабляющих policy; threat model, egress/secrets и risk acceptance |
| Operations Owner | VM2, ClamAV signatures, alerts, rollback/reprovision и restore rehearsal |
Один человек может выполнять несколько ролей, но для каждого production release роли и approvals должны быть записаны в release checklist.
2. Границы ответственности
2.1. Сервис отвечает за
- строгую валидацию internal DTO;
- нормализацию и rule-based проверку текста;
- извлечение и local-only проверку ссылок с versioned link cache;
- валидацию file metadata и фактического формата;
- чтение файла из S3-quarantine по read-only credentials;
- вычисление authoritative SHA-256;
- антивирусную проверку файла через ClamAV;
- выбор sync/async режима;
- создание и исполнение async safety tasks;
- sticky final verdict, verdict cache и audit в схеме
message_safety; - versioned text-rules cache по hash analysis form без хранения текста;
- PostgreSQL task queue/leases; Redis Safety только hot cache/rate/wakeup;
- internal API, health, метрики, трассировку и безопасные JSON-логи.
2.2. Сервис не отвечает за
- JWT пользователя, согласия и авторизацию доступа пользователя к диалогу;
- edge/API rate limits;
- загрузку файла и выдачу presigned URL;
- запись пользовательского сообщения и статусов в
han_app; - copy/promote файла из quarantine в S3-data и удаление объекта;
- доставку в Bitrix24, realtime и пользовательский текст ошибки;
- анализ входящих сообщений оператора;
- ML-модерацию смысла, токсичности или правдивости текста.
Этими операциями владеет api-backend или соответствующий архитектурный модуль.
2.3. Аварийный режим MOCK
Режим предназначен для ручного controlled bypass при поломке или неприемлемой деградации полного pipeline.
| Настройки | Text result | File result |
|---|---|---|
MOCK=false |
стандартный pipeline этого документа | стандартный pipeline этого документа |
MOCK=true, TEXT_FREE=true, FILE_FREE=true |
forced 200 allow |
forced 200 allow |
MOCK=true, TEXT_FREE=true, FILE_FREE=false |
forced 200 allow |
forced 403 deny |
MOCK=true, TEXT_FREE=false, FILE_FREE=true |
forced 403 deny |
forced 200 allow |
MOCK=true, TEXT_FREE=false, FILE_FREE=false |
forced 403 deny |
forced 403 deny |
При MOCK=true не выполняются normalization/rules, URL extraction/DNS, S3 read/checksum/format/ClamAV, verdict caches, task creation и async worker. Service authentication, body-size/JSON/strict DTO validation, idempotency conflict protection, PostgreSQL audit и rate limits остаются обязательными controls.
Forced allow использует rule_id=safety.mock_forced_allow; forced deny — rule_id=safety.mock_forced_deny, reason_code=message_blocked. Mock никогда не возвращает 202. Internal response содержит processing_mode=mock; api-backend не раскрывает mode/rule клиенту. Mode фиксируется при первом принятии message_id: ранее созданный standard task/idempotency result не переклассифицируется и завершается в standard, а новый mock request не создаёт task. Это исключает смену verdict посередине обработки.
MOCK не включается HTTP endpoint-ом. Root-owned helper атомарно меняет защищённый config и перезапускает фиксированную Message Safety API operation внутри root Compose project; пользователь deploy может через sudo запускать только этот helper, без доступа к Docker/config:
sudo /usr/local/sbin/han-message-safety-mode standard
sudo /usr/local/sbin/han-message-safety-mode mock --text-free true|false --file-free true|false
Helper принимает только указанные enum/boolean arguments, не принимает пути/commands/env expansion, пишет /etc/han-chat/message-safety-mode.env как root:han-message-safety 0640 (dedicated GID 10001 совпадает с primary GID контейнера), проверяет config, выполняет restart и health verification. Ошибка включает rollback к предыдущему файлу. Ограничения по времени нет: MOCK действует до явного standard, но всё время формирует audit/метрики и active alert.
Команда standard атомарно записывает MOCK=false, TEXT_FREE=false, FILE_FREE=false; скрыто сохранять предыдущие free flags запрещено.
3. Threat model MVP
3.1. Текст и ссылки
| Угроза | Контроль | Результат |
|---|---|---|
| Prompt/control injection | Версионированные RU/EN semantic rules | 200 allow + monitor audit/metric |
| Попытка выдать текст за system/developer instruction | Нормализация + monitor rule pack | 200 allow + monitor audit/metric |
| Script/active-content payload | Правила для script, event-handler и опасных embedding-конструкций | 403 deny |
| Опасная URL-схема | Разрешены только http и https для распознанных web URL |
403 deny |
| URL с userinfo/credentials | Запрет user:password@host |
403 deny |
| SSRF-ссылка | DNS/IP classification, запрет private, loopback, link-local, multicast, unspecified и metadata endpoints | 403 deny |
| Известный phishing/malware URL | Внешний reputation provider в MVP отсутствует; риск явно принят | Вне покрытия MVP |
| Обход Unicode/whitespace | NFKC, CRLF→LF, Unicode whitespace handling | Проверка нормализованного текста |
| ReDoS/DoS правилами | Линейные/ограниченные regex, лимиты текста, URL и времени | invalid limits → 400; internal rule timeout → 500 |
| SSRF через fetch содержимого | Запрет HTTP GET/HEAD/render/redirect follow к пользовательским URL | Не выполняется |
Rules не заменяют безопасный rendering. Frontend и Bitrix integration обязаны экранировать текст; safety является дополнительным барьером, а не HTML sanitizer.
3.2. Файлы
| Угроза | Контроль | Результат |
|---|---|---|
| Недопустимый размер/MIME | Сверка DTO с allow-list и лимитами | 403 deny |
| Подмена MIME | Magic-byte/content sniffing, сверка declared MIME | 403 deny |
| Подмена содержимого после complete | Version-specific read + ETag + полный SHA-256 | 403 deny |
| Malware | ClamAV scan актуальными сигнатурами | 403 deny |
| Архивная бомба/ресурсное истощение | Лимиты размера, stream scan, ClamAV limits/timeouts | deny при policy hit; error при сбое |
| Polyglot/неоднозначный формат | Строгий формат detector и deny при mismatch/ambiguity | 403 deny |
| Повтор известного файла | Cache по SHA-256 + versions | Sticky cached verdict |
api-backend применяет изменяемый бизнес allow-list han_app.app_settings:chat.attachments.*. message-safety не читает чужую схему han_app: он применяет active message_safety.config_versions.file_policy и immutable detector manifest §7.4. Эффективный allow — пересечение business allow-list, enabled MIME active safety config и форматов detector manifest; MIME/size в DTO должны пройти все слои. Config может только отключить MIME или ужесточить limits относительно manifest hard limits, но не добавить неподдерживаемый формат и не увеличить hard limit. Internal DTO не содержит имени файла, поэтому сервис не выводит расширение из object key.
3.3. Вне threat model MVP
- zero-day malware, отсутствующий в сигнатурах и эвристиках выбранного AV;
- OCR изображений и semantic analysis PDF;
- password-protected/encrypted containers: в MVP они запрещаются, если содержимое нельзя полностью проверить;
- DLP/поиск персональных данных, токсичности и запрещённой тематики;
- загрузка HTML/ресурсов страницы по пользовательской ссылке (browser-like fetch, redirect follow, screenshot, headless render);
- phishing/malware reputation URL без внешнего threat feed.
4. Общий pipeline
flowchart TD
postCheck[POST_check] --> auth[Auth_DTO_Idempotency]
auth --> mockMode{MOCK_enabled}
mockMode -->|true| mockKind{content_kind}
mockKind -->|text| mockText{TEXT_FREE}
mockKind -->|file| mockFile{FILE_FREE}
mockText -->|true| allow200[200_allow]
mockText -->|false| deny403[403_deny]
mockFile -->|true| allow200
mockFile -->|false| deny403
mockMode -->|false| kind{content_kind}
kind -->|text| normalizeText[Normalize_text]
normalizeText --> textCache{Text_rules_cache}
textCache -->|deny_hit| deny403
textCache -->|allow_or_monitor_hit| replayMonitor[Replay_monitor_audit]
textCache -->|miss| textRules[Text_rules]
textRules -->|deny| deny403
textRules -->|allow_or_monitor| persistTextCache[Persist_text_rules_result]
replayMonitor --> extractUrls[Extract_URLs]
persistTextCache --> extractUrls
extractUrls --> hasUrls{URLs_found}
hasUrls -->|no| allow200
hasUrls -->|yes| linkCache{Link_cache}
linkCache -->|allow_hit| refreshDns[Refresh_DNS_if_expired]
linkCache -->|deny_hit| deny403
linkCache -->|miss| linkChecks[Local_URL_checks]
refreshDns --> linkResult{Link_result}
linkChecks --> dnsClassify[DNS_IP_classification]
dnsClassify --> linkResult
linkResult -->|allow_or_monitor| allow200
linkResult -->|policy_deny| deny403
linkResult -->|dependency_error| error503[503_dependency]
kind -->|file| metadata[Metadata_validation]
metadata --> cache{SHA256_cache}
cache -->|hit| cached[Sticky_200_or_403]
cached --> cachedFileResult{Cached_file_verdict}
cachedFileResult -->|allow| allow200
cachedFileResult -->|deny| deny403
cache -->|miss| task[202_and_task]
task --> worker[File_worker]
worker --> objectRead[S3_stream_and_SHA256]
objectRead --> formatCheck[Format_validation]
formatCheck --> avScan[ClamAV_scan]
avScan --> finalVerdict[Persist_sticky_verdict]
finalVerdict --> taskGet[GET_task_200_or_403]
Приоритет:
- service authentication, body/content-type/size и DTO validation;
- idempotency/fingerprint conflict;
- mode snapshot: MOCK forced result либо standard pipeline;
- нормализация;
- обязательные проверки для соответствующего
content_kind; - любой deny имеет приоритет над allow;
- инфраструктурная ошибка не превращается ни в allow, ни в domain deny.
5. Текстовый pipeline
5.1. Нормализация
Pipeline детерминирован:
- принять только JSON UTF-8;
- заменить
CRLF/CRнаLF; - Unicode normalization
NFKC; - построить analysis form: унифицировать Unicode whitespace, удалить/маркировать default-ignorable и zero-width controls, отдельно выявить bidi controls;
- построить TR39 confusable skeleton и mixed-script signal только для detection; mixed-script сам по себе не является deny;
- сохранить display form, исходный регистр и punctuation без изменения пользовательского текста;
- применить IDNA2008/UTS-46 non-transitional к hostname;
- применить абсолютный hard ceiling 10 000 Unicode code points; значение не может быть увеличено runtime-настройкой, даже если бизнес-лимит станет больше;
- вычислить SHA-256 analysis form для correlation/cache без хранения текста.
Превышение 10 000 code points после normalization → 400 validation_error, details.field=text. Flags default_ignorable, zero_width, bidi_control, mixed_script записываются только как bounded normalization_flags[] в audit/metrics и не влияют на verdict MVP.
Strict union проверяется повторно независимо от upstream:
content_kind=textтребует непустойtextиattachment=null;content_kind=fileтребуетtext==""и непустойattachment;- mixed/unknown shape →
400 validation_errorдо создания task или idempotency side effect.
5.2. Rule engine
Rules поставляются как статический read-only bundle app/rules/{rules_version}/rules.yaml, валидируемый committed JSON Schema. Динамический код, regex или rule definitions из запроса запрещены.
Каждое правило содержит:
- стабильный
rule_id; reason_code;- severity;
- scope (
text,url,file_metadata); - action (
deny|monitor); rules_version;- тестовые positive/negative cases.
Нормативный каталог MVP:
rule_id |
Scope | Action | Условие |
|---|---|---|---|
text.prompt_instruction_override |
text | monitor | RU/EN попытка переопределить инструкции |
text.prompt_role_impersonation |
text | monitor | имитация system/developer/tool instruction |
text.prompt_secret_extraction |
text | monitor | запрос внутренних инструкций/credentials |
text.active_script |
text | deny | script tag, inline handler или active embedding |
url.malformed |
url | deny | распознанная ссылка не проходит canonical parser |
url.forbidden_scheme |
url | deny | scheme кроме HTTP/HTTPS |
url.credentials_present |
url | deny | URL userinfo/credentials |
url.confusable_host |
url | deny | malformed/confusable IDNA hostname |
url.private_destination |
url | deny | literal/resolved private, loopback, link-local или metadata IP |
url.reserved_destination |
url | deny | multicast, unspecified или reserved IP |
url.nxdomain |
url | monitor | DNS NXDOMAIN; сообщение разрешается |
file.unsupported_mime |
file_metadata | deny | MIME отсутствует в технической matrix |
file.size_limit |
file_metadata | deny | размер превышает hard format limit |
file.object_changed |
file_content | deny | version/ETag/size/checksum mismatch |
file.format_mismatch |
file_content | deny | detector не совпал с declared MIME |
file.polyglot_or_ambiguous |
file_content | deny | detector ambiguity/polyglot |
file.encrypted_content |
file_content | deny | encrypted/password-protected container |
file.active_content |
file_content | deny | PDF JavaScript/OpenAction/Launch/XFA/embedded |
file.parser_limit |
file_content | deny | parser resource/decompression/object limit |
file.malware_detected |
file_content | deny | ClamAV FOUND |
Все domain deny используют reason_code=message_blocked; детализация остаётся во внутреннем rule_id. DTO/schema/authorization errors не получают rule_id.
Совпадение должно учитывать границы токенов и контекст, чтобы обычное обсуждение терминов не блокировалось простым substring match. Regex обязаны иметь ограниченную сложность и проходить ReDoS tests. Bundle содержит schema_version, rules_version, правила и embedded positive/negative vectors. Startup валидирует schema, уникальность rule_id, action/scope, компилирует patterns и запускает smoke vectors; любая ошибка завершает startup и даёт core not-ready.
monitor hit возвращает 200 allow, пишет только message_id, hash, rule_id, version и метрику, не попадает в sticky deny cache. Retention такого audit — 180 дней. Перевод semantic rule в deny требует минимум 30 дней monitor rollout, corpus tests, precision ≥99.5%, false-positive rate ≤0.5% и approvals Rule Pack Owner + Security Owner. Неподдерживаемый язык не получает semantic deny.
Обычный текст без срабатывания enforce-rules получает 200 allow. Неуверенное отсутствие совпадения не является deny. Ошибка rule engine является 500, но не allow.
5.3. Кэш текстовых правил
После normalization и до link pipeline сервис проверяет durable PostgreSQL + hot Redis cache:
text_rules:{sha256(analysis_form)}:{rules_version}
Cache хранит только результат text rule engine: allow|deny, deny rule_id, список monitor rule_id, normalization flags, rules_version, timestamps/expiry. Raw text не хранится.
- deny hit сразу возвращает тот же
403; - allow/monitor hit пропускает повторный rule evaluation, но для каждого нового
message_idповторно пишет monitor audit/metric; - после allow/monitor text-cache сервис выполняет URL extraction: при отсутствии ссылок сразу возвращает
200, при наличии запускает link pipeline. Текстовый cache не кэширует DNS/link verdict и не позволяет обойти fresh DNS; - transient/internal errors не кэшируются;
- смена
rules_versionавтоматически инвалидирует key.
Начальный TTL active config — 48 часов (cache.text_rule_ttl_sec=172800).
6. Pipeline ссылок
После успешной text-rule phase сервис извлекает URL Unicode-aware parser-ом, а не одним regex. Если URL не обнаружены, text check сразу завершается 200 allow; link pipeline не запускается. При наличии URL проверка выполняется синхронно и не создаёт отдельный async task.
6.1. Запрет перехода по ссылкам
Сервис никогда не выполняет переход по пользовательской ссылке для анализа. Запрещено:
- HTTP/HTTPS GET, HEAD, POST и любые browser-like запросы к URL из сообщения;
- следование redirects (301/302/meta refresh/JavaScript redirect chains);
- загрузка HTML, JS, CSS, изображений или иных ресурсов страницы;
- headless browser, screenshot, OCR или render страницы;
- использование пользовательского URL как webhook/callback target внутри safety.
Допустимые исходящие сетевые вызовы для link pipeline ограничены:
- DNS resolve через доверенный resolver (только A/AAAA, без произвольного TCP к порту URL);
- служебные вызовы к PostgreSQL, Redis, OTLP и internal dependencies сервиса.
Это жёсткое архитектурное ограничение. Внешний reputation/threat-feed provider в MVP отсутствует; известные phishing/malware URL вне покрытия. Policy проверяется только по URL/host/DNS/IP metadata.
6.2. Кэш ссылок
По аналогии с file verdict cache каждая каноническая ссылка проверяется через durable/hot cache до повторного полного pipeline.
Cache key:
link_policy:{sha256(canonical_url)}:{rules_version}:{config_version}
Где canonical_url — результат нормализации (scheme/host/path/query без фрагмента #..., IDNA, lowercase host, согласованное кодирование). Полный URL в PostgreSQL/Redis/logs не хранится; для audit допустим hash и category.
link_verdict_cache (PostgreSQL) и hot cache Redis Safety хранят:
- canonical URL hash;
- sticky verdict (
allow/deny); rule_id,reason_code;rules_version,config_version;- first_seen_at, last_seen_at, hit_count;
- TTL/expiry.
Stable syntax/policy cache использует TTL active config (seed 48 часов). Он кэширует parsing, scheme, credentials, IDNA и literal-IP policy, но не разрешает обход свежей DNS classification. На stable allow hit при истёкшем DNS cache сервис повторяет A/AAAA lookup. DNS answers кэшируются отдельно не дольше фактического DNS TTL и active hard max (seed 900 секунд); NXDOMAIN negative cache — максимум active config (seed 60 секунд). Transient dependency errors в cache не попадают.
Инвалидация происходит при смене rules_version immutable bundle или active config_version.
6.3. Local URL checks
Для каждой ссылки без cache hit:
- ограничить сообщение до 5 URL, каждый не длиннее 2048 code points; превышение →
400 validation_error,details.field=urls; - выполнить canonical parsing без автоматического исправления malformed URL;
- разрешить только
httpиhttps; - запретить userinfo/credentials;
- нормализовать IDNA host и отклонить malformed/confusable host;
- для literal IP применить IP policy; IPv4-mapped IPv6
::ffff:0:0/96сначала нормализовать к IPv4; - для hostname параллельно выполнить DNS resolve через configured trusted resolver и проверить все A/AAAA, включая mapped IPv6;
- запретить loopback, RFC1918/ULA, link-local, multicast, unspecified, reserved ranges и cloud metadata endpoints.
Malformed URL или policy hit → sync 403 deny с rule_id из local link rules. NXDOMAIN → 200 allow с monitor event url.nxdomain; resolver timeout/SERVFAIL → dependency error 503.
Metadata deny-list как минимум включает 169.254.169.254/32, fd00:ec2::254/128, 100.100.100.200/32 и соответствующие provider-specific addresses из versioned policy bundle. Literal и resolved IP проходят одну policy.
6.4. Timeout budget local-only pipeline
Общий POST budget остаётся 5 секунд:
- normalization + text rules: до 1.5 с;
- URL extraction/local parsing: до 0.5 с;
- параллельные A/AAAA lookup: 1 с на lookup, до 2 с на весь link pipeline;
- cache/DB/margin: оставшийся budget.
Превышение internal rule budget → 500; DNS dependency timeout → 503. Сервис не превращает timeout в allow или domain deny.
7. Файловый pipeline
Precondition production file flow: Selectel S3 spike подтверждает signed If-None-Match: *, checksum headers, versioning, чтение конкретного version_id и conditional source match при promote. До успешного gate file capability не включается.
7.1. Fast path POST
До создания задачи сервис:
- валидирует attachment DTO;
- проверяет допустимый MIME и
size_bytes; - проверяет checksum
sha256:<64 lowercase hex>; - проверяет canonical key
quarantine/users/{user_uuid}/dialogs/{dialog_uuid}/{attachment_uuid}: lowercase RFC 4122 UUID, ASCII, длина ≤1024, без%,.., backslash и control characters; - требует
quarantine_version_idиquarantine_etag, зафиксированныеapi-backendпри complete; - ищет cache по
(sha256, config_version, rules_version, detector_version, scanner_engine, signatures_version).
Явно недопустимые metadata дают sync 403 deny. Cache hit возвращает sticky 200 или 403. Cache miss создаёт задачу и возвращает 202 Accepted.
Если capability files=unavailable, file POST синхронно возвращает 503 dependency_unavailable, terminal=false, retryable=true и не создаёт task. Для text с URL аналогично действует links=unavailable; text без URL продолжает text-only pipeline.
Сервис не доверяет key как path и не строит произвольный URL. S3 client обращается только к configured quarantine bucket с virtual-hosted addressing и read-only credentials.
7.2. Worker
Worker:
- атомарно claim-ит pending task с lease;
- открывает ровно указанную S3 object version с conditional ETag match и ограничением bytes/time;
- вычисляет authoritative SHA-256;
- сверяет фактический размер и checksum с DTO;
- определяет реальный формат по содержимому;
- сверяет detector result с declared MIME;
- запрещает encrypted/password-protected и неподдерживаемые containers;
- передаёт поток в
clamdчерез internal network; - сохраняет sticky final verdict и audit;
- записывает cache только для terminal результата;
- завершает lease.
Чистый файл получает allow только если успешно завершились все обязательные проверки. ClamAV FOUND, checksum mismatch, format mismatch, unsupported encrypted content или policy limit дают deny с отдельным rule_id.
ClamAV timeout, protocol error, недоступность S3/DB/Redis или потеря lease не являются deny. Task остаётся pending/retryable в пределах deadline; после исчерпания retry получает terminal infrastructure failure, который API отдаёт как 503, а не 403.
7.3. AV runtime
MVP использует отдельный clamd sidecar/service в private Docker network:
- порт не публикуется наружу;
- сигнатуры обновляет
freshclam; - readiness требует daemon PING и допустимый возраст signatures;
max_signature_age_hoursдопускается в диапазоне1..720часов (не более 30 дней); seed policy использует240часов (10 дней);- limits согласованы с максимальным размером файла;
- контейнер non-root, read-only root filesystem где возможно, отдельный writable volume только для signatures/runtime;
- worker не передаёт в clamd object key, имя пользователя или иные PII.
Недоступность AV переводит capability files в unavailable и запрещает новые file allow, но оставляет core/text readiness доступной.
7.4. Исполнимая матрица форматов
| Declared MIME | Обязательная проверка | Limits | Deny |
|---|---|---|---|
image/jpeg |
JPEG magic + полный bounded decode | ≤5 MiB, ≤25 MP, dimension ≤10000 | truncation, decode error, polyglot |
image/png |
PNG signature/chunks + полный bounded decode | ≤5 MiB, ≤25 MP | invalid chunks, decompression budget |
image/webp |
RIFF/WEBP + bounded decode | ≤5 MiB, ≤25 MP, ≤100 frames | invalid/oversized animation |
image/heic, image/heif |
ISO BMFF ftyp brand + libheif bounded decode |
≤5 MiB, ≤25 MP, ≤100 items | unknown brand, ambiguity |
application/pdf |
%PDF-, bounded structural parser + EOF/xref validation |
≤5 MiB, ≤500 pages, ≤100000 objects, ≤100 MiB decoded budget | encryption/password, JavaScript, OpenAction, Launch, XFA, embedded files, malformed/polyglot |
Parser выполняется с CPU/memory/wall-time limits в отдельном sandboxed subprocess. Detector ambiguity, resource limit и disagreement detector↔declared MIME дают соответствующий 403 rule_id. Immutable detector-manifest.json содержит bundle version, package/native library versions, supported MIME и hard limits; detector_version вычисляется как hash manifest, а не задаётся env. Active config ссылается на доступные rules_version/detector_version и может только сузить MIME/limits. Изменение manifest или active file policy инвалидирует file verdict cache через versioned cache key.
scanner_engine=clamav. signatures_version формируется из ClamAV engine version и CVD/CLD metadata/hash после успешного freshclam activate/reload; значение входит в verdict/cache/audit и readiness.
8. Internal API
Все /internal/safety/v2/* доступны только api-backend через private TLS gateway ВМ2.
Headers:
X-Service-Token: ${MESSAGE_SAFETY_SERVICE_TOKEN}
X-Request-ID: UUID/ULID; при отсутствии генерируется сервисом
traceparent: optional W3C
Content-Type: application/json; charset=utf-8
Token сравнивается constant-time. Missing/invalid token → 401 service_unauthorized без подсказок.
8.1. POST /internal/safety/v2/messages/check
{
"message_id": "uuid",
"content_kind": "text",
"text": "Текст сообщения",
"attachment": null
}
{
"message_id": "uuid",
"content_kind": "file",
"text": "",
"attachment": {
"attachment_id": "uuid",
"quarantine_object_key": "quarantine/users/00000000-0000-4000-8000-000000000001/dialogs/00000000-0000-4000-8000-000000000002/00000000-0000-4000-8000-000000000003",
"quarantine_version_id": "opaque-version-id",
"quarantine_etag": "\"etag\"",
"mime_type": "application/pdf",
"size_bytes": 12345,
"checksum": "sha256:<64-lowercase-hex>"
}
}
rule_id и reason_code — internal diagnostic contract. api-backend не проксирует их клиенту и для любого domain deny использует public 422 message_blocked и мнемонику safety.chat.blocked.
Неизвестные поля запрещены. text и file — строгий discriminated union.
200:
{
"verdict": "allow",
"processing_mode": "standard",
"config_version": 1,
"rule_id": "safety.all_checks_passed",
"rules_version": "2026-01-01"
}
expires_at = created_at + active_config.task_execution_deadline_sec; task сохраняет config_version, поэтому последующая активация config не меняет его deadline. Это terminal execution deadline, а не inline public wait budget.
403:
{
"verdict": "deny",
"processing_mode": "standard",
"config_version": 1,
"rule_id": "file.malware_detected",
"reason_code": "message_blocked",
"rules_version": "2026-01-01"
}
202 Accepted:
Headers:
Location: /internal/safety/v2/messages/tasks/{task_id}
Retry-After: 2
Cache-Control: no-store
{
"verdict": "pending",
"processing_mode": "standard",
"config_version": 1,
"task_id": "uuid",
"poll_after_ms": 2000,
"expires_at": "2026-07-29T15:00:00Z",
"rules_version": "2026-01-01"
}
8.2. GET /internal/safety/v2/messages/tasks/{task_id}
- running/retryable →
202 pendingс тем же body,Location,Retry-Afterиexpires_at; - sticky allow →
200 allow; - sticky deny →
403 deny; - terminal infrastructure failed →
503 task_failedсtask_id,task_status=failed,terminal=true,retryable=false; - malformed UUID →
400 validation_error; - неизвестный task или task после retention expiry →
404 task_not_found; - transient dependency failure до terminal state →
503 dependency_unavailable,terminal=false,retryable=true.
При достижении expires_at без verdict task атомарно становится failed, а GET возвращает terminal 503 task_failed. После final verdict повторный GET возвращает тот же HTTP status, verdict, rule_id и rules_version. Task не удаляется до retention expiry.
8.3. Error envelope
{
"error": {
"code": "validation_error",
"message": "Request is invalid",
"request_id": "uuid",
"details": {}
}
}
| HTTP | Code/смысл | Retry |
|---|---|---|
400 |
malformed DTO/path | нет без исправления |
401 |
service_unauthorized |
нет без исправления secret |
403 |
domain deny |
нет |
404 |
task_not_found/expired |
только краткий reconciliation; затем terminal fail |
409 |
safety_request_conflict |
нет |
429 |
rate_limit_exceeded |
по Retry-After |
500 |
internal_error |
да |
503 |
dependency unavailable или terminal task_failed |
по error.details.retryable |
Domain 403 не считается circuit breaker failure.
Обязательные error.details:
| Code | Fields |
|---|---|
validation_error |
field, constraint без echo пользовательского значения |
safety_request_conflict |
message_id, terminal=true, retryable=false |
rate_limit_exceeded |
retryable=true, retry_after_sec |
dependency_unavailable |
dependency_category, terminal=false, retryable=true |
task_failed |
task_id, task_status=failed, terminal=true, retryable=false |
internal_error |
terminal=false, retryable=true |
8.4. MOCK responses
После auth/DTO/idempotency forced text/file allow возвращает sync 200:
{"verdict":"allow","processing_mode":"mock","config_version":1,"rule_id":"safety.mock_forced_allow","rules_version":"mock"}
Forced deny возвращает sync 403:
{"verdict":"deny","processing_mode":"mock","config_version":1,"rule_id":"safety.mock_forced_deny","reason_code":"message_blocked","rules_version":"mock"}
processing_mode и config_version обязательны во всех v2 verdict/pending responses и сохраняются в internal audit/checkpoint. Значение mock не доказывает прохождение safety controls.
9. Idempotency и state model
Fingerprint = SHA-256 от RFC 8785/JCS serialization нормализованного DTO без token/request-id/trace headers. Field order, null, nested attachment и Unicode test vectors входят в contract tests.
- одинаковый
(message_id, fingerprint)возвращает тот же активный task или sticky final verdict; - тот же
message_idс другим fingerprint →409 safety_request_conflict; - concurrent duplicate создаёт одну task;
- final verdict изменять запрещено;
- transient error не кэшируется как allow/deny;
- повторный worker delivery безопасен через task state compare-and-set.
Task states:
pending -> processing -> allowed
-> denied
processing -> pending (retry with lease expiry)
processing -> failed (infrastructure retries exhausted)
allowed, denied, failed terminal и sticky. failed не является safety deny.
Inline poll api-backend ограничен caller-параметром MESSAGE_SAFETY_TASK_POLL_MAX_SEC=300 на ВМ1. Timeout/disconnect не отменяет task: han_app.safety_tasks recovery продолжает GET до terminal state или expires_at, что наступит раньше. Safety execution deadline берётся из snapshot active config (seed 1200 с); после него retry запрещён.
10. Хранение данных
10.1. PostgreSQL schema message_safety
Минимальный logical contract safety_tasks:
| Поля | Тип/constraint |
|---|---|
id, message_id, attachment_id |
UUID; PK id, UNIQUE message_id; attachment required для file task |
request_fingerprint, content_sha256 |
bytea(32), NOT NULL |
processing_mode |
`standard |
config_version |
bigint NOT NULL, FK → immutable config_versions.version |
status |
`pending |
attempt_count, lease_generation |
integer ≥0; generation увеличивается на каждый claim |
lease_owner, lease_until, next_attempt_at |
nullable text/timestamptz |
expires_at |
timestamptz NOT NULL |
quarantine_object_key, quarantine_version_id, quarantine_etag |
text NOT NULL для file task |
declared_mime, declared_size_bytes, declared_checksum |
bounded text/bigint, NOT NULL |
verdict, rule_id, reason_code |
nullable до terminal; CHECK согласован со status |
rules_version, detector_version, scanner_engine, signatures_version |
bounded text NOT NULL; snapshot фактически использованных artifacts |
created_at, updated_at, finished_at, purge_after |
timestamptz; finished/purge_after только terminal |
Обязательные индексы: UNIQUE (message_id), queue (status, next_attempt_at, created_at), lease recovery (status, lease_until), retention (finished_at). Final write использует WHERE lease_generation=:generation AND lease_owner=:owner.
file_verdict_cache: (content_sha256, config_version, rules_version, detector_version, scanner_engine, signatures_version) UNIQUE; verdict allow|deny, rule_id, reason_code, created_at, expires_at; transient/failed result запрещён constraint-ом.
text_rules_cache: (analysis_sha256, rules_version) UNIQUE; result allow|deny, nullable deny rule_id, bounded monitor rule ids/normalization flags, created_at, expires_at. Это cache только text rule phase, не полного сообщения и не link verdict.
link_verdict_cache: (canonical_url_sha256, rules_version, config_version) UNIQUE; только stable syntax/policy allow|deny, rule_id, reason_code, first/last seen, hit count, expiry. DNS answers и NXDOMAIN monitor не являются durable sticky verdict.
safety_audit: UUID PK, nullable request/message/task identifiers, event enum (received, task_created, rule_hit, rule_hit_monitor, scan_completed, dependency_failed, mock_forced_allow, mock_forced_deny, config_activated), processing_mode, config_version, verdict/rule/version, normalization flags, duration/error category, created_at, purge_after; индекс (purge_after) для retention и (message_id, created_at) для controlled review.
Service-owned runtime config хранится не как EAV, а версионированным документом:
config_versions: id UUID PK, version bigint UNIQUE, schema_version integer, state draft|active|retired, config jsonb, config_sha256 bytea(32), created_by/created_at, approved_by/approved_at, activated_at/retired_at. Partial UNIQUE допускает не более одной active, а activation transaction и readiness требуют ровно одну; trigger запрещает UPDATE config/config_sha256/schema_version/version, active/retired rows не удаляются, пока на них ссылаются tasks/cache/audit.
Activation выполняет отдельный migration/config-admin role транзакционно: JSON Schema + cross-field validation, проверка referenced rules/detector artifacts, advisory lock, retire прежней версии, activate новой и config_activated audit. Runtime DB role имеет только SELECT config и не может активировать policy. HTTP admin endpoint для config отсутствует.
API загружает active config при startup и обновляет snapshot с max staleness 5 с (poll/optional LISTEN/NOTIFY); отсутствие/невалидность active version делает core readiness 503. Каждый POST атомарно фиксирует одну config_version; worker читает immutable version задачи, поэтому activation влияет только на новые requests и не меняет in-flight verdict.
Raw message text, file bytes, filename, полный URL, service token и AV stream в PostgreSQL не сохраняются. Quarantine key/version/ETag хранятся только в active/final task row для исполнения/recovery и удаляются вместе с task retention; в audit/cache/logs они запрещены.
PostgreSQL — единственный task queue/lease source. Claim выполняется FOR UPDATE SKIP LOCKED/atomic UPDATE; lease/heartbeat/max attempts/deadline берутся из immutable config snapshot задачи (seed 90/30 с, 3, 1200 с), каждый claim увеличивает fencing generation. 5 параллельных file-worker slots — deployment capacity из env/Compose, не policy в БД. Worker с потерянным lease не может записать final state.
10.2. Redis Safety
Redis используется для:
- hot text-rules cache;
- hot verdict cache;
- hot link verdict cache;
- optional worker wake-up;
- internal rate limits.
Redis находится на ВМ2 отдельным instance от DB0/DB1 ВМ1. Потеря Redis не меняет task/lease/sticky verdict и не делает core service not-ready: сервис читает PostgreSQL, временно теряет только cache/rate/wakeup acceleration.
Retention/TTL jobs используют active config для новых expiry и уже сохранённые expires_at для созданных rows: seed final task 30 дней, safety audit 180 дней, file verdict cache 30 дней, text-rules/stable link cache 48 часов. han_app checkpoint 7 дней и failed/orphan quarantine 48 часов принадлежат api-backend config. Deny-файл удаляет api-backend сразу best-effort. Raw text/URL/file в Safety не сохраняются.
11. Согласование с api-backend
POST check
200 -> allow -> text accepted or file promote -> Bitrix delivery
403 -> blocked/rejected -> no Bitrix -> public 422 message_blocked
202 -> persist han_app.safety_tasks -> poll Location
GET 202 -> continue
GET 200 -> allow branch
GET 403 -> deny branch
terminal task_failed -> stop poll, public 503, no promote, no Bitrix
timeout/retryable 5xx -> public 503/504, no promote, no Bitrix
message-safety не перемещает и не удаляет S3 object. На deny это идемпотентно делает api-backend. Клиент не получает internal 202: публичный POST продолжает ждать final result. Internal 409 safety_request_conflict считается нарушением инварианта caller, маппится api-backend в 500 internal_error + alert и не повторяет POST автоматически.
Public 422 содержит стандартный error envelope без internal rule_id/reason_code. api-backend создаёт локальную company-реплику с текстом из text_resources по единой мнемонике safety.chat.blocked; Message Safety не формирует пользовательский текст. Client timeout/disconnect не отменяет task: recovery продолжает poll до terminal state/expires_at.
При processing_mode=mock api-backend сохраняет mode в Message/audit. Forced file allow разрешает promote, но attachment получает scan_status=bypassed, а не clean; forced deny идёт по обычной deny-ветке. Mode не показывается пользователю в public DTO.
12. Security controls
- private HTTPS через private listener nginx ВМ2; public routes для Message Safety отсутствуют;
- unique service token только из env/secret mount;
- strict DTO, body/text/URL/file limits и запрет unknown fields;
- read-only S3-quarantine access, без list/write/delete;
- запрет HTTP fetch/render/redirect follow к пользовательским URL; link pipeline использует только доверенный DNS;
- egress по назначению: worker → DNS/S3/PostgreSQL, app → local Redis/OTLP/clamd; внешнего reputation API нет;
- parameterized SQL и least-privilege DB role только на schema
message_safety; - Redis Safety ACL только prefixes
han:safety:*; - dependency pinning, SBOM/image scanning;
- non-root, read-only root fs, tmpfs
/tmp, dropped capabilities, no-new-privileges; - OpenAPI UI выключен в production;
- безопасные generic errors без stack/internal addresses;
- правила и AV signatures обновляются только trusted deployment process.
- MOCK не имеет runtime/public API; mode file root-owned, а
deployимеет sudo только на argument-validating helper и approved restart.
13. Observability и privacy
JSON logs:
timestamp,level,service.name=message-safety,module,event;request_id,trace_id,span_id, route, status, duration;processing_mode, verdict,rule_id, rules/scanner/signatures version;- task state, attempt, age/size bucket;
- dependency/error code.
Запрещено логировать message text, extracted URL целиком, file bytes, object key/name, checksum целиком, DTO body и service token. URL host и hash допустимы только при утверждённой retention policy; по умолчанию логируется category/hash.
Metrics:
- request rate/latency/status по route;
- checks/verdicts по
content_kind,allow|deny|pending|error; message_safety_mock_enabledgauge и forced outcomes поtext|file/allow|deny;- rule hits по low-cardinality
rule_id; - task queue/age/attempts/lease conflicts/timeouts;
- scan latency/bytes buckets/AV outcome;
- signatures age/version info;
- cache hit/miss;
- PostgreSQL/Redis/S3/ClamAV latency and errors;
- active/used
config_version, activation result и config refresh age; - auth rejects/rate limit/readiness.
Telemetry collector unavailable не влияет на safety verdict и readiness.
Переход mode пишет security audit с actor deploy, old/new flags, release, host, timestamp и helper result. При MOCK=true постоянно активен high-severity alert; каждое forced allow отдельно считается, чтобы оценить объём непроверенного контента.
14. Health
GET /health/live проверяет только process/event loop.
GET /health/ready возвращает core readiness и capability map:
- ровно одна valid active config, доступность/verification referenced rules/detector artifacts и обязательных secrets;
- PostgreSQL read/write в schema
message_safety; - Redis Safety status как degraded accelerator, не core gate;
- worker heartbeat/lease processing;
- S3-quarantine Head/Get read permission на безопасный canary object;
- ClamAV PING и допустимый возраст signatures;
- trusted DNS resolver для
links; - OpenAPI/runtime parity проверяется на startup/CI, а не сетевым probe каждого ready request.
Пример partial degradation (HTTP 200, core готов):
{
"status": "degraded",
"processing_mode": "standard",
"config_version": 1,
"components": {
"postgres": "ok",
"redis": "degraded",
"s3_quarantine": "ok",
"worker": "ok",
"antivirus": "down",
"dns": "ok",
"rules": "ok"
},
"capabilities": {
"text": "ready",
"links": "ready",
"files": "unavailable",
"worker": "ready"
}
}
Core HTTP 503 status=not_ready используется только для invalid config/rules/PostgreSQL. Потеря worker heartbeat делает worker=unavailable и files=unavailable, но сохраняет text/links; ClamAV/S3 down выключает только files; DNS down — только links; Redis down прогревается из PostgreSQL и не выключает core. Каждый POST повторно проверяет требуемую capability и остаётся источником correctness; api-backend может кэшировать health snapshot не дольше 5 с только для fast-fail. Health доступен только через private SG ВМ1/ops, не требует service token и не раскрывает credentials/hostnames.
В MOCK health всегда явно возвращает processing_mode=mock, mock_policy.text=allow|deny, mock_policy.file=allow|deny и status=degraded, даже если forced responses доступны. Normal pipeline dependencies показываются как bypassed, не ok. Active alert не закрывается до возврата в standard.
15. Configuration
Конфигурация разделена на четыре источника без дублирования:
han_app.app_settings— business allow-list/размеры, применяемыеapi-backend;message_safety.config_versions— service-owned runtime policy;- env/secret files — bootstrap, topology, credentials и deployment capacity;
- root-owned mode file — только emergency MOCK.
15.1. Seed active config в message_safety
schema_version: 1
rules_bundle_ref: rules-2026-01-01
detector_manifest_ref: detector-2026-08-03
task:
file_scan_timeout_sec: 60
lease_sec: 90
heartbeat_sec: 30
max_attempts: 3
execution_deadline_sec: 1200
max_pending: 100
rate:
text_rps: 10
file_rps: 2
retention:
task_days: 30
audit_days: 180
cache:
file_verdict_ttl_sec: 2592000
text_rule_ttl_sec: 172800
link_ttl_sec: 172800
dns_max_ttl_sec: 900
dns_negative_ttl_sec: 60
link:
max_per_message: 5
url_max_length: 2048
dns_lookup_timeout_sec: 1
pipeline_timeout_sec: 2
clamav:
scan_timeout_sec: 45
max_signature_age_hours: 240
file_policy:
enabled_mime_types:
- image/jpeg
- image/png
- image/webp
- image/heic
- image/heif
- application/pdf
max_size_bytes: 5242880
JSON Schema задаёт типы/ranges и cross-field constraints: heartbeat_sec < lease_sec < execution_deadline_sec, scan/pipeline timeout не больше execution deadline, TTL/retention положительны, max_signature_age_hours не превышает 720 часов (30 дней). enabled_mime_types — непустое уникальное подмножество detector manifest; max_size_bytes и последующие format overrides не превышают hard limits manifest. Referenced rules/detector artifacts обязаны быть доступны и пройти hash/signature verification до activation.
15.2. Env и secrets Message Safety на ВМ2
APP_ENV=production-like
MESSAGE_SAFETY_WORKER_CONCURRENCY=5
MESSAGE_SAFETY_DNS_RESOLVERS=<VPC-resolver-IP>
MESSAGE_SAFETY_CLAMAV_HOST=clamd
MESSAGE_SAFETY_CLAMAV_PORT=3310
SELECTEL_S3_ENDPOINT_URL=https://s3.storage.selcloud.ru
SELECTEL_S3_BUCKET_QUARANTINE=han-chat-quarantine
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
Runtime secrets MESSAGE_SAFETY_DATABASE_URL, MESSAGE_SAFETY_REDIS_URL, MESSAGE_SAFETY_SERVICE_TOKEN, S3 read credentials и internal TLS key доставляются отдельными secret files. Bootstrap/topology env не переносятся в ту же БД, подключение к которой они обеспечивают.
MESSAGE_SAFETY_POST_TIMEOUT_SEC=5, MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC=2 и MESSAGE_SAFETY_TASK_POLL_MAX_SEC=300 — caller env api-backend на ВМ1, не настройки Message Safety. Nginx timeout выводится из caller poll budget.
15.3. Emergency mode
MESSAGE_SAFETY_MOCK_ENABLED, MESSAGE_SAFETY_MOCK_TEXT_FREE, MESSAGE_SAFETY_MOCK_FILE_FREE читаются только из root-owned /etc/han-chat/message-safety-mode.env, не из repository .env и не из БД. Startup отклоняет placeholders, insecure production defaults, отсутствующий/невалидный active config и комбинацию MOCK=false при любом *_FREE=true.
15.4. Performance acceptance MVP
Availability SLO для MVP не утверждается. До cutover обязателен воспроизводимый load test:
| Поток | Нагрузка | Gate |
|---|---|---|
| Text без URL/с URL | sustained 10 checks/s, URL в 30% запросов | p95 ≤2 с, p99 ≤5 с, error budget теста 0 для internal 500 |
| File | sustained 2 checks/s, 5 worker slots | среднее processing ≤2.5 с, p95 final ≤60 с, p99/public wait ≤300 с |
| Pending/backpressure | до 100 active tasks | queue age p95 ≤5 с; при 100 новый file POST получает retryable 503 без task |
File corpus: 70% JPEG/PNG/WebP до 1 MiB, 20% PDF/HEIC до 2 MiB, 10% boundary samples до 5 MiB; включает clean, EICAR, malformed, encrypted и parser-limit cases. Если 5 slots не подтверждают 2 file/s и среднее ≤2.5 с, Safety Service Owner до cutover увеличивает slots/CPU/scan lanes и повторяет тест.
Превышение text/file token bucket → 429 rate_limit_exceeded + Retry-After. Pending считается authoritative запросом PostgreSQL; Redis используется как быстрый счётчик. При Redis outage rate limiter использует conservative in-process limits, а pending gate остаётся в PostgreSQL.
16. OpenAPI
message-safety/openapi.yaml OpenAPI 3.1 обязателен и содержит:
X-Service-Tokensecurity scheme;- common request/trace headers;
- strict discriminated union text/file;
- обязательные internal
processing_modeиconfig_versionво всех verdict/pending responses; - exact POST responses
200/202/400/401/403/409/429/500/503; - exact GET responses
200/202/400/401/403/404/429/500/503; - domain deny отдельно от error envelope;
- UUID/checksum/max length examples;
- health contracts.
Runtime schema сравнивается с committed artifact contract test. Terminal 400 stub_final_error отсутствует.
17. Тестовая матрица
Unit
- NFKC, CRLF, Unicode whitespace, max length;
- positive/negative cases каждого text rule;
- границы токенов и false-positive corpus;
- ReDoS/time budget всех regex;
- text-rules cache hit/miss/version invalidation и monitor replay без raw text;
- URL parsing, IDNA, schemes, credentials и IP ranges IPv4/IPv6;
- stable link policy cache и short DNS cache hit/miss/TTL/invalidation;
- NXDOMAIN monitor allow, SERVFAIL/timeout
503, mapped IPv6/IP metadata deny; - DTO union, fingerprint и rule priority;
- MIME/magic/checksum decision table;
- sticky state transitions;
- log redaction.
Integration
- PostgreSQL migration/constraints/idempotency/audit;
- config draft/activate/retire, concurrent activation lock, invalid/cross-field reject и runtime read-only denial;
- in-flight task сохраняет прежнюю config version, новый request получает новую;
- PostgreSQL claim/lease/fencing concurrency/recovery; Redis cache loss;
- pending=100 backpressure, token buckets и 5 worker slots;
- S3 versioned read, ETag/version changed/missing/timeout;
- ClamAV clean, EICAR, FOUND, timeout, daemon down, stale signatures;
- full SHA-256 and size mismatch;
- duplicate concurrent request and worker retry;
- dependency recovery without changed final verdict.
Contract
- text allow and each deny category;
- text cache allow без URL сразу возвращает
200; с URL запускает link pipeline; deny hit возвращает тот же rule; - local link cache hit, DNS private-address deny и DNS dependency timeout;
- capability degradation: text without URL works while files/links unavailable;
- explicit absence of HTTP fetch to user URLs in integration tests;
- file cache hit,
202then sticky200,202then sticky403, terminal failed503; - no random/non-sticky outcomes;
- malformed
400never treated as deny; - auth missing/wrong/correct;
- request-id and trace propagation;
config_versionприсутствует и совпадает в POST/GET/checkpoint/audit;- api-backend maps only domain
403to public422 message_blocked; - deny/timeout never calls Bitrix and never promotes quarantine;
- OpenAPI runtime parity.
Security/failure
- EICAR and safe corpus;
- malformed/polyglot/encrypted files;
- decompression and parser resource limits;
- URL private/link-local/metadata/IPv4-mapped-IPv6 cases;
- DNS rebinding simulation;
- no raw text/token/URL/object key/checksum in logs/traces/errors;
- S3 credentials cannot list/write/delete;
- Redis ACL and PostgreSQL schema isolation;
- telemetry outage does not affect verdict;
- performance corpus and gates §15.4.
18. Definition of Ready и Definition of Done
18.1. Definition of Ready
- роли §1.1 назначены в release/project checklist;
- target OpenAPI v2, migrations design, config/rules JSON Schema и detector manifest reviewed;
- Selectel S3 capability spike §7 пройден;
- corpus positive/negative/EICAR/boundary утверждён Rule Pack Owner и Security Owner;
- VM2 private DNS/TLS/SG/egress design reviewed Operations Owner;
- root-owned MOCK helper, five exact sudo commands и audit/alert route reviewed Security/Operations Owner;
- api-backend mapping,
safety.chat.blocked, M8 redaction и company-replica contract согласованы; - implementation backlog §20 оценён, зависимости/порядок cutover назначены;
- отсутствуют открытые TBD, меняющие DTO, verdict, retention, hard limits или security boundary.
18.2. Definition of Done
- production v2
200/202/403contract реализован без stub divergence; - MOCK 2×2 text/file matrix даёт только sync
200/403; тест доказывает отсутствие calls к rules/DNS/S3/ClamAV/cache/workers и сохранение auth/DTO/idempotency/audit/rate limits; - text-rules cache, text rules, link cache и URL pipeline покрывают threat model и false-positive corpus;
- fetch/render/redirect follow к пользовательским URL отсутствует по design и тестам;
- files проходят metadata, authoritative SHA-256, format detector и ClamAV;
- final task verdict sticky и durable;
- idempotency/concurrency/recovery доказаны тестами;
- PG schema
message_safety, Redis Safety и S3 version-specific read работают по least privilege; - versioned config activation/rollback, one-active invariant, immutable task snapshot и invalid-config readiness покрыты migration/integration tests;
- cache versioned rules/scanner/signatures и не сохраняет transient errors;
- api-backend mapping allow/deny/timeout проверен end-to-end;
- mock mode сохраняется internal, не раскрывается public; forced file allow получает
scan_status=bypassed, неclean; - public deny использует
safety.chat.blockedи не раскрывает internal rule; - blocked/failed content не попадает в Bitrix и не promote-ится;
- health/readiness и capability-specific POST отражают PG/Redis/S3/workers/ClamAV/DNS/rules;
- performance acceptance §15.4 пройден на target sizing;
- immutable/versioned S3 negative tests и conditional promote пройдены;
- OpenAPI 3.1 и runtime parity зелёные;
- logs/metrics/traces не содержат содержимое сообщений, файлов и secrets;
- hardened containers запускаются без public port;
- runbook описывает signature update, stale signatures, AV outage, retry и rollback.
deployможет выполнить все пять exact mode commands, но не читать/писать mode config и не получить Docker/general sudo; helper rollback, persistent MOCK alert и возврат в standard испытаны.
19. Размещение проекта и вынос на отдельную ВМ
19.1. Расположение репозитория
Production-проект Message Safety размещается вне codebase/backend/:
codebase/
message-safety/ # целевой production-сервис (отдельный deployable)
app/
tests/
openapi.yaml
Dockerfile
docker-compose.yml # service include root Compose ВМ2
backend/
message-safety/ # текущая test stub; подлежит замене/удалению после миграции
Stub в codebase/backend/message-safety/ не является целевой реализацией. После готовности production-сервиса stub исключается из compose HAN_CHAT и удаляется или архивируется отдельной задачей.
19.2. Целевая топология
Message Safety и bitrix-sync выносятся за пределы ВМ HAN_CHAT (ВМ1) на самостоятельную ВМ2 Processing в той же private network/VPC. Цель — снять с ВМ1 ClamAV/file scan, DNS classification, S3 streaming, CRM sync и входящий CRM webhook. Message Safety остаётся private API; Bitrix24 обращается напрямую к отдельному public host nginx ВМ2.
flowchart LR
subgraph vm1 [VM1_HAN_CHAT]
nginx[nginx]
api[api-backend]
redis1[Redis_DB0_DB1]
appDbRef[(han_app_checkpoint_reference)]
end
subgraph vm2 [VM2_Processing]
gateway[VM2_nginx_public_private]
safety[message-safety_api_worker]
sync[bitrix-sync]
clamav[clamd]
redis2[Redis_Safety]
collector[otel-collector]
end
managedPg[(Managed_PostgreSQL)]
s3q[(S3_quarantine)]
bitrix[Bitrix24]
signoz[Private_SigNoz]
api -->|"HTTPS 8443 + service token"| gateway
bitrix -->|"CRM webhook HTTPS 443"| gateway
api --> appDbRef
gateway --> safety
gateway --> sync
safety --> redis2
safety --> managedPg
safety --> s3q
safety --> clamav
sync --> managedPg
sync --> bitrix
collector --> signoz
На ВМ2 один root Compose включает собственный nginx с public webhook 443 и private 8443, message-safety API/worker, clamd/freshclam, bitrix-sync, Redis Safety и local OTEL Collector. Managed PostgreSQL и S3 остаются вне VM. Внешнего URL reputation provider нет.
19.3. Исключение из docker compose HAN_CHAT
Сервисы message-safety и bitrix-sync удаляются из root Compose ВМ1:
- нет их
build/imageна ВМ1; - нет
depends_on: message-safetyhealth gate уapi-backendна локальный контейнер; - вместо этого
api-backendобращается к remote URL через private network.
api-backend не делает remote Safety обязательным для общей read readiness. Send path читает capability status и fail-closed возвращает dependency error только для требуемой проверки.
19.4. Затронутые компоненты (impact analysis)
| Компонент | Текущее состояние | Изменение при выносе |
|---|---|---|
api-backend |
MESSAGE_SAFETY_URL=http://message-safety:8080 |
URL → https://processing.internal:8443, internal CA + token; сохраняет processing_mode, file MOCK allow → scan_status=bypassed |
han_app.safety_tasks |
checkpoint poll/recovery в App DB | таблица остаётся владельцем checkpoint; migration добавляет Location/version/ETag/rules/last-status fields, новой queue table нет |
message_safety schema |
в managed PG | остаётся; подключение с ВМ2 по TLS/private network |
| Redis DB2 | на ВМ1 compose | отдельный Redis Safety ВМ2; PG task queue остаётся authoritative |
| S3-quarantine read key | использовался локальным контейнером | credentials монтируются на ВМ2; firewall allow S3 endpoint |
| nginx | только edge ВМ1 | самостоятельный nginx ВМ2: public exact CRM webhook на 443 и private Message Safety на 8443 |
| Observability | collector ВМ1 | local collector ВМ2 → private SigNoz |
| Deployment/runbook | compose all-in-one | два root Compose/systemd stack, cutover/rollback и reprovision ВМ2 |
arch-03 |
один Compose ВМ1 | один root Compose на каждой VM |
arch-04 |
local Docker URLs | remote HTTPS URL ВМ1; service-specific env/secrets ВМ2 |
| CI/CD | build из backend tree | отдельный pipeline artifact codebase/message-safety |
| Security groups | intra-docker network | internet→nginx ВМ2:80/443 с exact route policy; ВМ1→ВМ2:8443; ВМ2→PG/S3/DNS/Bitrix/SigNoz/signature CDN по назначению |
Схема han_app не получает новых таблиц для remote safety: orchestration и recovery по-прежнему владеет api-backend. Меняется только сетевой адрес internal API и deployment boundary.
19.5. Сетевые и эксплуатационные требования ВМ2
- только private IP; публичный ingress запрещён;
- inbound: private TCP 8443 только с SG ВМ1/ops; server-auth TLS internal CA + service token;
- outbound по container identity: worker→PostgreSQL/S3/DNS;
freshclam→signature CDN;bitrix-sync→Bitrix24; collector→SigNoz; - health
/health/liveи/health/readyдоступны api-backend и ops из private network; - при недоступности ВМ2
api-backendвозвращает503 dependency_unavailable, не отправляет сообщения в Bitrix.
ВМ2 — принятый SPOF MVP. Initial sizing: 4 vCPU, 8 GiB RAM, 80 GiB SSD. Меры: immutable images, PG/PITR durable truth, rebuildable Redis, resource/PID limits, queue backpressure, alerts, reprovision/restore rehearsal и RTO ≤4h. Scale-out/ВМ3 рассматриваются при sustained CPU/RAM >70%, queue age >30 с, провале performance gates §15.4, contention bitrix-sync или необходимости независимого release cadence.
20. Implementation backlog и переход с v1
Текущая реализация в codebase/backend/message-safety/ остаётся test stub и не соответствует этой постановке. Реализация разбивается на reviewable tracks:
- API/schema foundation — production project, OpenAPI v2, versioned config schema/activation, settings validation, migrations и error contracts.
- Tasks/queue — PostgreSQL claim/lease/fencing/deadline, 5 slots, recovery и Redis wakeup/cache.
- Text/rules — normalization, versioned bundle/schema, monitor corpus и hard active-content deny.
- URL policy — parser/IDNA/IP/DNS, cache split, NXDOMAIN monitor и запрет fetch.
- File/S3 — immutable version precondition, authoritative checksum, detector manifest и hard matrix.
- ClamAV — clamd streaming, freshclam activate/rollback/signature age и EICAR tests.
- api-backend integration — v2 adapter, Location polling, M8/company replica/mnemonic, conditional promote.
- VM2/observability/MOCK — root Compose, internal nginx/TLS, egress, local collector, root-owned five-command mode helper, persistent alert и dashboards.
- Acceptance — contract/security/failure/load tests §15–18 и S3 negative gate.
- Cutover — route v1→v2, reconcile active tasks, rollback rehearsal, затем удалить stub/references.
Каждый track имеет owner role, dependencies, tests и ссылку на §18. Merge track не означает production enable; cutover разрешён только после полного DoD.
До выполнения перехода контейнер stub на ВМ1 должен быть явно маркирован как non-production security control и не использоваться как целевой safety layer.