Все системы работают Ваш IP: 216.73.216.190 info@cloudhosting.lv +371 66 66 29 69 Личный кабинет

← Все вопросы

AlmaLinux 9: настройка firewalld и nftables

Свежий AlmaLinux 9 стартует с запущенным firewalld и почти закрытыми портами, и первый рефлекс многих админов: выключить его, чтобы приложение наконец начало отвечать. На машине с публичным IP этот рефлекс обходится дороже, чем экономит. Открыть ровно то, что нужно, занимает около минуты, а сделать это по SSH и не отрезать себе доступ стоит одной дополнительной команды.

Что на самом деле работает в AlmaLinux 9

Три слоя, которые постоянно путают. firewalld 1.x работает как демон политики, который вы правите. Эта политика компилируется в правила nftables. Исполняет их netfilter, хук ядра. В EL9 бэкендом по умолчанию работает nftables (FirewallBackend=nftables в /etc/firewalld/firewalld.conf), а в том же файле остаётся и бэкенд iptables, объявленный устаревшим и намеченный к удалению. Привычные же команды iptables идут через слой совместимости (iptables-nft), который в RHEL 9 и его пересборках тоже объявлен устаревшим. Старого бинарника iptables вне nft в дистрибутиве больше нет, поэтому инструкции для CentOS 7 с service iptables save обращаться уже не к чему.

systemctl is-active firewalld
firewall-cmd --state
firewall-cmd --version

В минимальных и облачных образах firewalld иногда отсутствует. Если firewall-cmd нет, поставьте его: dnf install firewalld и systemctl enable --now firewalld. Запуск обычно не рвёт сессию, потому что зона public по умолчанию разрешает сервис ssh, но проверьте --list-all до того, как закроете терминал, а не после.

Зоны и почему на VPS важна только public

Зона представляет собой именованный набор правил, привязанный к интерфейсам или к адресам источника. AlmaLinux 9 кладёт каждый новый интерфейс в public; остальные зоны придуманы для ноутбуков и роутеров, которые ходят по разным сетям. На VPS с одной сетевой картой public будет единственной, которую вы тронете.

firewall-cmd --get-default-zone
firewall-cmd --get-active-zones
firewall-cmd --zone=public --list-all

Ловушка: firewall-cmd --list-all без --zone печатает зону по умолчанию, а это не обязательно та зона, в которой реально находится ваш интерфейс. Сверяйтесь с --get-active-zones. Привязкой владеет NetworkManager, туда интерфейс и переносят, и сначала узнайте настоящее имя профиля: в EL9 профили NetworkManager лежат в /etc/NetworkManager/system-connections, а не в старом /etc/sysconfig/network-scripts, и профиль обычно называется по устройству (ens3, eth0) или "Wired connection 1", а не "System eth0", как было в EL6 и EL7. Последняя команда заново активирует профиль, и интерфейс на мгновение уходит, поэтому выполняйте её, держа под рукой консоль из панели хостинга.

nmcli -g NAME connection show
nmcli connection modify "Wired connection 1" connection.zone internal
nmcli connection up "Wired connection 1"

Одно изменение firewalld 1.0 больно бьёт по перенесённым конфигам: дрейф зон убран, и пакет, попавший в зону по адресу источника, обрабатывается только этой зоной и больше не проваливается в зону интерфейса. Вторую зону на VPS стоит заводить ровно в одном случае: под приватный интерфейс в internal, где база данных открыта только там и больше нигде.

Как открыть то, что нужно

firewall-cmd --permanent --zone=public --add-service=http
firewall-cmd --permanent --zone=public --add-service=https
firewall-cmd --reload

--permanent пишет в /etc/firewalld/zones/public.xml и не трогает работающий набор правил, а --reload его применяет. Забыли --permanent, правило исчезнет после перезагрузки. Забыли --reload, оно заработает только после следующей перезагрузки. Большинство обращений вида "открыл порт, а соединение всё равно отклоняется" сводится к одному из этих двух случаев.

