Настройка файрвола UFW без потери доступа к серверу

Опубликовано 15 мин. чтения

Верный порядок команд при настройке UFW, правила IPv6, nftables в роли бэкенда, ограничение частоты через ufw limit и путь назад через консоль, если что-то всё-таки пошло не так.

Пакетный фильтр на root-сервере не роскошь, а базовое оснащение. UFW (Uncomplicated Firewall) делает работу с ним приятно простой, но у него есть особенность, из-за которой каждый год тысячи администраторов теряют доступ к собственным серверам: команда, включающая файрвол, способна оборвать ту самую SSH-сессию, из которой её запускают. В этом руководстве показан порядок действий, при котором такого не происходит, и, что почти важнее, путь назад, если это всё-таки случилось.

Все сведения относятся к Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS и Ubuntu 22.04 LTS. Команды рассчитаны на работу от имени root. Если вы работаете под обычным пользователем, добавляйте перед каждой командой sudo.

Почему порядок действий решает всё

Классическая ошибка выглядит так: сначала политика по умолчанию выставляется на «отбрасывать весь входящий трафик», затем включается файрвол, а правило для SSH планируется дописать потом, уже спокойно. Ровно между этими шагами и лежит промежуток, в котором сервер перестаёт быть доступным.

Причина, по которой эта ошибка так часто остаётся незамеченной, коварнее самой ошибки. В файле /etc/ufw/before.rules у UFW есть правило, пропускающее уже установленные соединения:

-A ufw-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

Ваша текущая SSH-сессия — это уже установленное соединение. Она переживёт включение файрвола даже в том случае, если правила для SSH нет вообще. Приглашение командной строки остаётся на месте, всё выглядит нормально. И только следующая попытка подключения, то есть обычно ваш вход на следующее утро, упирается в таймаут. Отсюда правило: пока файрвол не проверен второй, заново установленной сессией, первую сессию закрывать нельзя.

Простое правило: сначала разрешить, потом запретить, потом включить, потом проверить второй сессией, и только после этого закрывать первую.

До первой команды: запасной путь и инвентаризация

Прежде чем менять что-либо в пакетной фильтрации, проясните два вопроса.

1. Как вы попадёте на сервер без SSH?

У KVM root-серверов и выделенных серверов KernelHost VNC-консоль доступна прямо в личном кабинете. Эта консоль подключена не к сетевому стеку гостевой системы, а к слою виртуализации, соответственно к самому сетевому подключению. Поэтому правило файрвола внутри гостя её заблокировать не может. Зайдите через эту консоль один раз заранее и убедитесь, что знаете пароль root. Запасной путь, который впервые пробуют уже в аварийной ситуации, запасным путём не является.

2. Что вообще слушает на этом сервере?

Правила для служб, которых нет, безвредны. А вот служба, которую вы упустили из виду, обойдётся вам потерей доступа или простоем. Составьте общую картину:

ss -lntup

Столбец Local Address:Port при этом чётко разделяет 0.0.0.0:22 (только IPv4), [::]:22 (IPv6, а через dual-stack сокет обычно и IPv4) и 127.0.0.1:3306 (только локально, правило файрвола не нужно). Всё, что привязано к 127.0.0.1 или ::1, открывать не требуется.

Особенно важен фактический порт SSH. Не угадывать, а посмотреть. Команде нужны права на чтение хостовых ключей, поэтому она запускается от root либо через sudo:

sudo sshd -T | grep -i "^port "

Деталь, о которой умалчивают многие руководства: в Ubuntu 24.04 служба SSH запускается через активацию по сокету, то есть через ssh.socket, а не через ssh.service. Там systemctl is-enabled ssh.socket выдаёт enabled, а ssh.service выдаёт disabled. В Debian 12, Debian 13 и Ubuntu 22.04 всё ровно наоборот, там работает классическая постоянно запущенная служба.

Последствие для нашей темы неприятно конкретное: в Ubuntu 24.04 порт, о котором сообщает sshd -T, не обязательно совпадает с портом, который прослушивается на самом деле. Решающим там является параметр ListenStream в /lib/systemd/system/ssh.socket либо в дополняющем файле в каталоге /etc/systemd/system/ssh.socket.d/. Кто сменил свой порт SSH и положился на sshd -T, откроет в UFW неверный номер порта и потеряет доступ при следующем входе. На всех четырёх системах надёжен только один способ: посмотреть на процесс, который действительно слушает:

sudo ss -lntp | grep sshd

3. Аварийный выключатель

На случай, если что-то пойдёт не так, перед рискованным изменением поставьте таймер, который через десять минут сам выключит файрвол:

nohup sh -c 'sleep 600; ufw disable' >/dev/null 2>&1 &

Если всё заработало, отмените его:

pkill -f 'sleep 600; ufw disable'

Команда pkill завершает только внешний процесс оболочки. Процесс sleep продолжает работать осиротевшим и заканчивается без последствий, потому что вызвать после него ufw disable уже некому.

Порядок действий, при котором доступ не теряется

В Debian UFW, как правило, не предустановлен, в Ubuntu Server обычно уже есть. Установка в любом случае не повредит:

apt-get update
apt-get install -y ufw
ufw version

Теперь сам порядок, и именно в таком виде:

ufw allow 22/tcp comment 'SSH'
ufw default deny incoming
ufw default allow outgoing
ufw --force enable

Четыре замечания к этому:

  • Разрешающее правило идёт раньше всего остального. UFW принимает правила и в выключенном состоянии, сохраняя их в /etc/ufw/user.rules. При включении они действуют сразу.
  • Вместо 22/tcp можно использовать профиль приложения, например ufw allow OpenSSH. Но полагаться на это вслепую не стоит: профили поставляет не сам UFW, а установленные пакеты. В Ubuntu каталог /etc/ufw/applications.d/ без установленных служб пуст, и ufw app list выводит там только заголовок Available applications: без единой записи. В Debian уже сам пакет ufw приносит около 38 профилей, и профиль для SSH называется там SSH. Профиль OpenSSH в обоих дистрибутивах приходит из пакета openssh-server, то есть на серверах это обычная ситуация. При нестандартном порте SSH профиль в любом случае окажется бесполезным. Какие профили знает ваша система, покажет ufw app list; запись через порт, ufw allow 22/tcp, работает на всех четырёх системах одинаково и потому надёжнее.
  • ufw enable задаёт интерактивный вопрос: Command may disrupt existing ssh connections. Proceed with operation (y|n)?. В скриптах и в ролях Ansible используйте ufw --force enable, иначе выполнение зависнет.
  • Комментарий после comment появляется в выводе ufw status. Иначе через полгода вы уже не вспомните, зачем открыт порт 8443.

Остальные службы добавляются после, например веб-сервер:

ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'

И только теперь откройте второй терминал и войдите заново. Дело закрыто лишь тогда, когда этот вход прошёл успешно.

IPv6: второе семейство адресов, о котором забывают

IPv6 есть на каждом современном сервере, причём чаще всего без того, чтобы кто-то настраивал его специально. Если думать только про IPv4, в итоге получится файрвол, который управляет ровно половиной трафика, а вторую половину пропускает без разбора. Сначала проверьте, настроены ли вообще глобальные адреса IPv6:

ip -6 addr show scope global

Хорошая новость: во всех четырёх рассматриваемых дистрибутивах в /etc/default/ufw с завода стоит IPV6=yes. Тогда UFW параллельно ведёт правило IPv6 к каждому правилу IPv4. Проверять, а не доверять:

grep '^IPV6' /etc/default/ufw

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

ip6tables -L INPUT -n

В первой строке должно стоять Chain INPUT (policy DROP). Если там policy ACCEPT, а ниже нет цепочек ufw, значит по IPv6 ваш сервер полностью открыт, независимо от того, насколько хорошо выглядят правила IPv4.

Две ловушки:

  • Изменение параметра IPV6 в /etc/default/ufw не вступает в силу через ufw reload. Нужен ufw disable, а следом ufw enable. Ровно в этом промежутке файрвола у вас на короткое время нет, поэтому не делайте этого мимоходом на системе, открытой в интернет.
  • Кто блокирует ICMPv6 целиком, разрушает собственную связность. Neighbor Discovery и «Packet too big» в IPv6 не опция, а часть протокола. Нужные типы UFW разрешает уже в /etc/ufw/before6.rules. Трогайте этот файл, только если точно понимаете, что делаете.

