654 lines
48 KiB
Markdown
654 lines
48 KiB
Markdown
# 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;
|
||
- `freshclam` имеет 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.
|
||
|
||
### Daemon и updater как разные security-профили
|
||
|
||
Если один vendor image используется для daemon и updater, им задаются разные
|
||
сети, mounts и health semantics. Проверенный паттерн ClamAV:
|
||
|
||
- `clamd` не имеет signature-CDN egress, читает signatures read-only и имеет
|
||
healthcheck реального daemon socket;
|
||
- `freshclam` один получает ограниченный egress и write к signatures;
|
||
- оба используют vendor `init-unprivileged` и только выделенные writable
|
||
`/run/clamav`, `/var/log/clamav` и `/tmp`;
|
||
- updater запускается как постоянный foreground daemon, чтобы restart policy
|
||
не превращала успешный one-shot exit в download loop/rate limit;
|
||
- унаследованный healthcheck, проверяющий отсутствующий в updater-контейнере
|
||
daemon, отключается; updater контролируется по `Up`, restart count, логам и
|
||
возрасту сигнатур;
|
||
- security policy разрешает настроить порог возраста не выше `720` часов
|
||
(30 дней); production-like seed использует более строгие `240` часов.
|
||
|
||
Ошибки `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 проверяет процесс, реально присутствующий в контейнере, а
|
||
updater freshness контролируется отдельным сигналом;
|
||
- 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;
|
||
- отклонения имеют владельца, компенсирующую меру и срок пересмотра.
|