# arch-06. Стандарт безопасности размещения сервисов > Общие границы системы и прикладная безопасность — в [`arch-01-system-architecture.md`](arch-01-system-architecture.md). Docker Compose, nginx и сети контейнеров — в [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md). Настройки и секреты — в [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md). Процесс разработки и Definition of Done — в [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md). ## Назначение Документ задаёт обязательный минимальный стандарт размещения сервисов HAN на виртуальных машинах, подготовки VM и production-деплоя. Главная цель — ограничить последствия компрометации отдельного сервиса: захват процесса или контейнера не должен автоматически давать доступ к host OS, Docker daemon, соседним сервисам, чужим секретам или всей private network. Стандарт применяется к: - VM с публичной точкой входа; - внутренним VM в private network; - VM без постоянного доступа в интернет, включая SigNoz; - пользователям `deploy`, `admin` и `tunnel`; - systemd-юнитам, Docker Compose и deployment-артефактам. Если конкретный сервис не может выполнить требование, отклонение должно быть явно описано в его спецификации: причина, риск, компенсирующая мера, владелец и срок пересмотра. Молчаливое ослабление требований запрещено. ## Модель угроз и границы доверия Базовое допущение: атакующий может добиться выполнения кода внутри одного прикладного контейнера. После этого он не должен получить: - доступ к Docker socket или Docker API; - root на host OS; - возможность менять compose-файлы, systemd-юниты, deployment-скрипты или `sudoers`; - секреты сервисов, которые не нужны скомпрометированному процессу; - произвольный доступ к PostgreSQL, S3 и другим VM; - возможность публиковать новый host port или подключать host directories; - постоянный канал управления через неограниченный исходящий трафик. Изоляция строится несколькими независимыми слоями: IAM и секреты, Unix-права, systemd/sudo, настройки контейнера, Docker networks, host firewall и cloud security groups. Один слой не считается заменой остальных. ## Классы VM ### VM с постоянным egress VM имеет только утверждённые исходящие направления, необходимые сервисам: Selectel Secrets Manager, S3, внешние API, package/image registry и DNS/NTP по принятой схеме. Постоянный egress не означает unrestricted internet access. Направления и назначение фиксируются в deployment inventory; лишние правила удаляются. ### Private/no-egress VM В steady state VM: - не принимает соединения из интернета; - не имеет общего выхода в интернет; - принимает только явно разрешённый трафик из private network; - администрируется через утверждённую private точку входа: bastion/основную VM или VPN. SigNoz относится к этому классу, если его UI, OTLP и SSH доступны только из private network. ### Каноническая классификация ВМ2 Processing ВМ2 с `message-safety` и `bitrix-sync` — **самостоятельная service VM с минимальным public ingress и постоянным ограниченным egress**: - собственный public DNS/IP или dedicated LB направляет `80/443` только на nginx ВМ2; - public `80` обслуживает только ACME challenge/HTTPS redirect; - 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; - 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; - registry, package repositories и OS updates открываются только в bootstrap/controlled maintenance window. ВМ2 использует отдельный cloud IAM principal. `han-secrets` получает только секреты сервисов ВМ2 и материализует раздельные root-owned файлы на tmpfs; общий secret bundle с ВМ1 запрещён. На ВМ2 один root Compose project и отдельный root-owned systemd deployment unit. Для схемы `message_safety` разделяются DB roles: API/worker runtime читает active/исторические `config_versions`, но не создаёт и не активирует их; migration/config-admin role используется только controlled job и имеет право version activation. Config не содержит secrets, endpoint topology или MOCK flags. Public и private ingress ВМ2 завершаются разными server blocks одного nginx без общего fallback. Public block использует сертификат доверенного CA и до proxy ограничивает Contact/alert webhook version-controlled source IP CIDR allow-list. Штатный робот передаёт отдельный receiver token в query и `application/x-www-form-urlencoded` body; query/body исключаются из logs/traces, а upstream проверяет token и document/entity/domain/member fields. Server-to-server ingress `8443` использует внутренний CA и service token вторым слоем. mTLS не обязателен для MVP. ## Lifecycle private/no-egress VM Этот lifecycle применяется к SigNoz и иным полностью private VM. Для ВМ2 обязательный public webhook ingress `80/443` после bootstrap не удаляется; вместо этого проверяются exact route и source IP CIDR allow-list, а SSH и все прочие public ports закрываются. Новый IP не добавляется автоматически: всплеск восстановлений Contact инкрементальной reconciliation инициирует проверку rejected-IP telemetry и controlled review allow-list. Для первичной раскатки применяется двухфазный процесс. ```mermaid flowchart LR Bootstrap[Bootstrap_phase] Verify[Verify_services_and_private_links] Lockdown[Steady_state_lockdown] Bootstrap -->|"temporary public SSH and package egress"| Verify Verify -->|"remove public ingress and egress"| Lockdown ``` ### Фаза bootstrap На ограниченное время разрешаются: - SSH из утверждённого trusted ops CIDR, а не из `0.0.0.0/0` - управляется через группу безопасности облачного провайдера; - egress, необходимый для обновлений ОС, установки пакетов и получения pinned images/artifacts; - доступ `deploy` и `admin` в пределах правил этого документа. До перехода в lockdown необходимо: 1. установить обновления и минимальный набор пакетов; 2. установить и проверить host firewall и fail2ban; 3. развернуть сервисы и секреты; 4. проверить health/readiness; 5. проверить требуемые private-соединения в обоих направлениях; 6. подтвердить альтернативный private путь администрирования; 7. сохранить rollback-инструкцию и inventory разрешённых соединений. ### Фаза lockdown После проверки: - public IP удаляется, если он больше не нужен; - публичный SSH и любой иной internet ingress удаляются из cloud security group; - host firewall принимает административный и прикладной трафик только из утверждённых private CIDR/SG; - общий internet egress закрывается на cloud и host-уровне; - временные bootstrap credentials, правила, installer-файлы и package caches удаляются, если они больше не нужны; - с внешней сети проверяется недоступность SSH и сервисных портов; - с VM проверяется запрет неразрешённого egress; - из private network повторно проверяются SSH и обязательные service flows. Раскатка private/no-egress VM не завершена, пока lockdown и обе группы проверок не зафиксированы в deployment checklist. ### Повторное открытие Временное открытие ingress/egress после lockdown — break-glass операция: 1. фиксируются причина, исполнитель, окно работ и необходимые destination/ports; 2. правило ограничивается trusted CIDR и минимальным сроком; 3. после работ правила удаляются; 4. повторяются проверки lockdown; 5. факт закрытия фиксируется в runbook/журнале изменений. Постоянно оставлять bootstrap-доступ «для будущих обновлений» запрещено. ## Пользователи host OS ### `deploy` Используется для штатного деплоя. Пользователь: - не входит в группы `docker`, `root` и другие root-equivalent группы; - не имеет общего `sudo`, shell root и `sudoedit`; - не меняет compose-файлы, systemd-юниты, deployment-скрипты и конфигурацию секретов; - может записывать только в выделенный incoming/staging-каталог; - может запускать только заранее утверждённые операции над конкретными systemd-юнитами; - на ВМ2 может запускать пять exact-argument вариантов root-owned Message Safety mode helper; - читает только логи своего стека, без доступа к секретам других сервисов. Членство в группе `docker` считается эквивалентом root и запрещено. ### `admin` Break-glass пользователь для восстановления: - не используется для штатного деплоя; - имеет персональные SSH-ключи, а не общий ключ команды; - доступен только из trusted ops network/VPN, а для private VM — только через private path после lockdown; - расширенные sudo-права выдаются осознанно и аудируются; - ключи хранятся отдельно от deploy credentials и регулярно пересматриваются. Доступ через cloud console/recovery mode также считается break-glass и должен быть ограничен ролями облачного проекта. ### `tunnel` Отдельный пользователь основной/bastion VM для доступа к PostgreSQL и другим private endpoints: - не имеет sudo; - не входит в deployment-группы; - не получает доступ к секретам приложения; - разрешает только local TCP forwarding; - имеет allow-list конкретных `host:port` через `PermitOpen`; - использует login shell `/usr/sbin/nologin` (после отдельной проверки, что forwarding-only соединение работает); - не разрешает agent forwarding, X11 forwarding и TTY; - ключ ограничивается теми же возможностями в `authorized_keys`. Произвольный SOCKS proxy и forwarding на неутверждённые адреса запрещены. Настройка должна быть проверена отдельной SSH-сессией до отключения старого пути. ### Root - `PermitRootLogin no`; - пароль root заблокирован (`passwd -l root`) как дополнительная мера; - штатные операции выполняются через именных пользователей; - прямой root допускается только механизмом recovery провайдера при инциденте. ## SSH baseline Минимальные настройки production VM: ```text PermitRootLogin no PasswordAuthentication no KbdInteractiveAuthentication no PubkeyAuthentication yes MaxAuthTries 3 AllowAgentForwarding no X11Forwarding no AllowUsers deploy admin tunnel ``` Дополнительно: - SSH для Private/no-egress VM разрешается cloud SG и host firewall только из trusted ops CIDR/VPN/private network; - ключи пользователей индивидуальны; общий приватный ключ запрещён; - устаревшие алгоритмы и пустые пароли запрещены; - fail2ban включается на VM, где SSH хотя бы временно доступен из интернета; - `AllowTcpForwarding no` задаётся по умолчанию, а исключение `local` — только в `Match User tunnel`; - после изменения выполняется проверка конфигурации sshd и вход во второй независимой сессии; - текущую рабочую сессию не закрывают до успешной проверки нового доступа. `AllowUsers` должен содержать только реально созданные учётные записи. Неиспользуемая роль не создаётся «на будущее». ## Права `deploy` и production-деплой ### Управление только через systemd `deploy` не запускает `docker`, `docker compose` или произвольные root-скрипты через sudo. Docker Compose запускается root-owned systemd-юнитом или root-owned deployment helper с фиксированным интерфейсом. Обновление executable, config или helper, используемого уже активным `Type=oneshot` unit с `RemainAfterExit=yes`, требует явного `systemctl restart `. `enable --now` включает unit и стартует только неактивный unit, но не применяет новый helper к уже active/exited unit. Rollout проверяет фактический live state после restart: созданные firewall rules, freshness materialized secrets, owner/mode файлов и exit status последнего запуска. Один `ActiveState=active` не доказывает применение новой версии. Sudoers хранится только в `/etc/sudoers.d/deploy` и проверяется через `visudo`. `/etc/sudoers` напрямую не редактируется. Разрешения перечисляют полные команды и конкретные unit names без wildcard. Принципиальный пример: ```sudoers Cmnd_Alias HAN_STATUS = /usr/bin/systemctl --no-pager status han-stack.service Cmnd_Alias HAN_DEPLOY = /usr/bin/systemctl start han-deploy.service, \ /usr/bin/systemctl restart han-stack.service Cmnd_Alias HAN_LOGS = /usr/bin/journalctl --no-pager -u han-stack.service Cmnd_Alias HAN_SAFETY_MODE = /usr/local/sbin/han-message-safety-mode standard, \ /usr/local/sbin/han-message-safety-mode mock --text-free true --file-free true, \ /usr/local/sbin/han-message-safety-mode mock --text-free true --file-free false, \ /usr/local/sbin/han-message-safety-mode mock --text-free false --file-free true, \ /usr/local/sbin/han-message-safety-mode mock --text-free false --file-free false deploy ALL=(root) NOPASSWD: HAN_STATUS, HAN_DEPLOY, HAN_LOGS, HAN_SAFETY_MODE ``` Фактические пути сверяются через `command -v`; разрешается только необходимый набор. Нельзя разрешать: - `systemctl *`, `journalctl *`, wildcard в unit name; - `systemctl status`/`journalctl` с интерактивным pager (он может дать shell escape под root); - shell, editor, package manager, `cp`, `mv`, `chmod`, `chown`; - произвольный путь к compose-файлу или environment-файлу; - команды с параметрами, позволяющими подменить unit, working directory, image или mount. `han-message-safety-mode` — исключение с конечным exact-argument allow-list, а не произвольный root-script. Он принадлежит `root:root`, недоступен `deploy` на запись, не принимает paths/commands/env expansion, атомарно меняет только `root:han-message-safety 0640` `/etc/han-chat/message-safety-mode.env`; dedicated host group имеет GID `10001`, совпадающий с primary GID non-root контейнера. Helper валидирует конфигурацию и выполняет только фиксированную Message Safety API recreate/restart operation внутри root Compose project. MOCK не имеет автоматического срока действия; выключение — отдельная явная команда `standard`. Все вызовы и old/new mode аудируются. ### Ownership deployment-файлов Root-owned и недоступны `deploy` на запись: - `/etc/systemd/system/han-*.service`; - production compose-файлы; - deploy/helper scripts; - `/etc/han-chat/message-safety-mode.env`; - `/etc/sudoers.d/deploy`; - secret mappings и credentials; - active release manifest. `deploy` может загружать артефакты только в отдельный каталог, например `/var/lib/han-deploy/incoming`, без права менять его parent. Активация выполняется фиксированным root-owned процессом после проверок: - артефакт относится к ожидаемому проекту и версии; - digest/signature соответствует approved release; - отсутствуют symlink/path traversal; - compose config прошёл валидацию; - image reference pinned по version/digest; - миграции и rollout соответствуют release manifest; - release manifest задаёт owner/group/mode для каждого класса файлов: data/config `0644` или строже, secrets отдельно, executable scripts, preflight, hooks и helpers — `0755`/`0750` по назначению; - перед активацией выполняется `test -x` для каждого executable из manifest. Глобальный `rsync --chmod=F644`, рекурсивный `chmod` или иной blanket mode, снимающий executable bit со scripts/hooks/preflight, запрещён. Права восстанавливаются из version-controlled manifest или явными `install -m` для каждого класса артефактов, а не после первой ошибки запуска. Если такого валидатора пока нет, compose/unit changes выполняет `admin`, а `deploy` ограничивается запуском уже подготовленного релиза. Выдавать `deploy` запись в production compose — не допустимая замена автоматизации. Shell-артефакты (`*.sh`, entrypoint, hooks и helpers) обязаны поставляться с LF line endings. Репозиторий фиксирует это через `.gitattributes`, а release preflight проверяет отсутствие `CRLF` до активации. Ошибка вида `cannot execute: required file not found` при существующем executable-файле считается признаком некорректного shebang/line endings, а не основанием менять права или запускать файл через обходной интерпретатор. ## Изменение прав `deploy` Новая доработка не получает дополнительные права автоматически. В change request указываются: 1. требуемая операция и конкретный systemd unit; 2. почему существующего интерфейса недостаточно; 3. полный executable path и фиксированные аргументы; 4. какие root-owned файлы читает или меняет операция; 5. возможность command/path/argument injection; 6. тест негативных сценариев; 7. способ отзыва права и rollback. Изменение: - проходит review владельца инфраструктуры/безопасности; - вносится отдельным файлом в `/etc/sudoers.d`; - проверяется `visudo`; - сначала проверяется в production-like среде; - отражается в этом документе и deployment runbook; - после rollout подтверждается через `sudo -l`, что лишних прав нет. Wildcard, временный `NOPASSWD: ALL` и включение в `docker` group запрещены даже как «временное» решение. ## Секреты ### Общие требования - секреты не коммитятся и не хранятся в обычном `.env`; - `.env` содержит только несекретную конфигурацию и ссылки/имена secret files; - контейнер получает только необходимые ему секреты; - общий файл со всеми секретами стека не монтируется во все контейнеры; - секреты не передаются в command line, build args, image layers и логи; - runtime secret files доступны только root и целевому process UID/GID; - ротация не требует выдачи сервису доступа к чужим секретам; - приложение не выступает сетевым прокси секретов для других VM. Значения `uid`, `gid` и `mode` в Compose file secrets нельзя считать security boundary: Docker Compose при bind-backed secret может их игнорировать. Фактические owner/mode задаются host-side materializer'ом и проверяются через `stat`, позитивный read-test от фактического container UID/GID и негативный тест от постороннего UID. Проверяется также execute/traverse permission всех parent directories. `root:root 0600` нельзя bind-mount в процесс, работающий не от root: для несекретного control file используется dedicated host group, совпадающий с primary GID контейнера, и минимальный режим `0640`; secrets получают эквивалентный минимальный ACL/group contract. Предупреждение Compose об игнорировании этих атрибутов не подавляется и не трактуется как подтверждение прав. Структурированные секреты валидируются до старта потребителя. Для PEM это означает проверку парсинга certificate/private key, отсутствие повторного base64 или литеральных `\n`, соответствие public key и запрет зашифрованного private key, если сервис не поддерживает non-interactive passphrase. ### VM с egress: `han-secrets` На VM с утверждённым доступом к Selectel: - один host-side `han-secrets` запускается через systemd до старта стека; - используется отдельный IAM principal на VM/контур; - IAM разрешает чтение только секретов сервисов этой VM; - контейнеры не получают cloud IAM credentials и сами не обращаются в Secrets Manager; - materialized secrets размещаются в `/run/han-chat/secrets` на tmpfs; - ошибка получения обязательного секрета блокирует rollout (fail closed); - автоматический fallback с Selectel на локальный production-файл запрещён. Использование единой реализации `han-secrets` на нескольких VM допустимо; общая IAM-учётная запись и общий набор секретов — нет. ### Private/no-egress VM Cloud sync не требуется. `admin` во время bootstrap: - получает минимальный набор секретов по защищённому каналу; - размещает их в root-owned каталоге вне репозитория; - задаёт каталогам `0700`, файлам `0400` или более узкие ACL для целевого UID; - по возможности передаёт их процессу как systemd credentials или read-only secret files; - удаляет временную копию и историю команд; - фиксирует fingerprint/version секрета без его значения. Допускается `han-secrets` в локальном `file`-режиме как единый loader, но он не должен создавать egress или зависимость от основной app-VM. Для ротации используется повторная контролируемая provisioning-процедура. ## DB roles и migration boundary Runtime, migration и config-admin роли разделяются для каждого сервиса. Runtime-role не получает DDL, ownership схемы или право менять immutable configuration. Migration-role монтируется только в controlled job и не передаётся runtime-контейнерам. Cross-schema migration не получает постоянный broad access. Если ей нужно однократно перенести legacy data: 1. владелец исходной схемы или DB administrator выдаёт именованной migration-role минимальные временные `USAGE` на schema и `SELECT` на конкретную таблицу; 2. migration копирует данные и fail-closed проверяет полноту переноса; 3. владелец/администратор отзывает временные права после успешного commit. Migration-role не выполняет `REVOKE`, `ALTER` или `DROP` на объекте чужого owner. Такие contract-операции принадлежат owner migration исходного сервиса либо отдельной административной процедуре. Проверку существования объекта нельзя реализовывать как «нет доступа — значит объекта нет»: permission error должен блокировать rollout, иначе legacy data может быть молча пропущена. Alembic graph обязан сохранять известные ранее выданные revision IDs, включая no-op baseline revisions. Удаление revision из нового image при наличии её в `alembic_version` запрещено; совместимость обеспечивается bridge/no-op revision, а не ручным `stamp` или правкой production DB. ## Права на каталоги Базовая модель: | Путь | Владелец / режим | Назначение | |---|---|---| | `/opt/han-chat/releases/` | `root:root`; каталоги `0755`, data/config `0644`, executable по manifest `0755/0750` | immutable release | | `/opt/han-chat/current` | `root:root` | active release link; меняет только deployment helper/admin | | `/var/lib/han-deploy/incoming` | `deploy:deploy`, `0750` | загрузка неактивированных артефактов | | `/etc/han` | `root:root`, `0750` или строже | конфигурация и secret mappings | | `/run/han-chat/secrets` | `root:root`, `0700` | runtime secrets на tmpfs | | `/var/lib/han-chat/public-tls` | `root:han-nginx-tls`, `0750`; key/cert `0640` | минимальный TLS staging для non-root edge | | `/var/lib/han-chat/acme` | `root:root`, `0755` | ACME webroot без private key | | `/var/lib/han-chat/` | UID сервиса, минимальные права | service state | | `/var/log/han-chat` | root/service group, без world-read | host-side логи при необходимости | Требования: - world-writable каталоги в deployment path запрещены; - setuid/setgid binaries не добавляются без обоснования; - сервис не получает write к каталогу с executable/config, если ему нужен только state; - bind mounts задаются read-only, кроме явно выделенных state/upload paths; - backup-файлы и дампы получают не менее строгие права, чем исходные данные; - symlinks из writable каталога не используются привилегированным helper без безопасной проверки. ## Hardening контейнеров Для каждого production-контейнера обязательна оценка и, где применимо, конфигурация: ```yaml services: service: user: "10001:10001" read_only: true security_opt: - no-new-privileges:true cap_drop: - ALL tmpfs: - /tmp:rw,noexec,nosuid,nodev ``` Базовые правила: - процесс запускается непривилегированным UID/GID; - root в контейнере допускается только с документированным обоснованием; - `privileged: true` запрещён; - `network_mode: host`, `pid: host`, `ipc: host` и `userns_mode: host` запрещены; - Docker socket/API не монтируется; - Linux capabilities удаляются все, затем точечно возвращаются необходимые; - root filesystem read-only; writable paths — отдельные volume/tmpfs; - mount host paths минимален и read-only; - default seccomp сохраняется; AppArmor/аналог провайдера включается, если доступен; - задаются CPU/memory/PID limits, restart policy и healthcheck; - сервис подключается только к необходимым Docker networks; - внешний `ports:` разрешён только утверждённой edge-точке; - image использует pinned version/digest, проходит vulnerability scan и не содержит package managers/compilers без необходимости; - секреты не копируются в image и не доступны healthcheck-команде. Если контейнер не может работать с `read_only`, в спецификации перечисляются конкретные writable paths. Полное отключение `read_only` без анализа запрещено. ### Проверка совместимости image с hardening Non-root UID сам по себе недостаточен. Для каждого pinned digest до rollout составляется inventory всех путей, куда пишет entrypoint и процесс: runtime/socket, cache/temp, generated config, logs и persistent state. Каждый путь получает отдельный volume/tmpfs с минимальным размером и явными `uid/gid/mode`; writable root filesystem, запуск root или возврат capabilities не используются как универсальный workaround. Проверяется не только основной binary, но и image entrypoint. Если vendor image имеет отдельный unprivileged entrypoint, при принудительном `user` используется именно он. Root entrypoint, который делает `mkdir/chown`, несовместим с non-root + `read_only`, даже если сам daemon способен работать без root. Изменение image digest повторяет эти проверки: tag/version, entrypoint, writable-path inventory, healthcheck semantics и фактический UID/GID считаются частью security contract образа. ### Ownership persistent named volumes Новый Docker named volume нельзя считать writable для non-root process: начальный owner часто `root:root`, даже если target path в image принадлежит service UID. Persistent volume до запуска потребителя подготавливает идемпотентный one-shot init service: - использует уже утверждённый pinned image с необходимыми `sh/chown/chmod`, а не непроверенный floating utility image; - запускается с `user: 0:0`, `read_only: true`, `cap_drop: [ALL]` и возвращает только `CHOWN`/`FOWNER`, если они действительно нужны; - не имеет сети (`network_mode: none`) и доступа к secrets; - меняет owner/mode только mount root конкретного volume, без recursive chown чужого state; - завершается с кодом `0`, а consumer зависит от `condition: service_completed_successfully`; - безопасно повторяется после recreate и отдельно проверяется в preflight. Запуск основного collector/service от root, `chmod 0777` и ручная правка `/var/lib/docker/volumes` на host запрещены как способы исправления ownership. ### TLS для non-root edge Root-only дерево ACME/Certbot не монтируется целиком в non-root nginx и не делается world-readable. Host-side root hook атомарно копирует только `fullchain.pem` и `privkey.pem` в выделенный staging-каталог с группой `han-nginx-tls` (канонический GID `11001`); nginx получает этот каталог read-only. Renewal hook сначала обновляет staged files, затем выполняет полный config test и только после успеха отправляет reload. Права и соответствие certificate/key проверяются preflight. Успешный hook обязан завершаться с кодом `0` и пустым stderr: benign output `nginx -t` и progress Compose перехватываются/подавляются на success path, но полностью выдаются в stderr при ненулевом exit code. Иначе Certbot/оркестратор может пометить успешный renewal как hook error. ### Host KESL и root-owned broker KESL 12.4 standalone и custom integration broker работают на host VM2, вне Compose. Граница между non-root Message Safety worker и privileged host: - 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 capability. ## Сетевые ограничения ### Cloud и host Используются одновременно: 1. cloud security groups — граница между internet/VPC/managed services; 2. host firewall (UFW/nftables/iptables) — защита VM; 3. `DOCKER-USER` — защита от обхода UFW опубликованными Docker ports; 4. Docker networks — разделение сервисов внутри VM. Default policy для ingress — deny. Разрешение задаёт source, destination, protocol, port и назначение. Правила «вся private network на все порты» запрещены. `DOCKER-USER` обрабатывает forwarded packet после Docker DNAT. Поэтому policy для published host ports сопоставляет original destination через `-m conntrack --ctorigdstport ` (или эквивалент nftables), а не текущий `--dport`, который уже может быть container port. Для каждого разрешённого host port обязательны: - positive external/private probe и рост counter именно allow rule; - negative probe неразрешённого source/port и рост deny counter; - проверка `iptables -S`/nft ruleset после setup, restart firewall unit, restart Docker и reboot; - запрет считать UFW INPUT counter доказательством фильтрации published Docker port: такой трафик может обходить INPUT. Firewall setup обязан обновлять helper и явно перезапускать active oneshot unit; наличие нового текста helper без изменения live rules является неуспешным rollout. Cloud SG для managed PostgreSQL разрешает TLS-подключения только от VM/SG сервисов, которым нужна соответствующая схема. PostgreSQL, Redis, OTLP receivers, admin UI и internal API не публикуются в интернет. ### Egress - сервис без внешней интеграции не подключается к сети `egress`; - внешние destination/ports фиксируются в inventory; - DNS/NTP и package/image registry учитываются отдельно; - временный bootstrap egress удаляется при lockdown; - отсутствие технической возможности фильтровать по FQDN компенсируется NAT/proxy/provider firewall и мониторингом исходящих соединений. ### Fail2ban Fail2ban обязателен для SSH, временно или постоянно доступного из интернета. Он дополняет allow-list trusted CIDR и key-only auth, а не заменяет их. Для VM без публичного ingress в steady state fail2ban можно оставить включённым, но основная защита — отсутствие внешнего маршрута и закрытые SG/firewall. ## Минимизация host OS - используется поддерживаемый минимальный образ ОС; - пакеты устанавливаются из доверенных репозиториев с проверкой подписи; - компиляторы, отладчики, сетевые утилиты и installer dependencies не остаются без эксплуатационной необходимости; - отключаются неиспользуемые daemon/socket units; - автоматические security updates или утверждённое patch window обязательны; - kernel и container runtime регулярно обновляются; - удаление пакетов выполняется по утверждённому allow-list, а не слепым `autoremove`; - старые images/releases удаляются только после сохранения необходимого rollback window; - cleanup не удаляет active image, последний рабочий релиз, forensic data или backup. Для no-egress VM обновление выполняется в контролируемое окно через временный ограниченный egress либо проверенные offline packages/images. После обновления повторяется lockdown. ## Логи, аудит и инциденты Аудируются: - входы `deploy`, `admin`, `tunnel`; - sudo-вызовы и systemd deployment actions; - изменение SG/firewall/SSH/sudoers; - получение и ротация секретов без записи значений; - открытие и закрытие break-glass доступа; - версия/digest развернутого релиза. Секреты, токены, содержимое credentials и полные PII в логи не попадают. При подтверждённой компрометации контейнера: 1. изолировать VM/контейнер сетевыми средствами; 2. не использовать скомпрометированную VM как доверенную точку восстановления; 3. ротировать доступные контейнеру секреты и service tokens; 4. проверить соседние сервисы по разрешённым network flows; 5. сохранить необходимые snapshot/log evidence; 6. пересоздать VM из доверенного образа вместо ручной «очистки», если затронут host; 7. задокументировать причину выхода за границу изоляции, если он произошёл. ## Definition of Done для новой VM или сервиса - определён класс VM: egress или private/no-egress; - составлена матрица ingress/egress; - созданы отдельные OS users и IAM principal; - root/password SSH отключены после проверки key access; - `deploy` не состоит в `docker` и имеет только конкретные systemd-команды; - production-файлы root-owned и недоступны `deploy` на запись; - release manifest сохраняет executable modes; preflight/hooks запускаются напрямую без обхода через `bash`; - каждый контейнер проверен по hardening baseline; - для каждого image digest проверены entrypoint, UID/GID и полный inventory writable paths; - bind files проходят positive read-test от container UID/GID и negative test от постороннего UID; named volumes подготовлены ограниченным init-job; - секреты разделены по сервисам/VM и отсутствуют в обычном `.env`; - bind-backed secrets и staged TLS проверены по фактическим owner/mode и содержимому, а не только по декларации Compose; - runtime/migration/config-admin DB roles разделены, временные cross-schema grants выданы и отозваны владельцем; - healthcheck проверяет процесс, реально присутствующий в контейнере, а KESL broker/database freshness контролируется отдельным host-сигналом; - healthcheck-команда и все её binaries подтверждены внутри exact pinned digest; отсутствие `curl`/`wget` не обнаруживается впервые в production; - внутренние ports недоступны извне; - live `DOCKER-USER` rules проверены по original host ports после DNAT и переживают Docker restart/reboot; - обновлённые oneshot helpers применены explicit restart, а renewal hooks имеют пустой stderr при успехе; - backup/restore и rollback проверены в объёме релиза; - для private/no-egress VM завершён и зафиксирован lockdown; - проверена недоступность внешних портов и неразрешённого egress; - отклонения имеют владельца, компенсирующую меру и срок пересмотра.