Короткий ответ

На Ubuntu штатный путь — UFW (обёртка над nftables) либо один ручной nftables.service, не оба вперемешку. Цель: default deny incoming, allow outgoing (пока нет модели egress), явный allow SSH для admin с LAN/jump 10.0.20.0/24, затем точечные порты приложений. Включайте ufw enable только когда вторая сессия SSH уже открыта и правило OpenSSH видно в numbered. Не используйте ufw disable / nft flush / iptables -F как рабочее состояние.

Хост host.example (10.0.20.10).

Симптомы и как отличить

  • sudo ufw statusinactive;
  • с интернета открывается :5432, :6379, :2375;
  • nft list ruleset пустой или hook input policy accept без ограничений;
  • «защищает облако», при этом SG 0.0.0.0/0.
КартинаНе «FW не настроен»Куда
UFW active, сервис недоступенлишнее deny / не тот NICчинить правило, не disable
Порты слушают localhostоклишние порты если 0.0.0.0
Fail2Ban не банитjailFail2Ban
Timeout SSHвы уже отрезали себяконсоль, не новый enable вслепую

Возможные причины

  1. Образ cloud с пустым UFW «чтобы не мешал».
  2. Кто-то сделал ufw disable из-за одного BLOCK и не вернул.
  3. Включён iptables-legacy в голове админа, на диске nft, правила «не те».
  4. nftables.service masked, UFW тоже off.
  5. Docker публикует порты в обход ожиданий: DOCKER-USER, не только ufw-user.
  6. IPv6 IPV6=no в UFW при живом AAAA — дыра по v6.

Диагностика

sudo ufw status verbose
sudo ufw status numbered
systemctl is-enabled ufw nftables 2>/dev/null
sudo nft list ruleset | head -n 80
ss -tulnp
ip -br link; ip -br addr

Сверьте слушателей с матрицей доступов. Скан с jump-хоста (не с самого сервера):

# с admin-станции в LAN 10.0.20.0/24
nc -vz 10.0.20.10 22
nc -vz 10.0.20.10 5432

Облако: отдельно выгрузка SG. Гостевой UFW active при SG any-any = периметр открыт.

Docker:

docker ps --format '{{.Names}} {{.Ports}}' 2>/dev/null
sudo nft list table ip filter 2>/dev/null | grep -A20 DOCKER || true

Проверка IPv6:

cat /etc/default/ufw | grep -E '^IPV6='
sudo ufw status verbose | head
ip -6 addr

Решение

Выберите один движок.

Сценарий A. UFW (рекомендуемый на Ubuntu)

Порядок, консоль открыта:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 10.0.20.0/24 to any port 22 proto tcp comment 'ssh-admin'
sudo ufw show added

Если нужен SSH ещё с одного /32 jump:

sudo ufw allow from 10.0.20.10 to any port 22 proto tcp comment 'self-or-jump'

10.0.20.10 здесь сам хост — для allow с LAN используйте адрес клиента, не путайте. Jump-хост подставьте свой.

Приложение, которое должно быть с LAN:

sudo ufw allow from 10.0.20.0/24 to any port 5432 proto tcp comment 'postgres-lan'

Не allow 5432/tcp в Anywhere, если база не публичная.

Включение:

sudo ufw --force enable
sudo ufw status numbered
sudo systemctl enable ufw

--force не спрашивает tty. Без allow SSH не используйте.

Проверка новой SSH-сессии до закрытия старой. Дальше — матрица портов.

IPv6: в /etc/default/ufw IPV6=yes, правила dual-stack. Не отключайте IPv6 в sysctl «вместо firewall».

Сценарий B. Чистый nftables без UFW

UFW должен быть inactive, не два фильтра.

sudo ufw status
sudo systemctl disable --now ufw
sudo apt-get install -y nftables
sudo systemctl enable nftables

Базовый /etc/nftables.conf (идея; адаптируйте NIC ens3):

flush ruleset
table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;
    ct state established,related accept
    iif lo accept
    ip saddr 10.0.20.0/24 tcp dport 22 accept
    icmp type echo-request accept
    icmpv6 type { echo-request, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
  }
  chain forward { type filter hook forward priority 0; policy drop; }
  chain output { type filter hook output priority 0; policy accept; }
}
sudo nft -c -f /etc/nftables.conf
sudo systemctl restart nftables

flush внутри загружаемого файла при nft -f — штатно для этого файла, это не то же самое, что интерактивный flush на живой сессии без готового allow. Сначала -c синтаксис, консоль открыта.

Не ставьте policy accept на input «на время отладки» и не уходите с работы.

Сценарий C. Docker публикует минуя UFW

Правило в DOCKER-USER / документация Docker+UFW. Не публикуйте -p 0.0.0.0:3306:3306. Слушайте 127.0.0.1 или внутреннюю сеть. См. лишние порты и docker.sock.

Сценарий D. Fail2Ban рядом

Один datapath: при UFW — banaction = ufw. Не выключайте UFW, чтобы Fail2Ban «заработал».

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

sudo ufw status verbose
# или
sudo nft list chain inet filter input
ss -tulnp

Ожидание: incoming default deny; 22 разрешён с admin-сети; лишние порты с «интернет- hop» не коннектятся (nc -vz timeout). SSH admin@10.0.20.10 жив. systemctl is-enabled ufw или nftables — enabled. Firewall active.

Скан с внешней точки должен совпасть с матрицей, не с ss на localhost.

Если не помогло

  • После enable нет SSH: консоль, ufw status numbered, добавьте allow, не disable навсегда.
  • Правила есть, трафик не режется: смотрите облако, второй интерфейс, IPv6, Docker.
  • ufw deny сервиса, который слушает 127.0.0.1 — UFW ни при чём, это не inbound WAN.
  • Конфликт UFW+nftables.service: оставьте один.
  • AppArmor DENIED на сервис — не firewall; AppArmor, не ufw disable.

Профилактика

  • Cloud-init/ansible: default deny + SSH CIDR в образе.
  • Алерт: ufw status inactive.
  • Любой новый listen — тикет и правило с source, не Anywhere.
  • Регулярный baseline.
  • Не держать «временный» allow 22 from any месяцами.

FAQ

UFW или nft напрямую?

На Ubuntu Server UFW достаточно и проще не сломать. nft — если уже есть сложные таблицы и вы ими владеете. Не оба.

Нужен ли deny outgoing?

Для high-security — да, с allow DNS/NTP/HTTP(S) к репозиториям. На обычном app-сервере allow outgoing приемлемо. Не копируйте deny outgoing без DNS: сломаете apt и обновления.

iptables -L пустой, ufw active

Нормально: backend nft. Смотрите nft list / ufw status, не только iptables-legacy.

Можно ли policy drop на forward и сломать VM-роутер?

На хосте-роутере/гипервизоре forward не drop вслепую. Эта статья про сервер приложений, не про шлюз. Для шлюза пишите явный forward.

ufw reset

Сносит правила. Только с консолью и готовым списком allow. Не как «ремонт».