АйТи Фреш
Главная / Статьи / Сети
Сети

nft -c проходит успешно, а после применения правил пропадает SSH: как готовить откат

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~23 мин чтения
nft -c проходит успешно, а после применения правил пропадает SSH: как готовить откат
Иллюстрация к статье «nft -c проходит успешно, а после применения правил пропадает SSH: как готовить откат».

Знакомая последовательность: правите nftables на удалённом сервере, прогоняете nft -c, получаете тишину и код возврата 0, применяете набор через nft -f. Сессия жива, всё выглядит хорошо. Выходите — и обратно уже не заходите. Разберу, что на самом деле проверяет ключ -c, почему ваша текущая SSH-сессия врёт вам в лицо через conntrack, как снять резервный набор правил, который действительно восстановится, и как я применяю ruleset на боевых шлюзах через таймер отката, чтобы худшее, что случится, — это две минуты недоступности.

Что на самом деле проверяет nft -c и чего он не проверяет никогда

Начну с формулировки из первоисточника, потому что вокруг неё построено всё недоразумение. Руководство nft(8) описывает ключ ровно одной строкой: «-c, --check — Check commands validity without actually applying the changes», то есть «проверить допустимость команд, не применяя изменения». Ключевое слово — commands. Не «проверить, что сервер останется доступен». Не «проверить, что политика корректна». Проверить, что набор команд допустим.

На практике -c ловит три класса ошибок, и это действительно полезно. Первое — грамматика: опечатка в ключевом слове, незакрытая фигурная скобка, dport вместо tcp dport. Второе — типы и семантика внутри файла: попытка сравнить IPv6-адрес в таблице семейства ip, ссылка на неопределённую переменную из define, jump в цепочку, которой в этом же файле нет. Третье — сочетания, которые ядро не поддерживает: тип цепочки nat на хуке, где его быть не может. Всё это -c выловит до того, как вы что-то сломаете, и одна эта проверка экономит массу нервов.

А теперь то, чего он не умеет и не будет уметь никогда. Ключ -c ничего не знает о вашей топологии. Он не проверит, что имя интерфейса в iifname "eth0" совпадает с реальным ens18, потому что iifname сравнивает произвольную строку — синтаксически там допустимо что угодно. Он не проверит, что порт вашего SSH не 22, а 33822. Он не проверит порядок правил: если drop стоит выше accept, файл абсолютно валиден и абсолютно смертелен. Он не увидит, что параллельно с вашей новой таблицей на том же хуке живёт таблица от ufw или Docker, а вердикт drop в любой из базовых цепочек терминален и обрывает пакет, сколько бы accept ни было в соседних. Проверка допустимости команд — это линтер, а не тест доступности сервера.

Есть ещё приятная мелочь, о которой мало кто знает: -c комбинируется с оптимизатором. В руководстве прямо сказано про -o/--optimize: «You can combine this option with -c to inspect the proposed optimizations». То есть nft -c -o -f new.nft покажет, как nft свернул бы ваши правила в наборы и конкатенации, ничего не меняя. Удобно для ревью чужих конфигов, но к безопасности применения отношения по-прежнему не имеет.

Правило, которое я повторяю всем своим инженерам: nft -c — это компилятор, а не мониторинг. Успешная проверка означает «ядро примет этот набор», а не «после этого набора я смогу зайти по SSH».

Почему сессия остаётся живой и создаёт ложное чувство, что всё хорошо

Вот самая коварная часть истории. Вы применили заведомо кривой набор правил, где SSH разрешён на несуществующем интерфейсе, — а терминал не отваливается. Вы жмёте Enter, приглашение отвечает, nft list ruleset печатается. Вывод напрашивается сам собой: всё в порядке, правила легли, можно фиксировать конфиг и идти домой. Через двадцать минут вы выходите из сессии — и всё, дверь захлопнулась.

