SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor

UFW и IPv6: как закрыть открытые порты на VPS

На Ubuntu 24.04 UFW поддерживает IPv6, но облачный файрвол может фильтровать только IPv4. Проверьте доступные службы и закройте пробел на VPS.

Ловушка IPv6-файрвола в одном предложении

Ваш файрвол защищает IPv4. Ваш VPS почти наверняка имеет публичный IPv6-адрес, и многие службы по умолчанию ожидают соединений на нем. Если ваш файрвол настроен только для IPv4 или вы полагаетесь на облачный файрвол, фильтрующий только IPv4, каждая из этих служб доступна из всего интернета по протоколу IPv6, в то время как IPv4-сторона выглядит защищенной. Вы проверяете порт с помощью curl, видите отказ в соединении и чувствуете себя в безопасности. Злоумышленник подключается к тому же порту по IPv6 и получает доступ.

Это руководство показывает, откуда берется данный пробел на обычном VPS с Ubuntu 24.04, как увидеть, что именно вы выставляете наружу, и как это закрыть. UFW здесь не виноват. В современной установке Ubuntu UFW уже поддерживает IPv6. Уязвимость возникает из-за окружающих уровней защиты и служб, о прослушивании которых вы не знали.

Почему ваш VPS по умолчанию использует IPv6

Почти каждый VPS сегодня поставляется с публичным IPv6-адресом, часто целой подсетью /64, в дополнение к IPv4-адресу. Проверьте свой:

ip -6 addr show scope global
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
    inet6 2001:db8:2a::1/64 scope global

Значение 2001:db8:2a::1 в этом руководстве — это заполнитель из 2001:db8::/32, префикса, зарезервированного для документации, который никогда не маршрутизируется в интернете. Реальный адрес, который ваш сервер выводит в этой строке, доступен для маршрутизации из любой точки интернета, точно так же, как и ваш IPv4-адрес. Теперь посмотрите, что ожидает соединений:

sudo ss -tlnp
State   Recv-Q  Local Address:Port   Process
LISTEN  0       0.0.0.0:22           sshd
LISTEN  0       [::]:22              sshd
LISTEN  0       127.0.0.1:5432       postgres
LISTEN  0       [::]:8080            docker-proxy

Внимательно изучите столбец Local Address. 0.0.0.0:22 означает «ожидать соединений на всех IPv4-адресах». [::]:22 означает «ожидать соединений на всех IPv6-адресах». 127.0.0.1:5432 привязан к loopback и не является публичным, поэтому строка с Postgres безопасна. Две строки [::] отвечают всему интернету по IPv6, а строка docker-proxy — это тот случай, когда вы забыли, что запустили сервис.

Большинство демонов по умолчанию привязываются к ::, так как в Linux сокет :: обычно принимает и IPv4-соединения. Таким образом, состояние нового сервера по умолчанию — «отвечать в обоих стеках, отовсюду». Ваш брандмауэр — это единственное, что стоит на пути, поэтому брандмауэр, который видит только один стек, является серьезной проблемой.

Откуда на самом деле берется брешь в IPv6

Существует три распространенных источника. На конкретном сервере может присутствовать один из них или несколько одновременно.

1. Облачный межсетевой экран, фильтрующий только IPv4. Многие межсетевые экраны провайдеров и продукты для управления группами безопасности создавались с расчетом на IPv4. Они либо игнорируют IPv6, либо требуют ручного добавления отдельных правил для IPv6. Если ваш единственный межсетевой экран — это панель управления провайдера, и она не охватывает IPv6, ваши [::] сервисы открыты, независимо от того, что там сказано про 22 порт в IPv4. Изучите документацию провайдера по межсетевому экрану и ищите упоминания именно IPv6.

2. Написанные вручную правила iptables без ip6tables. Команда iptables затрагивает только таблицы IPv4. Для IPv6 существует отдельная команда ip6tables со своим собственным набором правил. Если вы написали скрипт межсетевого экрана, полный строк iptables -A INPUT ..., но не создали соответствующие правила ip6tables, ваш межсетевой экран IPv6 пуст. Пустая цепочка INPUT с политикой по умолчанию ACCEPT разрешает всё:

sudo ip6tables -L INPUT -n
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Этот вывод наглядно демонстрирует всю ловушку. IPv4 фильтруется, а IPv6 принимает запросы отовсюду.