Если правило по порту корректно создано в обоих семействах адресов, при добавлении UFW выводит две строки: Rule added и Rule added (v6). Нет второй строки, нет и половины защиты. Исключение составляют правила с ограничением по источнику: для ufw allow from 203.0.113.10 to any port 22 proto tcp появится только Rule added, и это правильно, потому что у адреса-источника IPv4 нет соответствия в IPv6.

Что UFW пишет на самом деле: nftables в роли бэкенда

Здесь много полузнания. На Debian 12, Debian 13, Ubuntu 22.04 и Ubuntu 24.04 картина одинаковая: UFW по-прежнему говорит на синтаксисе iptables, но сама команда iptables на всех четырёх системах представляет собой средство совместимости iptables-nft. То есть правила попадают в подсистему nftables ядра. Доказательство в одну строку:

iptables -V

Вывод заканчивается на (nf_tables). Если там (legacy), ваша система работает со старым бэкендом. Это работоспособно, но приводит к тому, что в ядре рядом лежат правила из двух разных миров и перекрывают друг друга. Какой вариант выбран, показывает update-alternatives --display iptables.

Со стороны nftables это выглядит так. Важно, что смотреть нужно только после включения файрвола:

apt-get install -y nftables
nft list tables

Дело в том, что пока UFW неактивен, таблицы попросту не существует: iptables-nft создаёт её лишь тогда, когда действительно загружаются правила, то есть не раньше ufw --force enable. До этого nft list tables остаётся пустым, а nft list table ip filter завершается сообщением Error: No such file or directory. Это не дефект, а ожидаемое состояние. Заглядывать внутрь имеет смысл только тогда, когда в списке появились table ip filter и table ip6 filter:

nft list table ip filter

Вывод начинается со строки Warning: table ip filter is managed by iptables-nft, do not touch!, и понимать её следует буквально: смотреть можно, править руками нельзя. Ниже расположены цепочки вида ufw-before-input, ufw-user-input и ufw-after-input. Именно здесь становится виден самый важный конфликт: не смешивайте UFW и написанные вручную правила nft. Команда nft flush ruleset удаляет из ядра все правила UFW, а сам UFW об этом ничего не узнает. После неё ufw status по-прежнему сообщает Status: active, хотя фактически не действует ни одно правило. Это один из самых неприятных источников ошибок вообще, потому что инструмент, которым вы проверяете, вас обманывает. Путь назад:

ufw reload

Поэтому после каждого вмешательства других инструментов работы с файрволом (Docker, Kubernetes, VPN-программы, iptables-persistent) проверяйте не статус UFW, а фактический набор правил в ядре.

ufw limit против перебора паролей, и где он заканчивается

Для SSH в UFW предусмотрено ограничение частоты подключений:

ufw limit 22/tcp comment 'SSH rate limit'

Семантика чётко описана в руководстве: соединения разрешаются обычным образом, но отбрасываются, как только один адрес источника устанавливает шесть или больше новых соединений в течение 30 секунд. Значения зашиты жёстко и через интерфейс UFW не меняются. Под капотом работает модуль recent, его видно в выводе iptables -S. Для IPv6 UFW создаёт равнозначное правило, если доступен соответствующий модуль ядра, а он доступен во всех четырёх дистрибутивах.

Если раньше вы уже задали ufw allow 22/tcp, сейчас появится второе правило. Старое нужно убрать, иначе оно сработает первым и ограничение окажется бесполезным:

ufw status numbered
ufw delete allow 22/tcp

Если вместо этого удалять по номеру (ufw delete 3), учтите две особенности. Во-первых, ufw status numbered нумерует правила IPv4 и IPv6 сквозным образом в одном общем списке, и после каждого удаления все последующие номера сдвигаются. Поэтому удаляйте всегда по одному правилу и выводите список заново, либо идите от самого большого номера вниз. Во-вторых, UFW при этом задаёт интерактивный вопрос: Proceed with operation (y|n)?.