Механика простая. nft flush ruleset удаляет таблицы, цепочки и правила, но не трогает подсистему отслеживания соединений: записи conntrack о ваших установленных TCP-потоках остаются на месте. Практически в любом вменяемом шаблоне первой строкой цепочки input стоит ct state established,related accept — и ваша текущая SSH-сессия проходит именно по этому правилу, не доходя до строки с портом 22. То есть механизм, который защищает вас от мгновенного обрыва, одновременно скрывает от вас, что новые подключения не проходят.

Отсюда единственный корректный способ проверки: новое соединение с другой машины. Не Enter в текущем окне, не nft list ruleset, а ssh -o ConnectTimeout=5 user@host со своего ноутбука или хотя бы nc -zv host 22 с соседнего сервера. Я держу для этого второе окно с уже открытой сессией — если новая не пойдёт, старая останется живой на conntrack и даст время откатиться руками.

Обратная ситуация — когда в новом наборе вообще нет правила для established — выглядит страшнее, а на деле безопаснее: сессия рвётся мгновенно, вы сразу понимаете, что сломали, и тянетесь за откатом. Хуже всего именно «тихий» отказ, когда обрыв случается не в момент применения, а в момент выхода.

Ваша живая SSH-сессия после nft -f не доказывает ничего. Она держится на записи conntrack и правиле ct state established,related accept, а не на том, что порт 22 открыт.
nft -c проходит успешно, а после применения правил пропадает SSH: как готовить откат — схема
Схема к статье. Открыть схему в полном размере

Резервный набор правил: почему первой строкой обязан идти flush ruleset

Официальный пример снятия копии на wiki nftables состоит ровно из двух команд, и порядок в них не случаен:

echo "flush ruleset" > /root/nft/rollback.nft
nft list ruleset >> /root/nft/rollback.nft

Сначала в файл пишется команда очистки, и только потом — дамп текущего набора. Восстановление выполняется одной командой nft -f /root/nft/rollback.nft, и загрузка происходит атомарно: как формулирует wiki, nftables читает все файлы, собирает конфигурацию в памяти рядом с действующей, а затем одной атомарной операцией подменяет старую новой. Если хоть одна команда в файле не пройдёт, не применится ничего — состояние «полуприменённого» ruleset невозможно.

Теперь про самую частую ошибку, из-за которой откат не спасает. Человек снимает копию так: nft list ruleset > /root/rules.bak. Без первой строки. В спокойной обстановке разница не видна, а в аварийной вылезает боком: при загрузке такого файла блоки table и chain работают по семантике «add», то есть таблица создаётся, если её нет, а правила дописываются в конец существующей цепочки. Если на момент восстановления в системе уже лежит ваш сломанный набор, вы получите не откат, а объединение: сломанные правила останутся сверху, старые добавятся снизу дублями. Первый совпавший drop отработает раньше, чем восстановленный accept, и вы по-прежнему заперты — только теперь ещё и с задвоенным ruleset, в котором невозможно разобраться из аварийной консоли.

И второй момент, который проверяют единицы: сам файл отката тоже надо проверять. Вывод nft list ruleset в подавляющем большинстве случаев загружается обратно без проблем — это прямой аналог пары iptables-save/iptables-restore, — но встречаются конфигурации, где дамп содержит объекты, порядок объявления которых при обратной загрузке важен. Поэтому сразу после снятия копии я прогоняю по ней ту самую проверку: nft -c -f /root/nft/rollback.nft. Если бэкап не проходит проверку — чинить его надо сейчас, а не в момент, когда сервер уже недоступен. Если счётчики в дампе мешают глазам, снимайте копию как nft -s list ruleset >> file — ключ -s/--stateless опускает stateful-информацию правил.

Бэкап без flush ruleset — это не бэкап, а вторая половина проблемы. Проверьте свои файлы отката прямо сейчас: `head -1 /root/*.nft`.
Порядок действий: Резервный набор правил: почему первой строкой обязан идти flush ruleset — схема
Порядок действий: Резервный набор правил: почему первой строкой обязан идти flush ruleset. Открыть схему в полном размере

