SSD Nodes Learn Hosting plans →
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-08-28

Docker обходить UFW: чому порт залишається доступним

Docker додає DNAT-правила iptables, які обходять UFW: заборонений порт 8080 усе одно відповідає з інтернету. Як це виправити двома способами.

Чому Docker обходить UFW

Docker обходить UFW, тому що опубліковані порти контейнерів не проходять через правила брандмауера, якими керує UFW. Коли ви виконуєте docker run -p 8080:80, Docker додає правило DNAT (трансляція мережевої адреси призначення) до ланцюжка PREROUTING таблиці nat ядра. Це правило змінює адресу призначення кожного пакета на приватну адресу контейнера ще до того, як ядро визначить, куди потрібно передати пакет. Потім змінений пакет пересилається до контейнера через ланцюжок FORWARD, яким керує Docker. Правила UFW розміщені в ланцюжку INPUT, але пакет до нього не потрапляє. Тому ufw status показує заборону за замовчуванням, sudo ufw deny 8080 повідомляє про успішне виконання, а порт 8080 усе одно доступний з усього інтернету.

Це не помилка Docker, і UFW не працює неправильно. Обидва інструменти налаштовують той самий брандмауер ядра. Правила Docker просто застосовуються на попередньому етапі проходження пакета, тому UFW не отримує цей пакет для перевірки. У цьому посібнику показано обхід, пояснено механізм і розглянуто 2 робочі способи виправлення: публікація портів на 127.0.0.1 і фільтрація в ланцюжку DOCKER-USER. Якщо ви ще не працювали з UFW, спочатку налаштуйте його за посібником з основ брандмауера UFW, оскільки брандмауер із забороною за замовчуванням залишається правильною базою для всього іншого на сервері.

Перевірте обхід на власному сервері

Почніть із VPS, на якому активний UFW із політикою deny за замовчуванням для вхідного трафіку. Запустіть web-контейнер із опублікованим портом:

sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpine

ufw status verbose показує Default: deny (incoming), allow (outgoing) і не містить правила для порту 8080. Згідно з власним звітом firewall, порт закритий. Тепер перевірте це з іншої машини, а не з самого сервера:

curl -I http://your-vps-ip:8080/
HTTP/1.1 200 OK

Контейнер відповідає. Додайте явне правило deny і повторіть перевірку:

sudo ufw deny 8080/tcp

Порт і далі відповідає, оскільки правило deny міститься в ланцюжку, до якого пакет не доходить. UFW не працював неправильно. До нього взагалі не зверталися. Саме тому проблему так важко помітити: помилка ніде не виводиться, розгортання завершується успішно, а вивід стану firewall виглядає так само, як у справді захищеного сервера.

Механізм: PREROUTING виконується перед INPUT

Ядро обробляє вхідний пакет у фіксованому порядку, і вся проблема полягає саме в цьому порядку.

  1. Спочатку виконується PREROUTING. Правила в цьому ланцюжку можуть змінити призначення пакета. Правило Docker для опублікованого порту робить саме це.
  2. Далі виконується маршрутизація. Пакет, адресований самому хосту, переходить до ланцюжка INPUT. Пакет, адресований іншій машині, переходить до ланцюжка FORWARD.
  3. Правила UFW розміщені в INPUT. Правила Docker розміщені в FORWARD.

Перегляньте правило Docker для контейнера, який ви щойно запустили:

sudo iptables -t nat -L DOCKER -n
Chain DOCKER (2 references)
target   prot opt source       destination
RETURN   0    --  0.0.0.0/0    0.0.0.0/0
DNAT     6    --  0.0.0.0/0    0.0.0.0/0    tcp dpt:8080 to:172.17.0.2:80

Рядок DNAT пояснює всю ситуацію. Для будь-якого пакета, що надходить на порт 8080, призначення змінюється на 172.17.0.2:80 — адресу контейнера у приватній bridge network Docker. Після цієї зміни пакет уже не адресований хосту, тому маршрутизація спрямовує його шляхом FORWARD. У цьому ланцюжку Docker уже додав правила, які дозволяють трафік до власних мереж. Правило deny 8080/tcp очікує в INPUT пакет, який до нього не надходить.

В Ubuntu 24.04 команда iptables є інтерфейсом над nftables, але порядок ланцюжків і результат залишаються такими самими. UFW і Docker записують правила в один і той самий конвеєр обробки пакетів ядра, але Docker підключається до нього раніше. Це не особливість UFW: firewalld на VPS з Rocky або AlmaLinux фільтрує трафік на тому самому етапі конвеєра, і те саме правило DNAT обходить його. Тому наведені нижче способи виправлення застосовні й там.

Повсякденне виправлення: публікуйте порти на 127.0.0.1

