SSD Nodes Learn Hosting plans →
Посібники Matt ConnorВід Matt Connor

UFW та IPv6: як закрити відкриті порти на VPS

UFW в Ubuntu 24.04 уже підтримує IPv6. Дізнайтеся, чому cloud firewall і правила iptables можуть залишати сервіси відкритими та як це перевірити й закрити.

Пастка IPv6 у firewall в одному реченні

Ваш firewall захищає IPv4. Ваш VPS майже напевно також має публічну IPv6-адресу, і багато сервісів типово прослуховують її. Якщо ваш firewall охоплює лише IPv4 або ви покладаєтеся на cloud firewall, який фільтрує тільки 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 показує сервіс, який легко забути вимкнути після запуску.

Більшість daemon за замовчуванням прив’язуються до ::, оскільки в Linux сокет :: зазвичай також приймає IPv4-підключення. Тому типовий стан нового сервера такий: «приймати підключення через обидва стеки та на всіх адресах». Єдиним захистом перед цими сервісами є firewall. Саме тому firewall, який бачить лише один стек, є реальною проблемою.

Звідки насправді виникає проблема з IPv6

Є три поширені джерела. На конкретному сервері може бути одне з них або кілька одночасно.

1. Хмарний firewall, який фільтрує лише IPv4. Багато firewall і security-group у провайдерів спочатку створювалися для IPv4, тому вони або ігнорують IPv6, або потребують окремих правил IPv6, які потрібно додати вручну. Якщо єдиний firewall налаштований у панелі провайдера та не охоплює IPv6, ваші [::] сервіси відкриті незалежно від того, що в ньому зазначено про порт 22 для IPv4. Перегляньте документацію провайдера щодо firewall і окремо знайдіть згадки про IPv6.

2. Самостійно написані правила iptables без ip6tables. Команда iptables працює лише з таблицями IPv4. Для IPv6 є окрема команда ip6tables зі своїм набором правил. Якщо ви написали firewall-скрипт із великою кількістю рядків iptables -A INPUT ..., але не додали відповідні правила ip6tables, firewall IPv6 порожній. Порожній ланцюжок INPUT із політикою ACCEPT за замовчуванням дозволяє весь трафік:

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

Цей вивід на одному екрані показує всю проблему. IPv4 фільтрується, а IPv6 приймає трафік з будь-яких адрес.

3. Docker публікує порти в обхід firewall. Коли ви запускаєте 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 або спричиняє тайм-аут. Ці два результати мають різне значення. Різниця між відхиленням з’єднання та тайм-аутом показує, чи відповів хост і відхилив з’єднання, чи firewall мовчки відкинув ваш пакет. Щоб отримати повну картину, проскануйте IPv6-адресу за допомогою nmap із системи за межами сервера:

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

Кожен порт, який nmap показує як відкритий через IPv6, доступний усьому інтернету, незалежно від результатів сканування IPv4. Порівняння сканувань IPv4 та IPv6 поруч — найшвидший спосіб знайти розбіжність: усе, що відкрите на -6, але закрите через IPv4, є сервісом, який ваш firewall не захищає.

Закрийте прогалину

Налаштуйте 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 в одному місці та усувають увесь цей клас помилок. Якщо ви пишете правила самостійно, одна таблиця фільтра nftables inet є найпростішим рішенням. Якщо на вашому VPS працює Rocky або AlmaLinux, а не Ubuntu, UFW для налаштування немає. У такому разі керуйте firewalld як front end, який одночасно застосовує правила зон до обох стеків.

Прив’язуйте сервіси, які не повинні бути публічними, до loopback. Базі даних, панелі адміністрування або endpoint для метрик зазвичай взагалі не потрібна публічна адреса. Прив’яжіть сервіс до 127.0.0.1 і ::1, щоб він від самого початку не прослуховував routable-адресу. Для Postgres встановіть listen_addresses = 'localhost'. Для сервера застосунків прив’яжіть його до 127.0.0.1 і розмістіть reverse proxy перед ним. Закрити listener ефективніше, ніж фільтрувати його через firewall, оскільки тоді до нього взагалі немає доступу.

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

Додайте правила IPv6 у firewall вашого провайдера або визнайте, що він не є вашим firewall для IPv6, і доручіть це завдання UFW або nftables на хості.

Перевірте, що порт справді закритий

Після внесення змін повторіть той самий зовнішній тест:

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

Порт, який раніше відповідав, тепер має відхиляти підключення або завершуватися за тайм-аутом, а nmap має показувати його як відфільтрований або закритий. Якщо порт досі відкритий, повторно перевірте три наведені вище джерела: сервіс усе ще прослуховує ::, а перед ним немає правила; правило Docker розташоване перед UFW; або firewall провайдера взагалі не обробляв 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). Проблема виникає, якщо ви покладаєтеся на firewall провайдера, який фільтрує лише IPv4, якщо власноруч написані правила iptables не мають відповідних правил ip6tables або якщо Docker публікує порт в обхід UFW. IPV6=no (зі старого образу або старого посібника) не відкриває IPv6: з ним активний UFW відкидає весь трафік IPv6, крім loopback. Перевірте цей параметр за допомогою grep IPV6 /etc/default/ufw.

Як перевірити, які порти мій VPS відкриває через IPv6?

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

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

Коли ви публікуєте порт за допомогою -p, Docker вставляє власні правила firewall перед правилами UFW. Тому опублікований порт залишається доступним, навіть якщо ufw status показує його як заборонений. Це відбувається в IPv4, а також в IPv6, якщо підтримку IPv6 у Docker увімкнено. Публікуйте порт на конкретній адресі, наприклад -p 127.0.0.1:8080:80, або розмістіть контейнер за reverse proxy і публікуйте лише порт проксі.

Чи потрібен мені firewall IPv6, якщо мій firewall IPv4 налаштований правильно?

Так. IPv4 та IPv6 — це окремі мережеві стеки з окремими правилами firewall. Ідеальний набір правил IPv4 не впливає на трафік IPv6. Якщо ваш VPS має публічну IPv6-адресу, а вона є майже в усіх VPS, будь-який сервіс, що слухає ::, залишається доступним через IPv6, доки правило firewall IPv6 або прив’язка до loopback не заблокує цей доступ.

Як змусити сервіс слухати лише IPv4 або лише localhost?

Задайте адресу прив’язки сервісу в його конфігурації. Використовуйте 127.0.0.1, щоб дозволити підключення лише через loopback IPv4, або 0.0.0.0, щоб слухати всі адреси IPv4 без listener IPv6. Postgres використовує listen_addresses, SSH — ListenAddress, а більшість серверів застосунків має параметр host або bind. Перевірте результат за допомогою sudo ss -tlnp і переконайтеся, що Local Address більше не показує [::].