Как я применяю правила на удалённом сервере: таймер отката

Идея древняя, из эпохи iptables-apply: перед применением взводится отложенная задача, которая через N секунд вернёт старый набор правил, а если админ успел подтвердить, что доступ жив, — задача снимается. С nftables это делается ровно так же, только чище, потому что откат — одна атомарная команда. Я оформляю это скриптом, который лежит на всех обслуживаемых шлюзах в /usr/local/sbin/nft-apply.

#!/bin/bash
set -euo pipefail

NEW="${1:?укажите файл с новым ruleset}"
BAK=/root/nft/rollback.nft
WAIT="${2:-120}"

mkdir -p /root/nft

# 1. снимаем откат ПРАВИЛЬНО: сначала flush, потом дамп
echo "flush ruleset" > "$BAK"
nft list ruleset >> "$BAK"

# 2. проверяем и новый набор, и сам откат
nft -c -f "$NEW"
nft -c -f "$BAK"

# 3. взводим откат ДО применения
systemd-run --unit=nft-rollback --on-active="$WAIT" \
            --timer-property=AccuracySec=1s \
            /usr/sbin/nft -f "$BAK"

# 4. применяем
nft -f "$NEW"

echo "Правила применены. Откат сработает через ${WAIT} c."
echo "Проверьте НОВОЕ подключение с другой машины, затем выполните:"
echo "  systemctl stop nft-rollback.timer"

Порядок шагов принципиален. Откат взводится до применения, а не после: если nft -f "$NEW" оборвёт вам управление в ту же секунду, задача уже стоит в очереди и отработает без вашего участия. Задача принадлежит systemd, а не вашей сессии, поэтому обрыв SSH её не убивает — это главная причина, по которой я не использую конструкции вида sleep 120 && nft -f backup в фоне текущего шелла: при разрыве соединения такой фон рискует получить SIGHUP и умереть ровно тогда, когда он нужен.

Если systemd на машине нет или атомарность отката хочется отдать отдельному демону, тот же результат даёт классический at: echo "/usr/sbin/nft -f /root/nft/rollback.nft" | at now + 2 minutes, снятие — через atq и atrm <номер>. В обоих случаях после успешной проверки доступа обязательно снимайте задачу: забытый таймер отката, отработавший ночью, — отдельный весёлый инцидент, когда шлюз внезапно откатывается на конфиг недельной давности.

Отдельно про фиксацию. В /etc/nftables.conf новый набор попадает только после того, как я подключился новой сессией и снял таймер. До этого момента файл на диске остаётся последним заведомо рабочим, и перезагрузка сервера сама по себе становится ещё одним путём отката: nftables.service при старте загружает то, что лежит в /etc/nftables.conf. На VPS, где есть кнопка «Reboot» в панели хостера, но нет KVM-консоли, это иногда единственная доступная спасательная верёвка. Проверьте заодно, что сервис вообще включён: systemctl is-enabled nftables.service. Если нет, после перезагрузки поднимется пустой набор правил. Это не локаут, но шлюз при этом открыт настежь. Включается сервис командой systemctl enable nftables.service.

Взводите откат ДО применения. Отложенная задача, созданная после nft -f, — это лотерея: если управление отвалилось на предыдущей строке, создавать её уже некому.

Разбор из практики: питомник «Молодая поросль», 23 рабочих места, 42 минуты без шлюза

Питомник растений «Молодая поросль»: 23 рабочих места — офис, склад, отгрузка и теплицы, из них 7 человек (менеджеры садовых центров и выездные агрономы) подключаются удалённо. Весной, в сезон продаж саженцев, простой периметра бьёт по отгрузкам сильнее всего, так что шлюз для них — не формальность. Периметр — арендованный виртуальный сервер у хостера, Debian 12, nftables из репозитория, версия 1.0.6-2+deb12u2. На нём же терминируется OpenVPN для удалённых сотрудников и проброс на внутренний сервер 1С. Компания на аутсорсинге у нас, но правила фаервола исторически правил их собственный админ — договорённость была такая, и меня это устраивало ровно до одного четверга.

