Почему правило firewall не работает в Ubuntu
Правило добавлено, но трафик идёт как раньше. Разбираем причины по порядку: цепочками владеет ufw, Docker обходит INPUT, правило не в той таблице, ruleset не сохранён.
Правило firewall не работает: с чего начать
Если правило firewall не работает в Ubuntu, причина почти никогда не в синтаксисе. Команда была принята, правило лежит в ядре, но пакет до него не доходит: решение о пакете приняли раньше, в другой цепочке или в другой таблице. Смотреть надо не на текст правила, а на путь пакета.
Ниже причины идут в том порядке, в котором они встречаются на практике. Для каждой сначала команда, которая показывает проблему, и только потом способ её исправить. Все команды выполняются на самом сервере с правами root. Ни одна из них не отвечает на вопрос сама: она даёт данные, которые надо прочитать.
Перед любыми экспериментами откройте вторую SSH-сессию и не закрывайте её. Половина причин из этого списка лечится правкой политики цепочки, а политика drop вместе с ошибкой в правиле для порта 22 закрывает вам доступ к собственному серверу. Если это уже случилось, спасает консоль в панели провайдера, а типовые поломки разобраны отдельно: как восстановиться после сломанного ufw.
Шаг ноль: правило вообще срабатывает?
Есть один вопрос с прямым ответом: растут ли счётчики пакетов на вашем правиле.
sudo iptables -L -v -n --line-numbers
sudo ip6tables -L -v -n --line-numbers
sudo nft -a list rulesetВ выводе iptables первые два столбца это pkts и bytes по каждому правилу. Обнулите их командой sudo iptables -Z, воспроизведите трафик с другой машины, посмотрите снова. Счётчик остался нулевым: пакет до правила не дошёл, и дальше надо искать, кто принял решение раньше. Счётчик растёт, а эффекта нет: правило совпадает, но делает не то, что вы думаете, например стоит REJECT вместо DROP или указан не тот интерфейс.
В nft счётчиков по умолчанию нет. Правило считает пакеты только тогда, когда в нём явно написано counter, поэтому «счётчик нулевой» часто означает «счётчика и не было». Правила, созданные через iptables, счётчик получают всегда, так что цепочки ufw в выводе nft считать умеют, а ваше собственное nft-правило без слова counter нет.
Второй способ увидеть путь пакета: поставить логирующее правило выше своего и смотреть журнал ядра.
sudo iptables -I INPUT 1 -p tcp --dport 8080 -j LOG --log-prefix "probe8080 "
sudo journalctl -k -fСмотрите, появляются ли строки с префиксом probe8080, когда вы стучитесь на порт снаружи. Появились: пакеты доходят до цепочки INPUT, значит дело в порядке правил или в условиях совпадения. Не появились: до INPUT пакет не доходит вовсе, и причину надо искать в разделах про Docker и про таблицу nat. После проверки удалите логирующее правило командой sudo iptables -D INPUT 1, иначе журнал ядра будет расти. У ufw есть свой журнал: включите sudo ufw logging medium и ищите в /var/log/ufw.log строки с маркером [UFW BLOCK]. Подробный разбор того, как читать текущий набор правил целиком, лежит здесь: как посмотреть активные правила firewall в Ubuntu.
Цепочками владеет ufw или firewalld, и правило стоит после них
Это причина номер один. Проверьте, кто хозяин цепочек:
sudo ufw status verbose
systemctl is-active ufw firewalld
sudo iptables -S INPUTВидите в выводе строки вида -A INPUT -j ufw-before-input: цепочкой INPUT управляет ufw. Ваше правило, добавленное через -A, встало в самый конец, то есть после всех переходов ufw. Дальше работает механика netfilter, которая ломает интуицию: вердикт ACCEPT внутри пользовательской цепочки завершает обход всей таблицы для этого пакета. Если ufw уже разрешил пакет внутри ufw-user-input, обход INPUT на этом закончился, и до вашего DROP в конце никто не дойдёт. Обратное тоже верно: политика INPUT при включённом ufw это DROP, поэтому ручное ACCEPT в конце цепочки для уже отброшенного трафика тоже бесполезно.
Правильный путь: говорить с ufw его собственными командами и ставить правило первым.
sudo ufw insert 1 deny from 203.0.113.0/24 to any port 22 proto tcp
sudo ufw status numberedВставка через sudo iptables -I INPUT 1 ... тоже подействует, но живёт она до первого sudo ufw reload. ufw пересобирает свои цепочки и переходы с нуля, и ручное правило исчезает молча. Если правило обязано выполняться раньше ufw и переживать перезагрузку, его место в файлах /etc/ufw/before.rules и /etc/ufw/before6.rules. Базовая настройка ufw описана отдельно: основы ufw на VPS.
firewalld на Ubuntu не стоит по умолчанию, но его иногда ставят руками, и тогда два фронтенда правят один и тот же netfilter. Если активны оба, выключите один: предсказать, чьи цепочки окажутся выше, нельзя. У firewalld есть своя ловушка того же вида. Правило без --permanent живёт только до firewall-cmd --reload, а правило с --permanent не действует, пока reload не сделан. Сравните sudo firewall-cmd --list-all и sudo firewall-cmd --list-all --permanent: расхождение между ними и есть ваша пропавшая настройка.
Почему Docker открывает порт в обход ufw
Если порт слушает контейнер, запущенный с -p 8080:80, правила ufw к нему не применяются. Посмотрите сами:
sudo iptables -t nat -S | grep -i docker
sudo iptables -S DOCKER-USER
sudo iptables -S FORWARDМеханика такая. Опубликованный порт Docker обслуживает через DNAT (destination network address translation, трансляция адреса получателя) в цепочке PREROUTING таблицы nat. После DNAT адрес получателя это уже IP контейнера, а не адрес сервера, поэтому пакет идёт по маршруту FORWARD, а не INPUT. Все правила ufw живут в INPUT. Цепочка, которую пакет никогда не проходит, ничего запретить не может.
Два рабочих решения. Первое и самое простое: не публиковать порт на весь мир. Запись -p 127.0.0.1:8080:80 привязывает публикацию к loopback, и снаружи порт просто не существует. Второе: писать правила в цепочку DOCKER-USER, единственную, которую Docker создаёт и больше не перезаписывает при перезапуске.
ip route show default
sudo iptables -I DOCKER-USER -i enp1s0 ! -s 10.0.0.0/8 -j DROPИмя интерфейса подставьте своё из ip route show default: на современных образах это enp1s0 или ens3, а не eth0. Правило в DOCKER-USER тоже надо сохранять, иначе оно исчезнет при перезагрузке. Ставить "iptables": false в /etc/docker/daemon.json не стоит, пока вы не готовы сами написать NAT для исходящего трафика контейнеров: без него контейнеры теряют доступ в сеть. Начиная с Docker Engine 28 (весна 2025 года) доступ с других хостов к неопубликованным портам контейнеров закрыт по умолчанию, но опубликованный порт по-прежнему проходит мимо INPUT. Полный разбор этого конфликта: почему Docker публикует порты в обход ufw.
Правило попало не в ту таблицу или не в то семейство
Правило может быть синтаксически верным и при этом лежать там, где нужный вам трафик не появляется. Начните с общей карты:
sudo nft list ruleset | grep -E 'table|chain|hook'
ss -ltnpПервое: семейство адресов. Команда iptables работает только с IPv4. Если сервис слушает [::]:8080, то есть двойной стек, клиент по IPv6 придёт мимо вашего правила. В выводе ss -ltnp смотрите, стоит ли *:8080 или [::]:8080, и проверяйте вторую половину командой sudo ip6tables -L -v -n. В nftables семейство inet обрабатывает оба протокола сразу, а ip только IPv4 и ip6 только IPv6, так что правило в таблице ip filter для IPv6-трафика не существует. У ufw за это отвечает строка IPV6=yes в /etc/default/ufw. Подробности про шестую версию: открытые порты IPv6 и ufw на VPS.
Второе: таблица и хук. Таблица nat видит только первый пакет соединения, поэтому фильтровать в PREROUTING бессмысленно: остальные пакеты потока туда не попадут. Фильтр живёт в таблице filter, а входящий трафик к самому серверу проходит хук input. Транзитный трафик, включая трафик к контейнерам и к VPN-клиентам, идёт через forward, и правило в input его не касается.
Третье, и это чисто nftables: несколько базовых цепочек на одном хуке работают одновременно. Если вы создали свою таблицу inet my_filter с базовой цепочкой на хуке input, она получает пакет вместе с цепочками ufw. Порядок задаёт число в priority: меньше значит раньше. Дальше важная деталь, обратная привычке из iptables. Вердикт accept в базовой цепочке nftables не окончателен: он завершает только эту цепочку, а следующая базовая цепочка на том же хуке пакет всё равно увидит и может его отбросить. Вердикт drop окончателен сразу. Поэтому ваше accept в новой таблице ничего не откроет, если ufw дальше делает drop, а вот ваше drop подействует даже после чужого accept. И если в своей базовой цепочке вы написали policy drop, вы закрыли всё, что сами в ней не разрешили, включая SSH.
Выше есть ACCEPT, и до вашего правила дело не доходит
Внутри одной цепочки решает первое совпадение. Флаг -A добавляет правило в конец, поэтому новое правило оказывается ниже всего, что уже написано.
sudo iptables -L INPUT -v -n --line-numbers
sudo nft -a list chain inet filter inputСмотрите на первые строки. Почти в каждом наборе там стоит ct state established,related accept или -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT. Это правило пропускает пакеты уже установленных соединений, и оно объясняет самый частый ложный вывод: вы добавили запрет, а открытая SSH-сессия и текущая загрузка файла продолжают работать. Ваше правило действует, просто оно применяется к новым соединениям. Проверяйте новым подключением, а не тем, которое уже открыто. Старые записи в таблице соединений можно убрать вручную:
sudo apt install -y conntrack
sudo conntrack -D -s 203.0.113.5Чтобы поставить правило на нужное место, используйте sudo iptables -I INPUT 3 ... с номером строки из --line-numbers. В nftables номеров строк нет, там есть handle: запустите nft -a list ruleset, возьмите число после # handle и используйте nft insert rule ... position <handle>, чтобы встать перед правилом, или nft add rule ... position <handle>, чтобы встать после него.
Отдельная ловушка совпадения: имя интерфейса. В nftables iifname "eth0" сравнивает имя во время прохождения пакета, поэтому опечатка или несуществующий интерфейс не вызывают ошибки. Правило просто никогда не совпадает и молчит. Запись iif eth0 разрешается в индекс при загрузке и на несуществующем интерфейсе выдаёт ошибку сразу. Проверьте имена командой ip -br link.
Правило есть в ядре, но его нет в файле
Команды iptables и nft меняют только работающее ядро. Перезагрузка стирает всё, что не записано в файл и не загружается сервисом. Симптом узнаваемый: правило работало весь день, утром после перезагрузки его нет.
Если вы работаете через iptables, сохранение выглядит так:
sudo apt install -y iptables-persistent
sudo netfilter-persistent saveФайлы окажутся в /etc/iptables/rules.v4 и /etc/iptables/rules.v6, и обе версии надо сохранять отдельно. Если вы работаете через nftables, набор правил живёт в /etc/nftables.conf, а загружает его сервис, который в Ubuntu по умолчанию выключен:
sudo nft -f /etc/nftables.conf
sudo systemctl enable --now nftablesЗдесь две ловушки. Первая: файл без строки flush ruleset в начале не заменяет набор правил, а добавляется к нему, поэтому каждая загрузка плодит дубли. Вторая: sudo nft list ruleset > /etc/nftables.conf выгрузит заодно таблицы ufw и Docker, и после этого один и тот же набор цепочек получает двух хозяев. Кто выиграет, зависит от порядка запуска сервисов, и это уже не отладка, а лотерея. Выберите один инструмент управления: либо ufw, либо ваш /etc/nftables.conf. Проверка простая и честная: перезагрузите сервер и посмотрите правила снова.
Как узнать, что у вас: iptables или nftables
Этот вопрос задают часто, и ответ в Ubuntu почти всегда один: оба, потому что это два интерфейса к одному ядру.
iptables --version
update-alternatives --display iptables
lsmod | grep -E 'nf_tables|ip_tables'Смотрите, что напечатала первая команда в скобках. Значение nf_tables означает, что /usr/sbin/iptables это iptables-nft: привычный синтаксис на входе, объекты nftables в ядре на выходе. Ваши iptables-правила физически лежат в тех же таблицах, что и всё остальное, и видны в sudo nft list ruleset. Значение legacy означает старый бэкенд ip_tables, и вот это уже второй, отдельный набор правил в ядре: две подсистемы могут быть загружены одновременно, и тогда пакет проходит правила обеих.
Практический вывод важнее терминологии. Команда nft list ruleset показывает всё, включая правила, созданные через iptables. Команда iptables -S показывает только то, что она способна выразить, поэтому нативное nft-правило со множествами или с несколькими действиями в ней может не появиться вообще. Если вы правите правила через nft, а проверяете через iptables, вы смотрите в окно меньшего размера и делаете вывод, что правила нет. Сравнение двух инструментов и их синтаксиса разобрано отдельно: чем iptables отличается от nftables в Ubuntu.
Вы проверяете не тот путь
Бывает, что правило работает, а проверка неверна. Запрос curl http://localhost:8080 с самого сервера идёт через интерфейс lo, который разрешён почти в любом наборе правил, и внешние запреты его не касаются. Проверяйте с другой машины, а не с той же.
Второе место, где теряется трафик: сетевой фильтр провайдера. Многие панели управления VPS имеют собственный firewall перед виртуальной машиной. Он отбрасывает пакет до того, как его увидит ядро вашего сервера, и тогда ваши правила вообще ни при чём. Отличить эти случаи помогает поведение соединения: отказ сразу и тайм-аут означают разные вещи, и это разобрано здесь: чем connection refused отличается от timed out. Заодно убедитесь, что порт слушает хоть кто-то: как проверить, открыт ли порт в Linux.
Порядок диагностики, если правило всё ещё не работает
- Обнулите счётчики, воспроизведите трафик с другой машины, посмотрите счётчики снова. Правило с нулевым счётчиком в игре не участвует.
- Проверьте, активны ли ufw или firewalld. Если да, добавляйте правило их командами, а не через
-A INPUT. - Проверьте
sudo iptables -t nat -S | grep -i docker. Если этот порт публикует Docker, правило вINPUTнеприменимо по определению. - Проверьте семейство адресов. Если
ss -ltnpпоказывает[::], нужныip6tablesили таблицаinet. - Проверьте порядок в цепочке и первую строку про
established,related, затем повторите тест новым соединением. - Проверьте имя интерфейса в правиле по выводу
ip -br link. - Проверьте, что тест идёт снаружи, а не через
localhost. - Сохраните набор правил и перезагрузите сервер, чтобы убедиться, что правило вернулось само.
FAQ
Почему iptables -A INPUT -j DROP не блокирует трафик при включённом ufw
Потому что ufw ставит в начало цепочки INPUT переходы на свои цепочки, а флаг -A добавляет ваше правило в самый конец. Вердикт ACCEPT внутри цепочки ufw завершает обход всей таблицы filter для этого пакета, поэтому до конца INPUT разрешённый трафик не доходит. Проверить это можно по sudo iptables -S INPUT: если там видны строки с -j ufw-before-input, цепочкой владеет ufw. Ставьте правило командой sudo ufw insert 1 ... или прописывайте его в /etc/ufw/before.rules, иначе оно исчезнет при первом sudo ufw reload.
Почему порт контейнера Docker доступен снаружи, хотя ufw его закрывает
Публикация -p 8080:80 создаёт правило DNAT в цепочке PREROUTING таблицы nat. После трансляции адрес получателя это IP контейнера, поэтому пакет идёт через хук forward, а не через input, где лежат все правила ufw. Посмотрите sudo iptables -t nat -S | grep -i docker, чтобы увидеть эти правила. Закрыть порт можно публикацией только на loopback (-p 127.0.0.1:8080:80) или правилом в цепочке DOCKER-USER, единственной, которую Docker не перезаписывает при перезапуске.
Как понять, что у меня работает: iptables или nftables
Запустите iptables --version и посмотрите на значение в скобках. Надпись nf_tables означает, что команда iptables это фронтенд iptables-nft и ваши правила лежат в объектах nftables, то есть оба инструмента показывают один и тот же ruleset с разной полнотой. Надпись legacy означает отдельный старый бэкенд ip_tables, и тогда в ядре может быть два независимых набора правил одновременно; проверьте это командой lsmod | grep -E 'nf_tables|ip_tables'. Команда sudo nft list ruleset показывает всё, а iptables -S только то, что выражается в синтаксисе iptables.
Почему правила firewall исчезают после перезагрузки
Команды iptables и nft меняют только работающее ядро и ничего не пишут на диск. Для iptables сохранение делает пакет iptables-persistent и команда sudo netfilter-persistent save, которая пишет /etc/iptables/rules.v4 и /etc/iptables/rules.v6. Для nftables набор правил хранится в /etc/nftables.conf и загружается сервисом nftables, который надо включить командой sudo systemctl enable --now nftables. Файл должен начинаться со строки flush ruleset, иначе каждая загрузка добавляет правила к уже существующим и вы получаете дубли.
Правило есть, счётчик растёт, но эффекта нет. Что дальше
Значит, совпадение работает, а действие не то, которое вам нужно. Сравните DROP и REJECT: первый молча отбрасывает пакет и клиент ждёт тайм-аут, второй отвечает отказом сразу. Проверьте также, к какому направлению относится правило: запрет в OUTPUT не влияет на входящие соединения, а запрет в INPUT не влияет на транзитный трафик к контейнерам и VPN-клиентам. И помните про таблицу соединений: уже установленное соединение продолжит работать, пока вы не удалите его запись через conntrack -D или не дождётесь её истечения.