firewall-cmd --permanent --zone=public --add-port=8443/tcp
firewall-cmd --permanent --zone=public --add-port=30000-30010/udp
firewall-cmd --reload

Сервисы хранятся как обычные XML-файлы в /usr/lib/firewalld/services. Свой сервис делает зону читаемой и через полгода:

firewall-cmd --permanent --new-service=nodeapp
firewall-cmd --permanent --service=nodeapp --set-short="Node app"
firewall-cmd --permanent --service=nodeapp --add-port=3000/tcp
firewall-cmd --reload
firewall-cmd --permanent --zone=public --add-service=nodeapp
firewall-cmd --reload

Первый reload не опечатка: firewalld проверяет имя сервиса в момент, когда зона на него ссылается, значит определение уже должно быть загружено. Для удаления есть --remove-service и --remove-port. А --runtime-to-permanent сохраняет всё, что сейчас активно, и это грубый инструмент: вместе с нужным он сохранит и временное правило, добавленное для отладки.

Как ограничить порт одним источником

Обычный --add-port открывает порт всему интернету. Для базы данных или агента мониторинга rich rule сужает его до одного источника:

firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" source address="203.0.113.10/32" port port="5432" protocol="tcp" accept'
firewall-cmd --reload
firewall-cmd --zone=public --list-rich-rules

Две частые ошибки. Если 5432 остался в обычном списке портов, rich rule не меняет ничего, потому что правило порта уже принимает отовсюду: проверьте --list-ports и уберите его. И family="ipv4" покрывает только IPv4, а у вашего VPS почти наверняка есть и маршрутизируемый IPv6, поэтому добавьте парное правило с family="ipv6", иначе ограничение получается половинчатым. Rich rule без адреса вообще не требует атрибута family и работает для обоих протоколов:

firewall-cmd --permanent --zone=public --add-rich-rule='rule service name="ssh" accept limit value="10/m"'
firewall-cmd --reload

Этот лимит начинает действовать только тогда, когда из зоны убран обычный сервис ssh, по той же причине, что и с портом 5432 выше. Убирайте его только со страховкой из следующего раздела, а не просто так.

Порядок правил внутри зоны не совпадает с порядком ввода: при равном приоритете firewalld применяет сначала правила логирования, потом запрещающие, потом разрешающие, поэтому rich rule с запретом сильнее обычного --add-service. Для явного управления есть атрибут priority. Чтобы увидеть, что именно отбрасывается:

firewall-cmd --set-log-denied=all
journalctl -k -f

Закончив, верните firewall-cmd --set-log-denied=off, иначе на публичном адресе журнал забьётся шумом сканеров.

Как увидеть, что открыто на самом деле

firewall-cmd --zone=public --list-all показывает намерение: target, интерфейсы, источники, сервисы, порты, masquerade, rich rules. ss показывает реальность.

ss -tulpen
nft list chain inet firewalld filter_IN_public_allow

Читайте их вместе. Порт, открытый в firewalld, но никем не слушаемый, на практике закрыт; демон, повешенный на 0.0.0.0, отделён от публичности одной опечаткой, а повешенный на 127.0.0.1 безопасен независимо от фаервола, и сокет на [::] обычно принимает ещё и IPv4. Проверяйте снаружи: nc -zv ваш.адрес 443 с другой машины, но не с самого сервера.

Особенность AlmaLinux: SELinux по умолчанию в режиме enforcing, и открытый в firewalld порт 8081 сам по себе не даёт nginx на него сесть. Если демон не стартует сразу после того, как вы открыли порт, причина обычно именно эта. Прежде чем менять метку, посмотрите, что у порта уже есть: http_port_t покрывает 80, 81, 443, 488, 8008, 8009, 8443 и 9000, а tcp/8081 в штатной политике уже принадлежит другому типу (transproxy_port_t). Порту, за которым тип уже закреплён, метку меняют ключом -m, а -a годится только для порта без типа, иначе semanage останавливается с ошибкой "Port tcp/8081 already defined".