Задача была штатная: перевести конфиг с давно живущих iptables-правил на нормальный ruleset nftables. Админ собрал файл по статье из интернета, где входной интерфейс был назван eth0. На виртуалке интерфейс назывался ens18 — предсказуемые имена. Ключевое правило выглядело как iifname "eth0" tcp dport 22 accept, политика цепочки — policy drop. Проверка nft -c -f /etc/nftables.conf прошла без единого замечания, что абсолютно логично: iifname сравнивает произвольную строку, и «eth0» для парсера ничем не хуже любой другой. Применение в 19:40, сессия живая, nft list ruleset печатает ровно то, что ожидалось. Админ полюбовался результатом, закоммитил конфиг и в 20:05 закрыл терминал.

Дальше — по учебнику. Обратно не заходит ни он, ни VPN-клиенты, ни мониторинг. Бэкап у него был, файл /root/rules.bak лежал на месте — но снят он был командой nft list ruleset > /root/rules.bak, без первой строки с очисткой. Восстановить его удалённо было невозможно в принципе: чтобы выполнить команду, надо сначала попасть на сервер. Позвонили нам в 20:20. Мы зашли в панель хостера, подняли VNC-консоль, выполнили nft flush ruleset руками, затем nft -f /root/rules.bak — и получили ровно то, что описано выше: старый набор лёг на пустое место нормально, потому что ruleset уже был очищен вручную. Если бы админ попробовал восстановиться «поверх» через ту же консоль, не сделав flush, он получил бы дубли и продолжил бы искать причину. Доступ вернулся в 20:22, суммарный простой — 42 минуты, из них 30 ушло на то, чтобы найти, у кого есть логин в панель хостера.

Что мы сделали после. Во-первых, убрали привязку разрешающего правила SSH к интерфейсу целиком: на этом шлюзе управление ходит только с внешнего адреса, фильтрация по интерфейсу там не давала ничего, кроме риска. Во-вторых, положили на сервер скрипт nft-apply из предыдущего раздела и завели правило: любое изменение ruleset — только через него, окно отката 120 секунд. В-третьих, добавили в чек-лист приёмки клиента строку «есть ли рабочий доступ к аварийной консоли и у кого именно» — оказалось, что на трёх других обслуживаемых площадках пароль от панели хостера знал ровно один уволившийся человек. За последующие полгода через nft-apply прошло больше тридцати применений; таймер реально отработал дважды — оба раза из-за правил, к SSH отношения не имевших, и оба раза никто, кроме админа, этого не заметил.

Проверьте прямо сегодня не правила, а две вещи: есть ли у вас доступ к KVM/VNC-консоли каждого шлюза и знает ли его хотя бы два человека. Без этого любой откат — теория.
Цифры и версии: Разбор из практики: питомник «Молодая поросль», 23 рабочих места, 42 минуты без шлюза — схема
Цифры и версии: Разбор из практики: питомник «Молодая поросль», 23 рабочих места, 42 минуты без шлюза. Открыть схему в полном размере

Шаблон ruleset, которым сложно себя запереть

Помимо процедуры применения помогает сама структура набора правил. Я держу правила в своей отдельной таблице и никогда не полагаюсь на глобальный flush в боевом файле. Идиома «add-then-delete» позволяет перезагружать только свою таблицу, не трогая чужие: строка table inet fw создаёт таблицу, если её нет, следующая строка её гарантированно удаляет вместе с содержимым, и дальше идёт полное определение. Всё это внутри одной атомарной транзакции nft -f. В свежих версиях nft и ядра ту же задачу решает команда destroy table inet fw: по nft(8) она удаляет таблицу и не завершается ошибкой, если таблицы нет. Но на Debian 12 с nft 1.0.6 я на неё не рассчитываю и оставляю переносимую пару table + delete table.

