# systemd and firewall ## Цель документа Зафиксировать базовый systemd/firewall слой под новую install model. ## systemd: Hysteria2 Базовые требования: - отдельный unit `hysteria-server.service` - отдельный пользователь `hysteria` - автозапуск после reboot - restart policy для падений Базовый ExecStart: ```bash /usr/local/bin/hysteria server -c /etc/hysteria/config.yaml ``` ## systemd: HY2XS admin Базовые требования: - отдельный unit `hy2xs-admin.service` - запуск от `User=hy2xs-admin`, не от root - отдельный install dir - отдельный data dir - отдельный жизненный цикл от Hysteria Рекомендуемый hardening: - `NoNewPrivileges=true` - `PrivateTmp=true` - `UMask=0077` - `ProtectHome=true` - `ProtectSystem=strict` - `ReadOnlyPaths=/etc/hysteria/config.yaml` - `ReadWritePaths=/var/lib/hy2xs-admin /var/log/hy2xs` - `RestrictAddressFamilies=AF_INET AF_UNIX` - `SystemCallArchitectures=native` - `LockPersonality=true` Для `hysteria-server.service` также обязателен sandbox-контур: - `ProtectSystem=strict` - `ReadOnlyPaths=/etc/hysteria/config.yaml` - `ReadWritePaths=/var/lib/hysteria` - `CapabilityBoundingSet=CAP_NET_BIND_SERVICE` Важно: - HY2XS admin не должен запускаться как часть unit Hysteria - unit-файлы не должны быть склеены ## Базовая firewall-модель Нужно разрешить: - UDP-порт Hysteria2 - TCP-порт SSH - established/related traffic IPv4-only policy: - использовать `table ip`, а не `table inet`; - IPv6 правила не добавлять; - UI работает только на `127.0.0.1` в production baseline. ## Firewall modes `HY2XS_FIREWALL_MODE=managed`: - orchestrator управляет baseline nftables. - существующий `foreign` entrypoint блокирует install/reconfigure (fail-fast). `HY2XS_FIREWALL_MODE=takeover`: - явный destructive takeover. - использовать только после ручной проверки хоста. `HY2XS_FIREWALL_MODE=external`: - orchestrator не модифицирует nftables. - оператор полностью управляет firewall вручную. `HY2XS_FIREWALL_MODE=off`: - firewall-слой оркестратора отключён. - `--skip-firewall` эквивалентно runtime-отключению на время операции. После staged-проверки можно включать default policy `drop`. ## Порядок применения 1. Снять резервную копию `/etc/nftables.conf`, `/etc/nftables.d/hy2xs.nft` и состояния юнита `nftables.service` в `/run/hy2xs/rollback//` и **доказать**, что копия создана. Отказ здесь останавливает операцию до первой мутации. 2. Подготовить candidate-файлы и проверить их `nft -c -f`. 3. Подставить candidate в production-пути. 4. Взвести rollback guard: транзиентный юнит `hy2xs-fw-rollback-` с окном 45 секунд. 5. Применить ruleset и проверить SSH/Hysteria/UI. 6. Снять guard и **доказать**, что он снят (см. ниже). 7. Долговечно зафиксировать успех. 8. Только после этого удалить данные отката и candidate-файлы. Порядок шагов 6–8 существенен: между снятием guard и удалением данных отката стоит фиксация успеха, поэтому отказ записи маркера (заполненный диск, read-only ФС) оставляет откат выполнимым. ### Rollback guard Guard — это защита от потери доступа к серверу. Он существует ради ситуации, в которой применённые правила отрезали SSH и оператор больше не может ничего сделать руками. Окно guard намеренно короткое — 45 секунд — и намеренно **не** покрывает smoke: smoke на медленном, но исправном сервере может идти заметно дольше. Увеличение окна лечило бы гонку расширением, а не устранением. Вместо этого guard оставляет за собой факт: ```text /run/hy2xs/rollback//auto-rollback-fired ``` Маркер создаётся rollback-скриптом **первым действием**, до любой проверки и до первой попытки восстановления. Отсюда инвариант фиксации успеха: ```text маркер auto-rollback-fired отсутствует И hy2xs-fw-rollback-.timer в состоянии inactive И hy2xs-fw-rollback-.service в состоянии inactive => автоматический откат больше не может сработать ``` Пока этот инвариант не доказан, `phase: installed` не записывается. Если guard успел сработать, операция **обязана** завершиться отказом — даже если smoke прошёл зелёным: сервер в этот момент работает на прежнем firewall, а не на том, который сгенерировала операция. Проверка состояния юнитов идёт по `ActiveState`, а не по коду возврата `systemctl stop`: для транзиентного юнита, который уже отработал и был убран systemd, `stop` возвращает 5, и этот исход неотличим от успешного снятия взведённого таймера. У факта срабатывания два независимых канала, и это не избыточность. Маркер — обычный. Отказ юнита — аварийный: если записать маркер не удалось (заполненный tmpfs `/run`, read-only ФС), скрипт поднимает код возврата, юнит уходит в `failed`, а `failed` на пути фиксации успеха запрещён так же, как и маркер. Без второго канала инвариант был бы верен лишь при дополнительном условии «guard способен записать маркер», которого никто не гарантирует. Сам rollback-скрипт восстанавливает файлы и ruleset, накапливает код возврата и уходит в `failed` при частичном восстановлении. Состояние `nftables.service` он сознательно не трогает: у этого юнита `ExecStop=/usr/sbin/nft flush ruleset`, то есть остановка сервиса стёрла бы только что восстановленные правила. Enable и active восстанавливает обычный откат в процессе оркестратора, где порядок стадий контролируется. ### Guard переживает свой процесс Guard — объект systemd, а не часть процесса оркестратора. Аварийно умершая операция оставляет его вооружённым, и он способен вернуть прежний firewall уже посреди **следующей** операции. Замок операций от этого не защищает: он действует, пока жив процесс-держатель. Поэтому условие начала новой операции — не «PID предыдущей мёртв», а «у предыдущей не осталось исполнителей, способных изменить систему». Каждый захват замка проходит через барьер покоя: если хоть один `hy2xs-fw-rollback-*` находится в состоянии `active`, `activating`, `deactivating` или `reloading`, операция отказывает. `inactive` и `failed` считаются покоем: отработавший guard больше ничего не сделает, а отказ по `failed` заблокировал бы `repair` — ровно тот инструмент, которым чинят последствия. Состояния `hysteria-server`, `hy2xs-admin` и `nftables.service` барьер сознательно не проверяет: незавершённый `systemctl restart` ничего не откатывает, он лишь повторяет то, что новая операция сделает сама. ### Проверка эффективного firewall `nft -c -f /etc/nftables.conf` разбирает текущий файл, каким бы он ни был, и поэтому ничего не говорит о том, чей это firewall. Smoke дополнительно сверяет: 1. `/etc/nftables.d/hy2xs.nft` совпадает с фрагментом, отрендеренным для этой конфигурации; 2. `/etc/nftables.conf` принадлежит HY2XS и подключает именно его; 3. таблица `inet hy2xs` реально загружена в ядро. Все три — наблюдение, поэтому проверка выполняется и в `doctor`, где она обнаруживает расхождение effective firewall с конфигурацией. ## Что не делаем В baseline не делаем: - port hopping - сложную динамическую firewall-логику - смешение UI-портов и публичного транспортного порта в один firewall-контур без правил ## Инварианты Система считается корректной, если: 1. Hysteria и HY2XS admin работают отдельными systemd unit 2. Hysteria слушает нужный UDP-порт 3. SSH не ломается после применения firewall 4. firewall-политика не противоречит listen policy 5. после reboot оба нужных сервиса стартуют корректно