dnf install policycoreutils-python-utils
semanage port -l | grep 8081
semanage port -m -t http_port_t -p tcp 8081
ausearch -m avc -ts recent

Как менять правила по SSH и не потерять доступ

Никогда не правьте постоянные правила, задевающие SSH, с последующим reload, если у вас нет второго пути внутрь. Работайте в runtime, с таймаутом, и закрепляйте изменение только после проверки. Сама по себе rich rule ничего не ограничивает: в зоне public по-прежнему разрешён сервис ssh, поэтому SSH остаётся открытым всему интернету, и это ровно та ловушка, которая выше описана для порта 5432. Ограничение начинается только в тот момент, когда вы этот сервис ещё и убираете:

firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="203.0.113.10/32" service name="ssh" accept' --timeout=10m
firewall-cmd --zone=public --remove-service=ssh
systemd-run --on-active=5m --unit=fw-rollback firewall-cmd --reload

--timeout работает только в runtime, не сочетается с --permanent и относится только к добавленному правилу, но не к удалению сервиса: сервис ssh сам по себе не вернётся. Строка с systemd-run работает как предохранитель: через пять минут она перезагрузит firewalld и выбросит все runtime-изменения, и именно этот reload возвращает сервис из постоянной конфигурации, если проверка не удалась. Теперь откройте новую SSH-сессию из другого терминала. Именно этот шаг и есть смысл всей затеи: reload сохраняет состояние отслеживания соединений, поэтому текущая сессия переживёт правила, которые новое подключение уже не пропустят. Если новая сессия работает, сразу отмените откат и сохраните, не дожидаясь конца таймаута:

systemctl stop fw-rollback.timer
firewall-cmd --runtime-to-permanent

Время здесь важно. У удаления сервиса ssh таймаута нет, а у rich rule он есть, поэтому, если правило истечёт раньше, чем вы сохраните, в runtime не останется ни того, ни другого, и --runtime-to-permanent запишет ровно это: постоянную потерю доступа, которую текущая сессия скрывает. Если время ушло, добавьте rich rule заново перед сохранением или выполните firewall-cmd --reload и начните сначала.

Удалённо избегайте двух команд: --complete-reload, которая сбрасывает conntrack и рвёт установленные сессии, и --panic-on, которая блокирует всё, включая вас. Держите открытой консоль из панели хостинга. Если машина в нашем рижском дата-центре всё же станет недоступной, наши инженеры на связи круглосуточно, а такую работу мы делаем и в рамках IT-поддержки.

nftables напрямую и как он уживается с firewalld

firewalld строит одну таблицу, inet firewalld, а её базовые фильтрующие цепочки цепляются с приоритетом filter + 10, намеренно после классического фильтрующего приоритета, поэтому написанная руками таблица на стандартном приоритете считается первой. Здесь важно понимать вердикты: drop окончателен сразу, а accept завершает только свою цепочку, и пакет всё равно проходит остальные базовые цепочки того же хука, так что пропустить его должны оба набора правил. Никогда не правьте inet firewalld руками, firewalld перезаписывает её при каждой перезагрузке правил.

Если вам ближе чистый nftables, выбирайте одно из двух и готовьте замену до того, как снимете работающий фаервол. Юнит грузит ровно один файл, /etc/sysconfig/nftables.conf, и в поставляемом виде он не подключает ничего: строка с /etc/nftables/main.nft закомментирована, а сам этот образец разрешает только ssh и 9090. Поэтому сначала запишите свой набор правил в /etc/nftables/main.nft, поверх образца. Начинайте его с flush ruleset, иначе повторное применение файла добавит дубли вместо замены. Рабочий минимум:

flush ruleset

table inet filter {
  chain input {
    type filter hook input priority filter; policy drop;
    ct state established,related accept
    ct state invalid drop
    iif lo accept
    ip protocol icmp accept
    ip6 nexthdr ipv6-icmp accept
    tcp dport 22 accept
    tcp dport { 80, 443 } accept
  }
  chain forward { type filter hook forward priority filter; policy drop; }
  chain output { type filter hook output priority filter; policy accept; }
}

