VPS и Linux · ufw

Брандмауэр UFW на Ubuntu Server: от нуля до продакшена

Закрываем всё лишнее на VPS одной утилитой: политики по умолчанию, SSH без риска запереть себя снаружи, правила по IP и rate-limit, IPv6, логи — и две вещи, о которых молчат почти все гайды: Docker в обход ufw и NAT для VPN.

Серия «VPS с нуля»
  1. Доступ через Termius и SSH-ключи
  2. Брандмауэр UFW на Ubuntu Server
  3. Fail2ban для SSH на VPSскоро
  4. Автообновления и unattended-upgradesскоро

Что такое ufw и зачем он на VPS

UFW (Uncomplicated Firewall) — надстройка над сетевым фильтром ядра Linux. Писать правила iptables или nftables руками долго и легко ошибиться; ufw превращает их в человекочитаемые команды вида ufw allow 443/tcp. В Ubuntu 22.04 и 24.04 ufw под капотом работает через iptables-nft, то есть правила в итоге попадают в nftables.

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

◆ ufw и файрвол хостера — это разные вещи
Многие провайдеры VPS дают облачный файрвол в панели управления — он режет трафик ещё до вашего сервера. Это хороший внешний слой, но не замена ufw: облачный файрвол не знает о Docker, не логирует попытки на самом сервере и остаётся у провайдера, если вы переедете. Лучшая схема — оба слоя с одинаковым набором открытых портов.

Перед стартом: страховка от самоблокировки

Единственный по-настоящему опасный момент в работе с ufw — включить его, не разрешив SSH. Сессия оборвётся, и попасть на сервер можно будет только через VNC-консоль хостера. Две страховки:

ssh · deploy@vps · страховка
# ставим таймер на отключение ufw через 5 минут
sudo systemd-run --on-active=5min --unit=ufw-deadman /usr/sbin/ufw disable

# ... настраиваем и включаем ufw, проверяем вход в новой сессии ...

# всё работает — отменяем таймер
sudo systemctl stop ufw-deadman.timer

Шаг 1 — установка и проверка состояния

В Ubuntu Server ufw обычно уже установлен, но выключен. Проверим:

ssh · deploy@vps
sudo apt update && sudo apt install ufw -y
sudo ufw status verbose
# Status: inactive

# что сейчас реально слушает порты — это список кандидатов на правила
sudo ss -tulnp

Вывод ss -tulnp — полезная ревизия. Всё, что слушает 0.0.0.0 или [::], доступно из интернета. Всё, что слушает 127.0.0.1, — только локально, и для него правила не нужны.

Шаг 2 — политики по умолчанию

Политика по умолчанию срабатывает, если пакет не совпал ни с одним правилом. Классическая схема для сервера — запретить всё входящее и разрешить всё исходящее:

ssh · deploy@vps
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw default deny routed       # трафик, который идёт "сквозь" сервер (Docker, VPN)
✓ а исходящий трафик закрывать?
default deny outgoing — мощная защита (заражённый сервер не сможет «позвонить домой»), но тогда придётся явно разрешать DNS, NTP, apt-репозитории и всё, куда ходят ваши сервисы. Для первого сервера оставьте allow outgoing и вернитесь к этому, когда освоитесь.

Шаг 3 — разрешаем SSH (до включения!)

Есть три варианта — от простого к лучшему:

ssh · deploy@vps
# вариант 1: по имени профиля приложения (порт 22/tcp)
sudo ufw allow OpenSSH

# вариант 2: если SSH на нестандартном порту
sudo ufw allow 2222/tcp comment 'ssh'

# вариант 3 (рекомендуется): с ограничением частоты подключений
sudo ufw limit 22/tcp comment 'ssh rate-limit'

ufw limit разрешает подключение, но блокирует IP, который открывает 6 и более соединений за 30 секунд. Обычному человеку это не мешает, а примитивный брутфорс резко тормозит. Вместе с входом только по ключу и fail2ban это закрывает SSH практически полностью.

⚠ если ваш домашний IP статический
Можно вообще разрешить SSH только с него: sudo ufw allow from 198.51.100.7 to any port 22 proto tcp. Это самый сильный вариант, но если провайдер сменит IP, вы останетесь снаружи — VNC-консоль хостера должна быть под рукой.