3. Docker публикует порты в обход межсетевого экрана. Когда вы запускаете docker run -p 8080:80, Docker вставляет свои правила перед правилами UFW. Поэтому опубликованный порт доступен, даже если ufw status утверждает, что доступ к нему запрещен. Современный Docker по умолчанию управляет правилами ip6tables, поэтому, когда IPv6 включен в сети контейнера, это правило применяется и к IPv6. В статье Почему Docker обходит UFW и как правильно фильтровать порты контейнеров объясняется механизм и способы исправления. См. основы Docker Compose на VPS, чтобы узнать, как объявляются эти опубликованные порты.

Примечание о настройке IPv6 в UFW. UFW поддерживает IPv6, и соответствующий переключатель находится в одном файле. Проверьте его:

grep IPV6 /etc/default/ufw

В современных версиях Ubuntu установлено IPV6=yes, поэтому UFW применяет каждое правило к обоим стекам. Если вы видите IPV6=no (из старого образа или руководства), IPv6 не остается открытым. При такой настройке активный UFW отбрасывает весь трафик IPv6, кроме трафика на интерфейсе loopback. В этом случае проблема проявляется иначе: всё, что вы планировали предоставлять по IPv6, перестает отвечать. Установите значение yes, если хотите, чтобы UFW фильтровал IPv6 согласно вашим правилам, а не блокировал его полностью.

Точно определите, что именно вы открываете для доступа

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

sudo ss -tlnp | grep '::'

Затем с другого компьютера подключитесь к публичному IPv6-адресу сервера и попробуйте обратиться к порту, который, по вашему мнению, закрыт:

curl -6 -v http://[2001:db8:2a::1]:8080/

Если в ответ придет страница или баннер, значит, порт открыт для IPv6. Закрытый порт выдаст Connection refused или приведет к тайм-ауту. Эти два типа сбоев означают разное, и разница между отказом в соединении и тайм-аутом показывает, ответил ли хост отказом или пакет был молча отброшен межсетевым экраном. Для получения полной картины просканируйте IPv6-адрес с помощью nmap с внешнего узла:

nmap -6 2001:db8:2a::1

Каждый порт, который nmap помечает как открытый по IPv6, доступен из Интернета, независимо от результатов сканирования по IPv4. Сравнение результатов сканирования IPv4 и IPv6 — самый быстрый способ обнаружить брешь: всё, что открыто на -6, но закрыто на IPv4, является сервисом, который вы упустили при настройке межсетевого экрана.

Устранение уязвимостей

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

sudo sed -i 's/^IPV6=no/IPV6=yes/' /etc/default/ufw
sudo ufw default deny incoming
sudo ufw allow 22/tcp
sudo ufw enable
sudo ufw status verbose

Если UFW был активен в момент изменения IPV6=yes, настройки вступят в силу только после выполнения sudo ufw reload.

ufw status выводит каждое правило дважды: один раз в обычном виде, второй — с суффиксом (v6). Наличие строк (v6) означает, что UFW фильтрует IPv6:

22/tcp                     ALLOW IN    Anywhere
22/tcp (v6)                ALLOW IN    Anywhere (v6)

Если вы управляете iptables вручную, дублируйте каждое правило в ip6tables или переходите на nftables, чьи таблицы inet охватывают IPv4 и IPv6 одновременно, что исключает подобные ошибки. Единая таблица фильтрации inet в nftables — наиболее чистое решение при написании правил вручную. Если на вашем VPS используется Rocky или AlmaLinux, а не Ubuntu, то UFW отсутствует, и в качестве интерфейса управления используется firewalld, который применяет правила зон к обоим стекам одновременно.

Привязывайте сервисы, не предназначенные для публичного доступа, к loopback. Базе данных, панели администратора или эндпоинту метрик редко требуется публичный адрес. Привяжите их к 127.0.0.1 и ::1, чтобы они изначально не прослушивали маршрутизируемые адреса. Для Postgres установите listen_addresses = 'localhost'. Для сервера приложений используйте привязку к 127.0.0.1 и установите перед ним reverse proxy. Закрытие порта на уровне приложения надежнее, чем фильтрация файрволом, так как в этом случае сервис становится недоступен для внешних запросов.

Не полагайтесь на UFW при защите опубликованных портов Docker. Публикуйте порты контейнеров на конкретный адрес, а не на все интерфейсы, например -p 127.0.0.1:8080:80, чтобы порт был доступен только с хоста и через настроенный вами прокси. Если контейнер действительно должен быть публичным, разместите его за reverse proxy Traefik и публикуйте только сам прокси, а не каждое приложение по отдельности.