Теперь укажите файлу, который читает юнит, на ваш набор правил, проверьте, что именно он загрузит, и запустите сервис. grep обязан напечатать строку include: если вывод пуст, sed не нашёл шаблон и строку в /etc/sysconfig/nftables.conf нужно дописать руками. flush ruleset в начале файла заодно очищает таблицу, построенную firewalld, и при замене это и нужно, поэтому с этого момента между машиной и интернетом стоят только ваши правила:

dnf install nftables
sed -i 's|^#include "/etc/nftables/main.nft"|include "/etc/nftables/main.nft"|' /etc/sysconfig/nftables.conf
grep -n '^include' /etc/sysconfig/nftables.conf
nft -c -f /etc/sysconfig/nftables.conf
systemctl enable --now nftables
nft list ruleset

Сначала прочитайте последний вывод: в нём должны быть ваша цепочка input, её policy drop и строка с портом 22. Затем откройте из другого терминала новую SSH-сессию, ровно так же, как в разделе про firewalld. Если она не проходит или набор правил не тот, что вы писали, возвращайтесь назад: systemctl stop nftables и systemctl restart firewalld вернут старые правила. Не считайте, что остановка юнита что-то очищает: посмотрите systemctl cat nftables, потому что сброс набора при stop зависит от юнит-файла конкретной версии, и, если сброса нет, перед запуском firewalld выполните nft flush ruleset вручную.

Снимать старый фаервол можно только после того, как новая сессия заработала:

systemctl disable --now firewalld
systemctl mask firewalld

Две заметки с боевых машин. Контейнерные движки пишут собственные правила netfilter, и перезагрузка firewalld может стереть то, что вставил Docker, поэтому если опубликованный порт умирает сразу после reload, перезапустите движок. И у fail2ban отдельные действия для firewalld и для nftables, причём имена менялись между 0.11 и 1.0, так что загляните в /etc/fail2ban/action.d/ и убедитесь, что баны реально видны в nft list ruleset.

Узкий случай, когда выключить можно

Один случай защитим: у машины нет глобально маршрутизируемого адреса, и она стоит за аппаратным фаерволом или пограничным ACL, которым вы управляете и который можете посмотреть. Это описывает выделенный сервер в приватном VLAN за вашей собственной железкой. Более узкий вариант: узел, чей путь пакетов полностью ведёт другой контроллер, например Kubernetes CNI, где firewalld только мешает.

systemctl disable --now firewalld
systemctl mask firewalld
nft list ruleset

От последней команды ждите пустой вывод. Маскируйте, а не просто отключайте, иначе обновление пакетов может тихо запустить сервис обратно. И всё равно привяжите демоны к приватному адресу (listen_addresses в PostgreSQL, bind-address в MySQL), потому что фраза "перед ним же стоит фаервол" отделена от неправды одним неверно настроенным портом коммутатора. Не защитимо: выключать потому, что порт не открылся, потому, что так написано в инструкции 2014 года, или потому, что с ним поспорил контейнерный движок. Все три случая диагностируются за то время, которое занимают ss -tulpen и --list-all.

Если всё равно не работает

СимптомЧто проверить
Правило добавлено, соединение отклоняетсяКто-нибудь слушает, та ли зона, был ли reload, метка порта в SELinux
Из офиса работает, из дома нетfamily в rich rule, маска CIDR или сменившийся адрес провайдера
После перезагрузки всё вернулосьПравило было только runtime, без --permanent
Порт открыт, снаружи тишинаФильтр выше хоста: пограничный ACL или роутер
Баны ничего не даютДействие fail2ban не соответствует фаерволу, который у вас реально работает
Хотите, чтобы это делали мы?
VPS. NVMe виртуальные серверы, запуск за минуту.
Подробнее

Готовы начать?

Запустите за минуты или обсудите с инженером, что подходит вашему проекту.