Шаг 4 — включаем файрвол

ssh · deploy@vps
# последний раз убедимся, что SSH в списке
sudo ufw show added

sudo ufw enable
# Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
# Firewall is active and enabled on system startup

sudo ufw status verbose
# Status: active
# Logging: on (low)
# Default: deny (incoming), allow (outgoing), deny (routed)
#
# To                         Action      From
# --                         ------      ----
# 22/tcp                     LIMIT IN    Anywhere          # ssh rate-limit
# 22/tcp (v6)                LIMIT IN    Anywhere (v6)     # ssh rate-limit

Не закрывая текущую сессию, откройте новое подключение в Termius. Зашли — отменяйте «мёртвую руку» из раздела выше. ufw включён и будет подниматься автоматически при перезагрузке.

Шаг 5 — веб-сервер и другие сервисы

Открываем только то, что действительно должно быть доступно снаружи. Несколько типовых примеров:

ssh · deploy@vps
# веб-сервер: HTTP нужен для выпуска сертификатов Let's Encrypt (HTTP-01)
sudo ufw allow 80/tcp  comment 'http'
sudo ufw allow 443/tcp comment 'https'

# HTTP/3 (QUIC) работает поверх UDP
sudo ufw allow 443/udp comment 'http3'

# несколько портов одним правилом — протокол обязателен
sudo ufw allow 80,443/tcp

# диапазон портов
sudo ufw allow 60000:61000/udp comment 'mosh'

# WireGuard
sudo ufw allow 51820/udp comment 'wireguard'

iПрофили приложений

Пакеты вроде nginx и OpenSSH кладут свои профили в /etc/ufw/applications.d/. Удобно, когда сервис использует несколько портов:

ssh · deploy@vps
sudo ufw app list
# Available applications:
#   Nginx Full
#   Nginx HTTP
#   Nginx HTTPS
#   OpenSSH

sudo ufw app info "Nginx Full"
sudo ufw allow "Nginx Full"

Правила по IP и подсетям

Административные панели, базы данных и мониторинг не должны быть открыты всему интернету. Открывайте их только для конкретных адресов:

ssh · deploy@vps
# всё с одного IP (например, ваш офис)
sudo ufw allow from 198.51.100.7

# конкретный порт с одного IP
sudo ufw allow from 198.51.100.7 to any port 5432 proto tcp comment 'postgres for office'

# подсеть — например, приватная сеть между серверами хостера
sudo ufw allow from 10.10.0.0/24 to any port 9100 proto tcp comment 'node_exporter'

# только на определённом интерфейсе
sudo ufw allow in on wg0 to any port 53 comment 'dns for vpn clients'

# заблокировать надоедливую сеть
sudo ufw deny from 203.0.113.0/24
✓ deny против reject
deny молча отбрасывает пакет — сканер ждёт таймаута и не знает, есть ли там что-то. reject сразу отвечает «порт закрыт». Для входящего трафика из интернета используйте deny; reject удобен внутри своей сети, чтобы клиенты не висели на таймаутах.

Порядок, удаление и вставка правил

ufw проверяет правила сверху вниз, и первое совпадение решает. Поэтому deny from 203.0.113.0/24, добавленный после allow 443/tcp, для HTTPS не сработает — пакет уже пропущен правилом выше. Блокирующие правила ставьте в начало через insert:

ssh · deploy@vps
sudo ufw status numbered
#      To                         Action      From
# [ 1] 22/tcp                     LIMIT IN    Anywhere
# [ 2] 80/tcp                     ALLOW IN    Anywhere
# [ 3] 443/tcp                    ALLOW IN    Anywhere
# [ 4] 22/tcp (v6)                LIMIT IN    Anywhere (v6)
# ...

# вставить правило на первую позицию
sudo ufw insert 1 deny from 203.0.113.0/24

# удалить по номеру (номера сдвигаются после каждого удаления!)
sudo ufw delete 3

# удалить по описанию — надёжнее в скриптах
sudo ufw delete allow 80/tcp

# посмотреть изменения без применения
sudo ufw --dry-run allow 8443/tcp