#!/usr/sbin/nft -f

# создаём, если нет, затем сносим — так delete не упадёт на чистой системе
table inet fw
delete table inet fw

table inet fw {
    chain input {
        type filter hook input priority filter; policy drop;

        ct state established,related accept
        ct state invalid drop
        iif lo accept

        meta l4proto icmp accept
        meta l4proto ipv6-icmp accept

        # управление — БЕЗ привязки к имени интерфейса
        tcp dport 22 accept

        tcp dport { 80, 443 } accept
        udp dport 1194 accept

        limit rate 5/minute log prefix "nft-drop: " level info
    }

    chain forward { type filter hook forward priority filter; policy drop; }
    chain output  { type filter hook output  priority filter; policy accept; }
}

Три решения здесь спорные, и я скажу об этом прямо. Первое: tcp dport 22 accept без ограничения по источнику — с точки зрения безопасности это хуже, чем разрешение с доверенных подсетей. Но пока вы не отладили процедуру применения и не проверили аварийную консоль, широкое правило для управления безопаснее узкого: попытка сразу сделать «правильно» через ip saddr { ... } tcp dport 22 accept заканчивается локаутом каждый раз, когда админ подключается не с того адреса. Сужайте потом, отдельным изменением, через тот же таймер отката. Второе: policy accept на output — многие считают это дырой; на пограничном шлюзе малого офиса я согласен зажимать output только там, где есть реальное требование комплаенса, в остальных случаях он даёт больше инцидентов, чем защиты. Третье: логирование дропов с limit rate — без ограничения скорости журнал заливает диск за сутки, я видел это не раз.

Отдельная история — хосты с Docker или Kubernetes. Docker на современных Debian/Ubuntu работает через iptables-nft, то есть его цепочки физически живут в nftables и видны в выводе nft list ruleset. Глобальный flush ruleset сносит их вместе с вашими, и контейнерная сеть встаёт до systemctl restart docker. Именно поэтому на таких хостах я не применяю файлы с глобальным flush и не восстанавливаюсь дампом, снятым до старта Docker: только своя таблица, только add-then-delete. Если же вы вынужденно сделали полный откат — сразу перезапускайте docker, не гадая, почему контейнеры потеряли сеть.

Ловушка: несколько базовых цепочек на одном хуке (ваша таблица + ufw + Docker) проходятся все, и вердикт drop в любой из них терминален. Ваш accept в соседней таблице пакет не спасёт.

Чек-лист и что делать, если уже заперлись

Сведу всё в короткий порядок действий. Он занимает лишние полторы минуты на каждое изменение и за годы окупился многократно: за последние два года у нас ни одного выезда и ни одного обращения к хостеру за консолью по вине фаервола. Соблюдать его тяжело ровно до первого инцидента — после первого никого уговаривать не приходится.

Если вы читаете это уже из состояния «не захожу», порядок такой. Сначала проверьте, не осталась ли живой какая-нибудь другая сессия — открытая tmux/screen на этом же сервере, соседний контейнер, машина в той же локальной сети, откуда фильтрация может не действовать; из неё всё чинится за десять секунд. Затем — аварийная консоль: у виртуалок это VNC/KVM в панели хостера, у железа — IPMI/iLO/iDRAC или физический серийный порт. Если и этого нет, остаётся перезагрузка через панель: она поможет, если вы не успели записать сломанный набор в /etc/nftables.conf. И только последним номером — режим восстановления (rescue) с монтированием диска и переименованием /etc/nftables.conf, чтобы сервис при старте не поднял поломанные правила.

Что действительно стоит сделать до того, как понадобится: убедиться, что у вас есть логин в аварийную консоль каждого шлюза, что он записан не только в голове одного человека, и что вы им хотя бы раз пользовались. Консоль, к которой вы никогда не подключались, в момент аварии обязательно потребует установить Java-плагин, подтвердить SMS на номер уволившегося сотрудника или пройти капчу, которая не грузится. Это не шутка, это статистика наших ночных вызовов.