А теперь честная оценка, которой в большинстве руководств нет:

  • Против распределённых атак это не помогает. Подсчёт ведётся по каждому адресу источника отдельно. Ботнет с тысячей адресов делает по пять попыток с адреса и остаётся ниже порога.
  • Оно бьёт по вашей собственной автоматизации. Скрипт резервного копирования с множеством отдельных вызовов rsync, запуск Ansible с несколькими форками или задание CI могут упереться в тот же предел. Тогда без доступа останется не атакующий, а ваш собственный конвейер развёртывания. Для таких источников лучше поставить перед ограничением явное исключение, например ufw allow from 203.0.113.10 to any port 22 proto tcp с реальным адресом вашего сборочного сервера.
  • Оно не заменяет аккуратную настройку SSH. Самый действенный шаг против подбора паролей: отключить пароли вовсе, то есть PasswordAuthentication no в /etc/ssh/sshd_config либо в файле в каталоге /etc/ssh/sshd_config.d/. Кто не принимает пароли, у того их и не подберут. В дополнение имеет смысл Fail2ban: в отличие от ufw limit, он реагирует на записи в журналах и блокирует дольше.

Как понять, что всё действительно сработало

«Команда отработала без ошибок» — это не доказательство. Четыре проверки, которые действительно о чём-то говорят:

Первое: общее состояние.

ufw status verbose

Ожидается вывод такого вида:

Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp                     LIMIT IN    Anywhere
22/tcp (v6)                LIMIT IN    Anywhere (v6)

Решающей является вторая строка с пометкой (v6). Без неё покрытие IPv6 отсутствует.

Второе: перезагрузка. Файрвол, который не переживает перезагрузку, бесполезен.

systemctl is-enabled ufw

Ответ должен быть enabled. А затем действительно перезагрузите сервер один раз и войдите в него. Это единственная проверка, которая по-настоящему отвечает на вопрос.

Третье: взгляд снаружи. Проверьте с другого хоста, действительно ли закрыт порт, который вы намеренно не открывали, например командой nc -zv ВАШ-IP 3306. Важно: проверка с localhost не доказывает ничего, потому что трафик через интерфейс обратной петли UFW пропускает всегда.

Четвёртое: журналы. Здесь между дистрибутивами есть настоящая разница. В настройке по умолчанию low UFW записывает заблокированные пакеты через журнал ядра. В Ubuntu 22.04 и 24.04 присутствует rsyslog, поэтому сообщения дополнительно попадают в /var/log/ufw.log. В Debian 12, и особенно в Debian 13, в минимальных установках rsyslog отсутствует, и этого файла там просто нет. Способ, который работает везде:

journalctl -k -n 50

Искать нужно строки, начинающиеся с [UFW BLOCK]. Если хочется видеть больше, поднимите уровень (ufw logging medium), а затем верните его обратно на ufw logging low. На сервере с публичным трафиком повышенный уровень заполняет накопитель быстрее, чем кажется.

Сообщения об ошибках дословно

ERROR: problem running ufw-init: чаще всего причина в конфликте со вторым инструментом работы с файрволом, обычно с nftables.service или iptables-persistent, либо в смешении бэкендов legacy и nft. Проверьте iptables -V и отключите конкурирующие службы. Кроме того, в составе UFW идёт проверочный скрипт, который последовательно проходит требования к ядру и сообщает, какое требование к модулю не выполняется.

ERROR: Could not find a profile matching 'OpenSSH': профиля приложения нет, потому что пакет openssh-server не установлен или файл в каталоге /etc/ufw/applications.d/ был удалён. В Ubuntu этот каталог без установленных служб и так пуст. Используйте вместо профиля номер порта.

ERROR: Bad port: обычно опечатка или имя службы, которого нет в /etc/services. Номера портов всегда однозначны.

Skipping adding existing rule, соответственно Skipping adding existing rule (v6): это не сообщение об ошибке, а указание на то, что правило уже существует. Если вам кажется, что правило изменено, а оно остаётся прежним, причина именно в этом.

ERROR: Invalid position '0': при удалении и вставке UFW считает с 1, а не с 0. Номера берутся из ufw status numbered и сдвигаются после каждого удаления. Поэтому удаляйте всегда от самого большого номера вниз.

WARN: Rules updated but not applied: правило лежит в конфигурации, но файрвол неактивен. Не хватает команды ufw enable.

