Files
han-app/architectory/arch-06-service-hosting-security.md

659 lines
48 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <unit>`. `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/<version>` | `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/<service>` | 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 <HOST_PORT>` (или эквивалент 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;
- отклонения имеют владельца, компенсирующую меру и срок пересмотра.