Добавьте правила IPv6 в файрвол вашего провайдера или признайте, что он не обеспечивает фильтрацию IPv6, и поручите эту задачу UFW или nftables на самом хосте.

Проверка фактического закрытия портов

Запустите тот же внешний тест повторно после внесения изменений:

curl -6 -v http://[2001:db8:2a::1]:8080/
nmap -6 2001:db8:2a::1

Порт, который отвечал ранее, теперь должен сбрасывать соединение или выдавать таймаут, а nmap должен показывать статус filtered или closed. Если порт всё ещё открыт, последовательно проверьте три источника, описанных выше: сервис всё ещё привязан к :: без ограничивающего правила, правило Docker перекрывает UFW или сетевой экран провайдера вообще не учитывает IPv6.

Полная изоляция критически важных сервисов от публичного интернета — ещё более надёжный метод. Перенесите SSH и панели администратора за WireGuard VPN и настройте правила firewall так, чтобы они отвечали только внутри туннеля; тогда вопрос доступности по IPv6 станет неактуальным. Чтобы замедлить перебор паролей (brute-force) для сервисов, которые остаются публичными, добавьте Fail2ban перед SSH поверх политики firewall по умолчанию, запрещающей все входящие соединения.

Если понятие портов для вас ново, что такое порты и как сервисы ожидают соединения — это базовый материал, с которого стоит начать.

FAQ

Блокирует ли UFW IPv6 по умолчанию?

В современной установке Ubuntu 24.04 — да. UFW считывает IPV6=yes из /etc/default/ufw и применяет каждое правило как к IPv4, так и к IPv6, а ufw status отображает правила IPv6 с суффиксом (v6). Проблема возникает, если вы полагаетесь на межсетевой экран провайдера, который фильтрует только IPv4, если для написанных вручную правил iptables нет аналогов в ip6tables или если Docker публикует порт в обход UFW. Команда IPV6=no (из старого образа или устаревшего руководства) не открывает доступ по IPv6: при её использовании активный UFW отбрасывает весь IPv6-трафик, кроме трафика на loopback-интерфейсе. Проверьте состояние переключателя с помощью grep IPV6 /etc/default/ufw.

Как проверить, какие порты VPS открыты по IPv6?

Выполните sudo ss -tlnp и обратите внимание на каждый процесс, чей локальный адрес начинается с [::], что означает ожидание соединений на всех IPv6-интерфейсах. Затем с другого компьютера протестируйте публичный IPv6-адрес сервера напрямую с помощью curl -6 -v http://[YOUR:IPV6::ADDR]:PORT/ или просканируйте его через nmap -6 YOUR:IPV6::ADDR. Любой порт, открытый при сканировании IPv6, но закрытый для IPv4, является вашей уязвимостью.

Почему я могу подключиться к порту Docker-контейнера, хотя UFW сообщает, что он заблокирован?

Docker вставляет свои правила межсетевого экрана перед правилами UFW при публикации порта через -p, поэтому опубликованный порт доступен, даже если ufw status помечает его как запрещенный. Это происходит как в IPv4, так и в IPv6, если включена поддержка IPv6 в Docker. Публикуйте порты на конкретный адрес, например -p 127.0.0.1:8080:80, или поместите контейнер за reverse proxy и публикуйте только сам прокси.

Нужен ли мне IPv6-файрвол, если мой IPv4-файрвол настроен надежно?

Да. IPv4 и IPv6 — это отдельные сетевые стеки с отдельными правилами фильтрации. Идеальный набор правил для IPv4 никак не влияет на IPv6-трафик. Если ваш VPS имеет публичный IPv6-адрес (а они есть почти у всех), то любой сервис, ожидающий соединений на ::, остается доступным по IPv6 до тех пор, пока его не ограничит правило IPv6-файрвола или привязка к loopback-интерфейсу.

Как заставить сервис слушать только IPv4 или только localhost?

Укажите адрес привязки в конфигурации самого сервиса. Используйте 127.0.0.1 для привязки только к IPv4-loopback или 0.0.0.0 для всех IPv4-адресов без IPv6-слушателя. Postgres использует listen_addresses, SSH использует ListenAddress, а большинство серверов приложений предоставляют флаг для указания хоста или адреса привязки. Подтвердите результат с помощью sudo ss -tlnp и убедитесь, что в Local Address больше не отображается [::].