Различия дистрибутивов одним взглядом

  • Debian 13 (trixie): UFW нужно доустановить. Бэкенд nf_tables. IPV6=yes с завода. В минимальных установках rsyslog отсутствует, поэтому журналы читаются через journalctl -k. SSH через ssh.service.
  • Debian 12 (bookworm): UFW нужно доустановить. Бэкенд nf_tables. IPV6=yes с завода. rsyslog присутствует в зависимости от варианта установки, поэтому наличие /var/log/ufw.log не гарантировано. SSH через ssh.service.
  • Ubuntu 24.04 LTS: UFW есть в серверных образах, но выключен. Бэкенд nf_tables. IPV6=yes с завода. Файл /var/log/ufw.log присутствует. SSH через активацию по сокету ssh.socket, поэтому прослушиваемый порт всегда проверяйте командой sudo ss -lntp | grep sshd, а не через sshd -T.
  • Ubuntu 22.04 LTS: UFW есть в серверных образах, но выключен. Бэкенд nf_tables. IPV6=yes с завода. Файл /var/log/ufw.log присутствует. SSH через ssh.service.

Что одинаково на всех четырёх системах: после установки UFW всегда выключен. Никто не включит вам файрвол незаметно, и никто не выключит его незаметно.

Docker обходит UFW стороной

Если на сервере работает Docker, действует важное ограничение: опубликованные порты контейнеров игнорируют ваши правила UFW. Docker создаёт собственные цепочки и работает с преобразованием адреса назначения, поэтому пакеты проходят мимо цепочек UFW в INPUT. То есть docker run -p 8080:80 доступен снаружи, хотя ufw status показывает аккуратное «deny incoming».

Самая простая и устойчивая контрмера состоит в том, чтобы вообще не публиковать порт на всех адресах, а только локально, и вести доступ через reverse proxy:

docker run -d -p 127.0.0.1:8080:80 nginx

В файле Compose этому соответствует запись порта "127.0.0.1:8080:80". Как вариант, существует возможность встраивать собственные правила UFW в зарезервированную Docker цепочку DOCKER-USER. Это действенно, но требует сопровождения и повторной проверки при каждом обновлении Docker. Привязка к 127.0.0.1 решает проблему в корне.

Если доступ всё-таки потерян

Это случилось, SSH больше не отвечает. По порядку:

  1. Не перезагружать. Перезагрузка не поможет, ведь UFW включён как служба systemd и восстанавливает свои правила при старте. Сервер вернётся ровно таким же закрытым, каким его выключили.
  2. Открыть VNC-консоль в личном кабинете и войти там под root.
  3. Выключить файрвол: ufw disable. Так вы снова в игре, но и снова без защиты.
  4. Найти причину, а не гадать. Посмотрите ufw status numbered и фактический порт SSH из ss -lntup. В девяти случаях из десяти причина одна из трёх: разрешающего правила для SSH не было вовсе, правило указывает на порт 22, тогда как sshd слушает другой порт, либо доступ был ограничен IP-адресом, которого у вашего подключения уже нет (динамическая выдача адресов у интернет-провайдера).
  5. Исправить и включить заново, на этот раз в правильном порядке и с поставленным аварийным выключателем.

Если нужно полностью сбросить набор правил, есть команда ufw reset. Важно знать: она выключает файрвол и кладёт резервные копии прежних файлов правил в /etc/ufw/, добавляя отметку времени в имя файла. То есть в спорном случае можно перечитать, что действовало раньше. Никогда не запускайте её по SSH-соединению, если рядом не открыта консоль.

Разумная начальная конфигурация

Для типичного веб-сервера полная последовательность выглядит так:

apt-get update
apt-get install -y ufw
ufw default deny incoming
ufw default allow outgoing
ufw limit 22/tcp comment 'SSH rate limit'
ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'
ufw --force enable
ufw status verbose

Обратите внимание: здесь default deny incoming хотя и стоит перед правилом для SSH, но включение происходит в самом конце. Пока UFW неактивен, политика по умолчанию вреда не приносит. Критично исключительно состояние в момент выполнения enable, а к этому времени правило для SSH давно сохранено.

Кто хочет вдобавок сделать административные доступы достижимыми только из собственной сети, работает с правилами, ограниченными по источнику, по образцу ufw allow from 203.0.113.0/24 to any port 22 proto tcp. Ещё чище вовсе не выставлять административные службы в открытую сеть, а подключать их через VPN. Экземпляр WireGuard настраивается на любой из четырёх систем за несколько минут и заменяет целую стопку исключений в файрволе одним открытым портом UDP.