# полный сброс к заводским настройкам (выключит ufw!)
sudo ufw reset
⚠ ufw reset
reset удаляет все правила и выключает файрвол, сохраняя бэкапы старых файлов в /etc/ufw/*.rules.<дата>. После сброса снова начинайте с шага 2 — и снова с SSH до enable.

IPv6 — не забыть вторую половину

Если у VPS есть IPv6-адрес, а файрвол фильтрует только IPv4, все ваши сервисы открыты через IPv6. В Ubuntu поддержка включена по умолчанию, но проверьте:

ssh · deploy@vps
grep IPV6 /etc/default/ufw
# IPV6=yes   ← должно быть так

При IPV6=yes каждое правило без указания адреса автоматически создаётся в двух вариантах — это те самые строки с пометкой (v6) в выводе status. Если поменяли значение, выполните sudo ufw reload.

Логи: кто стучится на сервер

ssh · deploy@vps
# уровень: off | low | medium | high | full  (low — разумный дефолт)
sudo ufw logging low

# живой поток заблокированных пакетов
sudo tail -f /var/log/ufw.log

# топ-10 IP, которые стучались в закрытые порты
sudo grep "UFW BLOCK" /var/log/ufw.log | grep -oP 'SRC=\K[^ ]+' | sort | uniq -c | sort -rn | head

# топ-10 портов, которые сканируют
sudo grep "UFW BLOCK" /var/log/ufw.log | grep -oP 'DPT=\K[0-9]+' | sort | uniq -c | sort -rn | head

Запустите последние две команды через сутки после включения ufw — обычно наверху окажутся 22, 23, 3389, 445 и 5900. Это наглядно показывает, от чего файрвол вас уже защищает.

Docker и ufw: главная ловушка

Это самый важный раздел статьи для всех, кто запускает контейнеры. Docker публикует порты в обход правил ufw. Когда вы пишете -p 8080:80 или ports: - "8080:80" в compose, Docker добавляет собственные правила NAT и пропускает трафик через цепочку FORWARD, а ufw фильтрует входящий трафик в цепочке INPUT. В итоге ufw status показывает, что 8080 закрыт, а из интернета контейнер прекрасно открывается.

Если вы видите порт закрытым в ufw status, это ещё не значит, что он закрыт. Для Docker проверяйте снаружи — nmap с другой машины или онлайн-сканер портов.

01Решение для 90% случаев: публикуйте на localhost

Если сервис стоит за реверс-прокси (nginx, Caddy, Nginx Proxy Manager), публиковать его порт на внешний интерфейс вообще не нужно. Привяжите его к 127.0.0.1 — тогда снаружи до него не достучаться, а прокси на том же сервере достанет:

docker-compose.yml
services:
  app:
    image: ghcr.io/example/app:latest
    ports:
      - "127.0.0.1:8080:80"   # ← вместо "8080:80"

  db:
    image: postgres:16
    # порты не публикуем вовсе — app ходит в db по внутренней сети compose

Для разового доступа к такой панели с вашего ПК используйте SSH-туннель — в Termius это Port Forwarding.

02Если порт должен быть открыт, но с фильтрацией

Когда контейнер должен быть доступен снаружи, но только с определённых IP, нужно, чтобы ufw управлял и маршрутизируемым трафиком. Популярное готовое решение — скрипт ufw-docker (проект chaifeng/ufw-docker на GitHub): он добавляет в /etc/ufw/after.rules блок, который заставляет трафик к контейнерам проходить через правила ufw, после чего доступ открывается командами вида:

ssh · deploy@vps · после установки ufw-docker
# открыть порт 80 контейнера app для всех
sudo ufw-docker allow app 80

# или вручную — route-правило на порт ВНУТРИ контейнера
sudo ufw route allow proto tcp from 198.51.100.7 to any port 80
◆ почему не "iptables": false
В старых гайдах советуют прописать "iptables": false в /etc/docker/daemon.json. Не делайте этого: Docker перестанет настраивать NAT, и у контейнеров пропадёт доступ в интернет, а сетевые проблемы будут проявляться неочевидно. Привязка к 127.0.0.1 или ufw-docker — правильные пути.

Форвардинг и NAT для VPN

Если на VPS работает VPN (WireGuard, AmneziaWG, OpenVPN), сервер должен пересылать трафик клиентов в интернет. При default deny routed ufw это запрещает — нужно разрешить форвардинг и настроить маскарадинг. Пример для WireGuard с подсетью клиентов 10.8.0.0/24 и внешним интерфейсом eth0 (проверьте свой: ip route | grep default):

01Включаем пересылку пакетов

/etc/ufw/sysctl.conf · раскомментировать
net/ipv4/ip_forward=1
net/ipv6/conf/default/forwarding=1
net/ipv6/conf/all/forwarding=1

02Добавляем NAT в before.rules

Блок *nat добавляется в самое начало файла /etc/ufw/before.rules, перед строкой *filter:

/etc/ufw/before.rules · в начало файла
# NAT для VPN-клиентов
*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
COMMIT

03Разрешаем маршрут и порт VPN

ssh · deploy@vps
sudo ufw allow 51820/udp comment 'wireguard'
sudo ufw route allow in on wg0 out on eth0 comment 'vpn -> internet'
sudo ufw reload

# проверка
sudo ufw status verbose | grep -E "routed|wg0"
sysctl net.ipv4.ip_forward    # = 1
✓ панели вроде 3X-UI и готовые скрипты
Многие установщики VPN сами добавляют правила iptables в обход ufw. После их установки всегда смотрите sudo ufw status и sudo ss -tulnp: панель управления не должна торчать в интернет на случайном порту — закройте её и ходите через SSH-туннель.

Готовый базовый набор правил

Скрипт для свежего VPS с веб-сервером. Запускать при открытой второй SSH-сессии:

ufw-baseline.sh
#!/usr/bin/env bash
set -euo pipefail

SSH_PORT=22          # поменяйте, если перенесли SSH

# страховка: выключить ufw через 5 минут, если что-то пошло не так
systemd-run --on-active=5min --unit=ufw-deadman /usr/sbin/ufw disable || true

ufw --force reset
ufw default deny incoming
ufw default allow outgoing
ufw default deny routed

ufw limit "${SSH_PORT}/tcp" comment 'ssh'
ufw allow 80/tcp  comment 'http'
ufw allow 443/tcp comment 'https'
ufw allow 443/udp comment 'http3'

ufw logging low
ufw --force enable
ufw status verbose

echo
echo "Проверьте вход в НОВОЙ сессии, затем выполните:"
echo "  sudo systemctl stop ufw-deadman.timer"

✓Шпаргалка команд

КомандаЧто делает
ufw status verboseсостояние, политики и правила
ufw status numberedправила с номерами для удаления
ufw allow 443/tcpоткрыть порт
ufw limit 22/tcpоткрыть с rate-limit (6 подключений / 30 с)
ufw allow from IP to any port Nоткрыть порт только для IP
ufw insert 1 deny from NETзаблокировать сеть с наивысшим приоритетом
ufw delete Nудалить правило по номеру
ufw route allow in on X out on Yразрешить форвардинг между интерфейсами
ufw reloadперечитать конфигурацию после правки файлов
ufw disable / enableвыключить / включить файрвол

Частые проблемы

СимптомПричина и решение
После ufw enable пропал SSHне разрешили SSH или он на другом порту. Зайдите через VNC-консоль хостера: sudo ufw allow <порт>/tcp или sudo ufw disable
Порт закрыт в ufw, но открыт снаружиего публикует Docker. Привяжите порт к 127.0.0.1 или используйте ufw-docker
Правило deny не работаетвыше в списке стоит allow для того же трафика. Используйте ufw insert 1 …
VPN-клиенты подключаются, но нет интернетане включён ip_forward, нет NAT-блока в before.rules или route-правила; проверьте имя внешнего интерфейса
ERROR: Could not find a profile matching 'Nginx Full'nginx не установлен из пакета — профиля нет. Используйте порты: ufw allow 80,443/tcp
Сервис открыт по IPv6IPV6=no в /etc/default/ufw. Поменяйте на yes и выполните ufw reload
Себя же заблокировал ufw limitскрипт или IDE открывает много SSH-соединений подряд. Добавьте свой IP отдельным allow выше правила limit

Итог: входящий трафик закрыт по умолчанию, SSH открыт с rate-limit, наружу смотрят только 80 и 443, контейнеры не публикуют порты мимо файрвола, а VPN-трафик идёт через явно разрешённый маршрут. Следующий слой — fail2ban, который умеет банить через ufw: достаточно указать banaction = ufw в его конфигурации.