Docker обходить UFW: як це працює та рішення
Docker створює правила DNAT в iptables, що ігнорують UFW. Дізнайтеся, чому порти залишаються відкритими та як це виправити за допомогою налаштувань Docker.
Чому Docker обходить UFW
Docker обходить UFW, тому що опубліковані порти контейнерів не проходять через правила брандмауера, якими керує UFW. Коли ви запускаєте docker run -p 8080:80, Docker створює правило DNAT (destination network address translation) у ланцюжку PREROUTING таблиці nat ядра. Це правило змінює адресу призначення кожного пакета на приватну адресу контейнера ще до того, як ядро визначить маршрут пакета. Потім переписаний пакет перенаправляється в контейнер через ланцюжок FORWARD, який контролюється Docker. Правила UFW знаходяться в ланцюжку INPUT, і пакет ніколи не потрапляє в нього. Тому ufw status показує заборону за замовчуванням, sudo ufw deny 8080 повідомляє про успіх, а порт 8080 залишається доступним для всього інтернету.
Це не помилка Docker, і UFW працює справно. Обидва інструменти програмують один і той самий брандмауер ядра. Правила Docker діють на ранішій стадії шляху пакета, тому запит до UFW не надходить. Цей посібник демонструє цей обхід, пояснює механізм, а потім розглядає два дієвих способи вирішення: публікацію портів на 127.0.0.1 та фільтрацію в ланцюжку DOCKER-USER. Якщо ви вперше стикаєтеся з UFW, спочатку налаштуйте його за допомогою базового посібника з UFW, оскільки брандмауер з політикою заборони за замовчуванням є необхідною основою для всього іншого на сервері.
Перевірте обхід захисту на власному сервері
Почніть з VPS, де UFW увімкнено з політикою default deny для вхідного трафіку. Запустіть веб-контейнер із опублікованим портом:
sudo ufw status verbose
docker run -d --name web -p 8080:80 nginx:1.29-alpineufw status verbose показує Default: deny (incoming), allow (outgoing), і жодного правила для порту 8080 немає. Згідно зі звітом фаєрвола, порт закритий. Тепер виконайте перевірку з іншої машини, а не з самого сервера:
curl -I http://your-vps-ip:8080/HTTP/1.1 200 OKКонтейнер відповідає. Додайте явне правило deny і протестуйте знову:
sudo ufw deny 8080/tcpПорт все ще відповідає, тому що правило deny знаходиться в ланцюжку, який пакет не відвідує. UFW не спрацював помилково. До нього просто не звернулися. Саме тому цю проблему важко виявити: ніде не виводиться помилка, розгортання працює, а вивід статусу фаєрвола виглядає як стан справного захищеного сервера.
Механізм: PREROUTING виконується перед INPUT
Ядро обробляє вхідний пакет у строго визначеному порядку, і проблема полягає саме в цьому порядку.
- Спочатку виконується
PREROUTING. Правила в цій ланці можуть змінити призначення пакета; саме це робить правило Docker для опублікованого порту. - Наступним кроком йде прийняття рішення про маршрутизацію. Пакет, адресований самому хосту, потрапляє в ланцюжок
INPUT. Пакет, адресований будь-якій іншій машині, потрапляє в ланцюжокFORWARD. - Правила UFW знаходяться в
INPUT. Правила Docker знаходяться вFORWARD.
Розглянемо правило Docker для щойно запущеного контейнера:
sudo iptables -t nat -L DOCKER -nChain 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-мережі Docker. Після переписування пакет більше не адресований хосту, тому рішення про маршрутизацію спрямовує його шляхом FORWARD, де Docker вже додав правила для прийняття трафіку у свої мережі. Ваше правило deny 8080/tcp чекає в INPUT на пакет, який ніколи не надійде.
В Ubuntu 24.04 команда iptables є надбудовою над nftables, але порядок ланцюжків та результат ідентичні. UFW та Docker обидва записують дані в один і той самий конвеєр пакетів ядра, але точка входу Docker знаходиться раніше.
Повсякденне рішення: публікація портів на 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"Це працює, тому що правило Docker DNAT тепер відповідає лише пакетам, адресованим на 127.0.0.1. Пакет з інтернету не може мати таку цільову адресу, тому ядро видаляє його до виконання будь-яких правил firewall. Порт доступний з хоста і більше нізвідки. Перевірте прив'язку:
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 і здійснює маршрутизацію за hostname. Не публікуйте нічого іншого. Саме цей підхід описаний у посібнику з reverse proxy Traefik, і саме так самокеровані додатки, як Nextcloud на VPS, залишаються недоступними поза межами проксі. Як оголошуються записи ports: та решта робочого процесу Compose, описано в посібнику з основ Docker Compose.
Коли кожен внутрішній контейнер працює на loopback, UFW повертається до своєї основної функції: захисту портів, які обслуговує сам хост. Сформуйте набір правил тут, а потім виконайте команди по черзі:
Real filtering: the DOCKER-USER chain
Іноді порт контейнера має залишатися відкритим у мережі, але бути обмеженим. Наприклад, порт репліки бази даних, до якого може мати доступ лише одна офісна адреса. Для цього Docker використовує ланцюжок DOCKER-USER. Кожен пакет, що спрямовується до будь-якого контейнера, проходить через DOCKER-USER перед власними правилами accept від Docker. Docker ніколи не записує власні правила в цей ланцюжок. Цей ланцюжок призначений для користувача, і Docker не змінює його вміст під час перезапуску демона.
Важливий нюанс перед виконанням команди: до того, як пакет потрапить у 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 з дозволеної адреси пройде успішно, а з будь-якої іншої — з'єднання перерветься за таймаутом.
Правила, додані за допомогою команди iptables, зникають після перезавантаження. Оскільки UFW вже керує цим фаєрволом, найкраще місце для їх збереження — /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 застосовує цей файл під час кожного перезавантаження та кожного запуску системи. Тепер фільтрація контейнерів зберігається в тому ж місці, що й решта налаштувань фаєрвола, і вона не зникає після перезавантаження або оновлення Docker.
Чому не варто вимикати інтеграцію Docker з iptables
Старі відповіді на цю проблему пропонують встановити { "iptables": false } у /etc/docker/daemon.json. Не робіть цього. Правила firewall від Docker виконують набагато більше функцій, ніж просто публікація портів. Правило masquerade забезпечує контейнерам вихідний доступ до інтернету через адресу хоста. Якщо вимкнути інтеграцію, контейнери не зможуть завантажувати образи, звертатися до дзеркал пакетів або викликати будь-який зовнішній API (application programming interface). Правила DNAT забезпечують роботу -p, тому без них опубліковані порти повністю перестануть працювати. Правила ізоляції, які розділяють мережі Compose, також зникнуть. Ви намагатиметеся виправити обхід захисту, але при цьому зруйнуєте мережеву взаємодію контейнерів. Вам доведеться вручну писати та підтримувати кожне з цих правил. У документації Docker цей параметр позначено як опцію для користувачів, які мають намір робити саме це. Ланцюжок DOCKER-USER створений саме для того, щоб цей перемикач не був потрібен.
IPv6 сторона тієї ж проблеми
Спочатку перевірте стан опублікованого порту в IPv6:
sudo ss -tlnp | grep 8080Починаючи з Docker Engine 27, Docker керує ip6tables за замовчуванням. У мережі Docker з увімкненим IPv6 опублікований порт отримує таку саму обробку DNAT у таблицях IPv6. Це створює таку саму уязвивість, тому застосовується те саме рішення: ланцюжок DOCKER-USER також існує в ip6tables. Віддзеркальте ваше правило за допомогою sudo ip6tables -I DOCKER-USER ... і протестуйте ззовні за допомогою curl на публічну IPv6-адресу сервера, наприклад curl -6 http://[2001:db8:2a::1]:8080/.
У мережі без IPv6 IPv6-клієнти обробляються через docker-proxy — звичайний процес у просторі користувача, який прослуховує [::]:8080 і перенаправляє трафік у контейнер через IPv4. Трафік до процесу хоста проходить через INPUT, тому UFW може фільтрувати цей шлях, але тільки якщо UFW взагалі керує IPv6. Чи керує він ним, а також інші способи виникнення прогалин у захисті IPv6 на VPS, описано в посібнику з UFW та IPv6.
Публікація на loopback вирішує це питання: -p 127.0.0.1:8080:80 прив'язується лише до IPv4 loopback, тому IPv6-слухач відсутній і до нього неможливо звернутися ззовні через будь-який стек.
Патерн, що забезпечує стабільність
- Публікуйте кожен внутрішній порт на
127.0.0.1, щоб він взагалі не був доступний зовні. - Надайте публічний доступ лише одному reverse proxy, який займає порти 80 та 443.
- Налаштуйте UFW у режимі default deny для хоста, дозволивши лише SSH та порти проксі.
- Фільтруйте порти контейнерів, що мають бути публічними, у
DOCKER-USERза оригінальним портом призначення, збереженим у/etc/ufw/after.rules. - Залиште інтеграцію Docker з iptables увімкненою.
Після одноразового налаштування це усуне непередбачуваність: ufw status описує хост, а DOCKER-USER описує контейнери. Жоден порт не буде опублікований випадково, і наступна команда docker run -p, яку ви введете, відкриє саме те, що ви планували.
FAQ
Чому я можу отримати доступ до Docker-контейнера, якщо UFW блокує порт?
Це стається тому, що Docker публікує порт за допомогою правила DNAT у ланцюжку PREROUTING. Це правило змінює адресу призначення пакета на адресу контейнера ще до початку фільтрації. Потім пакет проходить через шлях FORWARD, тоді як правила UFW знаходяться в ланцюжку INPUT, у який пакет не потрапляє. Брандмауер не перевіряє цей трафік, тому його правила блокування не діють на опубліковані порти контейнерів.
Як змусити UFW блокувати опубліковані порти Docker?
UFW не може цього зробити самостійно, оскільки його правила знаходяться в іншому ланцюжку. Ви можете або припинити експонування порту, опублікувавши його як 127.0.0.1:8080:80 (тільки для хоста), або налаштувати фільтрацію в ланцюжку DOCKER-USER за допомогою правила iptables, яке використовує conntrack для визначення оригінального порту призначення. Збережіть це правило в /etc/ufw/after.rules, щоб воно зберігалося після перезавантаження та ufw reload.
Чи варто встановлювати "iptables": false у Docker's daemon.json?
Ні. Це налаштування видаляє всі правила брандмауера та NAT від Docker. Це призводить до значно більших проблем, ніж обхід захисту. Контейнери втрачають доступ до інтернету, оскільки правило masquerade видаляється, а опубліковані порти перестають працювати через відсутність правил DNAT. Замість цього використовуйте публікацію через loopback та ланцюжок DOCKER-USER; це усуває вразливість, не порушуючи роботу мережі контейнерів.
Чи обходить Docker UFW також у IPv6?
У Docker Engine 27 та новіших версіях керування ip6tables увімкнено за замовчуванням. Тому порт, опублікований у мережі Docker з підтримкою IPv6, перенаправляється в обхід UFW так само, як і в IPv4. Для нього потрібно дзеркально налаштувати правило DOCKER-USER за допомогою ip6tables. У мережах без IPv6 процес docker-proxy прослуховує [::], і цей трафік проходить через INPUT, де UFW може його фільтрувати, якщо UFW керує IPv6. Публікація на 127.0.0.1 дозволяє уникнути обох випадків, оскільки в IPv6 нічого не прослуховується.