Більшість контейнерів узагалі не потрібно було робити публічними. База даних, сервер застосунку за reverse proxy, панель адміністрування, endpoint метрик — жоден із них не має напряму відповідати на запити з інтернету. Публікуйте їх на loopback-адресі:

docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.29-alpine

Або у файлі Compose:

services:
  web:
    image: nginx:1.29-alpine
    ports:
      - "127.0.0.1:8080:80"

Це працює, оскільки правило DNAT у Docker тепер відповідає лише пакетам, адресованим до 127.0.0.1. Пакет з інтернету не може легітимно містити таку адресу призначення, тому kernel відкидає його ще до застосування будь-якого правила firewall. Порт доступний із host і більше ні звідки. Перевірте прив’язку:

sudo ss -tlnp | grep 8080

У виводі має бути 127.0.0.1:8080, а не 0.0.0.0:8080 або [::]:8080. Потім з іншої машини переконайтеся, що підключення до curl http://your-vps-ip:8080/ відхиляється.

Для сервісів, які мають бути доступними з інтернету, запустіть один reverse proxy, який використовує порти 80 і 443 та маршрутизує запити за іменем host. Інші порти не публікуйте. Саме цей підхід описано в посібнику з Traefik reverse proxy. Завдяки йому self-hosted застосунок, наприклад Nextcloud на VPS, залишається недоступним, окрім підключень через його proxy. Спосіб оголошення записів ports: та решту робочого процесу Compose описано в посібнику з основ Docker Compose.

Коли всі внутрішні контейнери використовують loopback, UFW знову виконує свою звичайну роль: захищає порти, які обслуговує сам host. Створіть тут цей набір правил, а потім виконайте команди по черзі:

ToolUFW rule generator

Реальна фільтрація: ланцюжок DOCKER-USER

Іноді порт контейнера потрібно залишити опублікованим у мережі, але обмежити доступ до нього. Наприклад, це може бути порт репліки бази даних, до якого має доступ лише одна офісна адреса. Для цього Docker надає ланцюжок DOCKER-USER. Кожен пакет, що прямує до будь-якого контейнера, проходить через DOCKER-USER до застосування власних правил accept у Docker. Docker ніколи не записує правила в цей ланцюжок. Він призначений для ваших правил, і Docker не змінює його вміст після перезапуску daemon.

Перед виконанням команди врахуйте один нюанс: коли пакет досягає DOCKER-USER, перезапис DNAT уже виконано. Порт призначення пакета — це порт контейнера (80 у нашому прикладі), а не опублікований порт (8080). Тому правило, яке перевіряє --dport 8080, нічого не зіставить. Надійний спосіб — перевірити порт, до якого клієнт підключився спочатку. Цей порт зберігає connection tracker ядра:

sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP

Це означає: для пакетів, що надійшли через eth0 і належать до з’єднання, початковим портом призначення якого був 8080, відкидайте все, що надійшло не з 10.0.0.10. Перевірка --ctdir ORIGINAL обмежує правило напрямком від клієнта до контейнера, тому пакети відповідей не буде помилково відкинуто. Замініть eth0 на назву вашого публічного інтерфейсу; ip route | grep default виводить цю назву. Перевірте правило так само, як і раніше: curl з дозволеної адреси завершується успішно, а з будь-якої іншої адреси з’єднання завершується тайм-аутом. Така затримка означає, що правило DROP працює, а не те, що за портом немає сервісу. Різниця між відхиленим з’єднанням і з’єднанням, що завершується тайм-аутом — найшвидший спосіб відрізнити відфільтрований порт від сервісу, який просто не прослуховує порт.

Правила, додані командою iptables, зникають після перезавантаження. Оскільки UFW уже керує цим firewall, найкраще зберегти їх у /etc/ufw/after.rules. Додайте блок у кінці файлу:

*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL ! -s 10.0.0.10 -j DROP
COMMIT

Потім виконайте sudo ufw reload. UFW повторно застосовує цей файл під час кожного перезавантаження конфігурації та під час кожного запуску системи. Тому фільтрація контейнерів зберігається там само, де й решта правил firewall, і переживає як перезавантаження системи, так і оновлення Docker.

Чому не слід вимикати інтеграцію Docker з iptables

У старіших відповідях на цю проблему радять задати { "iptables": false } у /etc/docker/daemon.json. Не робіть цього. Правила брандмауера Docker роблять набагато більше, ніж публікують порти. Правило masquerade надає контейнерам доступ до інтернету через адресу хоста. Якщо інтеграцію вимкнути, контейнери не зможуть завантажувати images, звертатися до package mirrors або викликати жоден зовнішній API (application programming interface). Саме правила DNAT забезпечують роботу -p, тому опубліковані порти повністю перестануть працювати. Також зникнуть правила ізоляції, які розділяють окремі мережі Compose. Щоб усунути обхідне підключення, ви зламаєте мережеву взаємодію контейнерів, а всі ці правила доведеться самостійно створювати й підтримувати вручну. Власна документація Docker описує цей параметр як призначений для тих, хто саме це й планує робити. Ланцюжок DOCKER-USER існує саме для того, щоб нікому не потрібно було використовувати цей перемикач.

