Реализованы сервисы ВМ2 - проверка сообщений и синхронизация с Б24 (деплой еще без перевода в боевой режим)
This commit is contained in:
@@ -0,0 +1,576 @@
|
||||
# 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 с фиксированным интерфейсом.
|
||||
|
||||
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.
|
||||
|
||||
Если такого валидатора пока нет, 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` и негативный тест от постороннего UID. Предупреждение 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`/файлы `0644` | 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 образа.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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, логам и
|
||||
возрасту сигнатур.
|
||||
|
||||
Ошибки `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 на все порты» запрещены.
|
||||
|
||||
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` на запись;
|
||||
- каждый контейнер проверен по hardening baseline;
|
||||
- для каждого image digest проверены entrypoint, UID/GID и полный inventory
|
||||
writable paths;
|
||||
- секреты разделены по сервисам/VM и отсутствуют в обычном `.env`;
|
||||
- bind-backed secrets и staged TLS проверены по фактическим owner/mode и
|
||||
содержимому, а не только по декларации Compose;
|
||||
- runtime/migration/config-admin DB roles разделены, временные cross-schema
|
||||
grants выданы и отозваны владельцем;
|
||||
- healthcheck проверяет процесс, реально присутствующий в контейнере, а
|
||||
updater freshness контролируется отдельным сигналом;
|
||||
- внутренние ports недоступны извне;
|
||||
- backup/restore и rollback проверены в объёме релиза;
|
||||
- для private/no-egress VM завершён и зафиксирован lockdown;
|
||||
- проверена недоступность внешних портов и неразрешённого egress;
|
||||
- отклонения имеют владельца, компенсирующую меру и срок пересмотра.
|
||||
Reference in New Issue
Block a user