И напоследок оценка, которую стоит держать в голове: UFW фильтрует на самом сервере. Против объёмных атак, насыщающих канал, это по своей природе не помогает, ведь к моменту, когда ядро отбрасывает пакеты, они уже прошли по линии. Для этого нужна фильтрация в сети перед сервером. В KernelHost этим занимается вышестоящая инфраструктура в дата-центре maincubes во Франкфурте-на-Майне с фильтрацией Arbor в реальном времени. Ваш локальный файрвол и защита на уровне сети решают две разные задачи, и нужны обе.

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

Отключит ли «ufw enable» мою текущую SSH-сессию сразу же?
Нет, и в этом как раз и состоит ловушка. В файле /etc/ufw/before.rules UFW разрешает уже установленные соединения (состояние RELATED,ESTABLISHED). Поэтому ваша текущая сессия переживёт включение даже тогда, когда правила для SSH нет вообще. Не сработает только следующее подключение. Всегда проверяйте результат второй, заново установленной сессией, прежде чем закрывать первую.
Нужно ли включать IPv6 в UFW отдельно?
В Debian 13, Debian 12, Ubuntu 24.04 и Ubuntu 22.04 строка IPV6=yes стоит в /etc/default/ufw уже с завода. Тогда UFW автоматически создаёт к каждому правилу его соответствие для IPv6, что видно по сообщению «Rule added (v6)» и по пометке «(v6)» в выводе ufw status. Жёстко проверить это можно командой ip6tables -L INPUT -n: там должно стоять «policy DROP». Изменение параметра IPV6 вступает в силу только после ufw disable и следующего за ним ufw enable, одного ufw reload недостаточно.
UFW использует iptables или nftables?
В определённом смысле и то, и другое. UFW по-прежнему говорит на синтаксисе iptables, но команда iptables во всех четырёх дистрибутивах представляет собой слой совместимости iptables-nft. То есть правила попадают в подсистему nftables ядра и при активном файрволе видны через nft list table ip filter. Пока UFW ещё не включён, этой таблицы не существует вовсе и команда сообщает «Error: No such file or directory». Проверить бэкенд можно через iptables -V, вывод заканчивается на (nf_tables). Не смешивайте UFW с написанными вручную правилами nft: одна команда nft flush ruleset удаляет все правила UFW, тогда как ufw status продолжает сообщать «active».
Что именно делает ufw limit?
ufw limit разрешает соединения обычным образом, но отбрасывает их, как только один адрес источника устанавливает шесть или больше новых соединений в течение 30 секунд. Значения фиксированы и через интерфейс UFW не меняются. Против распределённых атак это не помогает, потому что подсчёт ведётся по каждому адресу источника отдельно. Кроме того, под ограничение может попасть ваша собственная автоматизация, например скрипты резервного копирования или задания CI с множеством параллельных SSH-соединений. Для таких источников поставьте перед ограничением явное исключение.
Я больше не могу зайти на сервер по SSH. Поможет ли перезагрузка?
Нет. UFW включён как служба systemd и восстанавливает свои правила при старте, поэтому сервер вернётся ровно таким же закрытым. Вместо этого воспользуйтесь VNC-консолью в личном кабинете, войдите там под root и выполните ufw disable. После этого ищите причину: отсутствующее правило для SSH, неверный порт или ограничение по источнику на IP-адрес, которого у вашего подключения уже нет.
Почему мои контейнеры Docker доступны снаружи, несмотря на активный файрвол UFW?
Docker создаёт собственные цепочки и работает с преобразованием адреса назначения, поэтому опубликованные порты контейнеров проходят мимо цепочек UFW. Для этого трафика «deny incoming» не действует. Самое устойчивое решение: публиковать порты только локально, то есть -p 127.0.0.1:8080:80 вместо -p 8080:80, и вести доступ через reverse proxy.
Почему на моём сервере с Debian нет файла /var/log/ufw.log?
Потому что там не установлен rsyslog. В минимальных установках Debian 13 системный журнальный демон больше не поставляется, система полагается на systemd-journald; в Debian 12 в зависимости от варианта установки его тоже может не быть. Сообщения при этом никуда не пропадают: читайте их через journalctl -k и ищите строки с [UFW BLOCK]. В Ubuntu 22.04 и 24.04 rsyslog присутствует, там этот файл есть.

UFW Файрвол Linux Debian Ubuntu nftables IPv6 SSH Безопасность сервера Root-сервер