Сторона IPv6 тієї самої проблеми

Спочатку перевірте, як опублікований порт виглядає в IPv6:

sudo ss -tlnp | grep 8080

Починаючи з Docker Engine 27, Docker типово керує ip6tables. У Docker network з увімкненим IPv6 для опублікованого порту застосовується така сама обробка DNAT у таблицях IPv6. Тому обхід також працює, і застосовується те саме виправлення: ланцюжок DOCKER-USER також існує в ip6tables, тому продублюйте правило за допомогою sudo ip6tables -I DOCKER-USER ... і перевірте його ззовні через curl, звернувшись до публічної IPv6-адреси сервера, наприклад curl -6 http://[2001:db8:2a::1]:8080/.

У network без IPv6 клієнтів IPv6 натомість обробляє docker-proxy — звичайний процес у user space, який прослуховує [::]:8080 і переспрямовує трафік у контейнер через IPv4. Трафік до процесу на хості проходить через INPUT, тому UFW може фільтрувати цей шлях, але лише якщо UFW взагалі керує IPv6. Саме це питання та інші способи виникнення проблеми з IPv6 на VPS розглянуто в посібнику з UFW та IPv6.

Публікація на loopback усуває всю цю проблему: -p 127.0.0.1:8080:80 прив’язується лише до loopback IPv4, тому слухача IPv6 немає і ззовні немає до чого підключатися в жодному зі стеків.

Шаблон, який працює стабільно

  • Публікуйте кожен внутрішній порт на 127.0.0.1, щоб він узагалі не був доступний іззовні.
  • Передайте публічну частину одному reverse proxy, який використовує порти 80 і 443.
  • Залиште для хоста політику UFW default deny, дозволивши SSH і порти reverse proxy.
  • Фільтруйте справді публічні порти контейнерів у DOCKER-USER за початковим портом призначення та зберігайте правила в /etc/ufw/after.rules.
  • Не вимикайте інтеграцію Docker з iptables.

Після одноразового налаштування це усуває непередбачувану поведінку: ufw status описує хост, а DOCKER-USER — контейнери. Нічого не публікується випадково, і наступний docker run -p, який ви введете, відкриє доступ саме до того, що ви визначили.

FAQ

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

Тому що Docker публікує порт за допомогою правила DNAT у ланцюжку PREROUTING. Це правило змінює адресу призначення пакета на адресу контейнера ще до початку фільтрації. Потім пакет проходить шляхом FORWARD, а правила UFW розміщені в INPUT — ланцюжку, до якого пакет не потрапляє. Брандмауер не перевіряє цей пакет, тому його правила deny не впливають на опубліковані порти контейнера.

Як зробити так, щоб UFW блокував опубліковані порти Docker?

Безпосередньо UFW цього зробити не може, оскільки його правила розміщені не в тому ланцюжку. Або припиніть відкривати порт назовні, опублікувавши його як 127.0.0.1:8080:80, щоб доступ до нього мав лише хост, або фільтруйте трафік у ланцюжку DOCKER-USER за допомогою правила iptables, яке через conntrack перевіряє початковий порт призначення. Збережіть це правило в /etc/ufw/after.rules, щоб воно переживало перезавантаження та ufw reload.

Чи слід встановити "iptables": false у daemon.json Docker?

Ні. Цей параметр видаляє всі правила firewall і NAT, які створює Docker. Це ламає значно більше, ніж сам обхід UFW. Контейнери втрачають доступ до інтернету, оскільки зникає правило masquerade, а опубліковані порти припиняють працювати, оскільки зникають правила DNAT. Використовуйте публікацію через loopback і ланцюжок DOCKER-USER. Вони усувають зовнішню доступність портів без порушення мережевої роботи контейнерів.

Чи обходить Docker UFW також через IPv6?

У Docker Engine 27 і новіших версіях керування ip6tables увімкнене за замовчуванням. Тому порт, опублікований у мережі Docker з підтримкою IPv6, обходить UFW так само, як і в IPv4, і потребує аналогічного правила DOCKER-USER разом із ip6tables. У мережах без IPv6 процес docker-proxy слухає на [::], і цей трафік проходить через INPUT. UFW може його фільтрувати, якщо керує правилами IPv6. Публікація на 127.0.0.1 усуває обидва випадки, оскільки через IPv6 взагалі нічого не слухає.