И последнее по порядку, но не по важности: nftables быстро развивается, поведение и диагностика в свежих версиях заметно приятнее. В Debian 12 (bookworm) едет 1.0.6, в Debian 13 (trixie) — 1.1.3, в Ubuntu 24.04 — 1.0.9, в Ubuntu 26.04 — 1.1.6, апстрим на начало сентября 2026 года дошёл до 1.1.7. Разница в сообщениях об ошибках между 1.0.x и 1.1.x ощутимая, но ни одна версия не научилась угадывать, что после применения правил вы останетесь без управления. Эту работу по-прежнему делает не парсер, а процедура.

Если доступ уже потерян и аварийной консоли нет — не тратьте время на подбор вариантов, сразу открывайте тикет хостеру на rescue-режим. Параллельно ищите живую сессию: чаще всего она находится в забытом tmux.
Порядок действий: Чек-лист и что делать, если уже заперлись — схема
Порядок действий: Чек-лист и что делать, если уже заперлись. Открыть схему в полном размере

Частые вопросы

Если nft -c -f прошёл без ошибок, можно применять правила на боевом шлюзе?

Можно применять, но не без подстраховки. Ключ -c по документации проверяет только допустимость команд: синтаксис, типы, ссылки на объекты внутри файла. Он не знает имён ваших интерфейсов, порта SSH, порядка правил и того, что рядом живут таблицы ufw или Docker. Успешный -c означает «ядро примет этот набор», а не «после него сервер останется доступен». Перед nft -f всё равно взводите таймер отката.

Почему после nft -f моя SSH-сессия не оборвалась, а зайти заново я не могу?

Потому что flush ruleset не очищает таблицу отслеживания соединений. Ваш поток остался в conntrack и проходит по правилу ct state established,related accept, не доходя до правила с портом 22. Новые подключения при этом отбивает policy drop. Проверять доступность нужно только новым соединением с другой машины — nc -zv host 22 или отдельным ssh, а не нажатием Enter в текущем окне.

Как правильно снять резервную копию набора правил nftables?

Ровно как в официальном примере: сначала записать в файл команду очистки, потом дописать дамп. echo "flush ruleset" > /root/nft/rollback.nft, затем nft list ruleset >> /root/nft/rollback.nft. Восстановление — nft -f /root/nft/rollback.nft, загрузка атомарная. Сразу после снятия прогоните по бэкапу nft -c -f, чтобы убедиться, что он вообще загрузится.

Что будет, если восстановиться из бэкапа, снятого без flush ruleset?

Правила не заменятся, а допишутся. Блоки table и chain при загрузке работают по семантике add: таблица создаётся, если её нет, а правила добавляются в конец существующей цепочки. Поверх сломанного набора вы получите дубли, где первый совпавший drop сработает раньше восстановленного accept, и доступ не вернётся. Перед загрузкой такого файла придётся вручную выполнить nft flush ruleset — а для этого нужно уже быть на сервере.

Как взвести автоматический откат правил, если на сервере нет systemd?

Через демон at: echo "/usr/sbin/nft -f /root/nft/rollback.nft" | at now + 2 minutes, снятие задачи — atq и atrm с её номером. Главное требование — задача должна принадлежать системному демону, а не вашей SSH-сессии. Конструкции вида sleep 120 && nft -f backup в фоне текущего шелла ненадёжны: при обрыве соединения фон может получить SIGHUP и умереть именно тогда, когда он нужен.

Правда ли, что nft flush ruleset ломает Docker?

Да, на современных Debian и Ubuntu Docker работает через iptables-nft, его цепочки физически лежат в nftables и видны в nft list ruleset. Глобальный flush сносит их вместе с вашими, и сеть контейнеров встаёт до systemctl restart docker. На таких хостах держите правила в своей таблице и перезагружайте только её идиомой table inet fw / delete table inet fw, не трогая глобальный набор.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 💬 MAX: +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи