SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-25

Почему Docker игнорирует правила UFW и как это исправить

Docker добавляет правила в iptables напрямую, обходя UFW. В результате порты остаются открытыми для всего интернета. Узнайте, как настроить фильтрацию через DOCKER-USER.

Почему Docker обходит UFW

Docker обходит UFW, так как опубликованные порты контейнеров не проходят через правила межсетевого экрана, которыми управляет UFW. Когда вы запускаете docker run -p 8080:80, Docker записывает правило DNAT (destination network address translation) в цепочку PREROUTING таблицы nat ядра. Это правило перезаписывает адрес назначения каждого пакета на частный адрес контейнера до того, как ядро решит, куда направляется пакет. Затем переписанный пакет пересылается в контейнер через цепочку FORWARD, которую контролирует Docker. Правила UFW находятся в цепочке INPUT, и пакет никогда в неё не попадает. Поэтому ufw status показывает политику по умолчанию deny, 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-alpine

ufw status verbose показывает Default: deny (incoming), allow (outgoing) и отсутствие правила для порта 8080. Согласно отчету самого межсетевого экрана, порт закрыт. Теперь выполните проверку с другого компьютера, а не с самого сервера:

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

Контейнер отвечает. Добавьте явное правило запрета и протестируйте снова:

sudo ufw deny 8080/tcp

Порт по-прежнему отвечает, так как правило запрета находится в цепочке, до которой пакет не доходит. UFW не дал сбой. К нему просто не обращались. Именно поэтому проблему так трудно обнаружить: нигде не выводится ошибок, развертывание проходит успешно, а статус межсетевого экрана выглядит в точности как у исправно защищенного сервера.

Механизм: 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 — адрес контейнера во внутренней мостовой сети 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

Большинству контейнеров изначально не требуется быть публично доступными. База данных, сервер приложений за обратным прокси, панель администратора, эндпоинт метрик — ничто из этого не должно отвечать напрямую из Интернета. Публикуйте их на адресе 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, а пакет из Интернета не может легитимно иметь такой адрес назначения, поэтому ядро отбрасывает его до срабатывания любых правил межсетевого экрана. Порт доступен с хоста, и больше ниоткуда. Проверьте привязку:

sudo ss -tlnp | grep 8080

В выводе вы должны увидеть 127.0.0.1:8080, а не 0.0.0.0:8080 или [::]:8080. Затем убедитесь с другого компьютера, что соединение на curl http://your-vps-ip:8080/ отклоняется.

Для сервисов, которые должны быть доступны из Интернета, запустите один обратный прокси, который занимает порты 80 и 443 и выполняет маршрутизацию по имени хоста; больше ничего публиковать не нужно. Это шаблон, на котором строится руководство по обратному прокси Traefik, и именно так self-hosted приложение, например Nextcloud на VPS, остаётся недоступным иначе, как через свой прокси. О том, как объявляются записи ports: и об остальном рабочем процессе Compose, рассказано в базовом руководстве по Docker Compose.

Когда все внутренние контейнеры работают через loopback, UFW возвращается к выполнению своей обычной задачи: защите портов, которые обслуживает сам хост. Создайте этот набор правил, а затем выполните команды по порядку:

ToolUFW rule generator

Реальная фильтрация: цепочка DOCKER-USER

Иногда порт контейнера должен оставаться опубликованным в сети, но с ограничениями доступа — например, порт реплики базы данных, к которому может обращаться только один IP-адрес офиса. Для этого 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 с разрешенного адреса проходит успешно, а с любого другого — соединение зависает по таймауту. Это зависание — признак того, что правило DROP работает, в отличие от порта, за которым ничего нет, а разница между отклоненным соединением и соединением по таймауту — самый быстрый способ отличить отфильтрованный порт от сервиса, который просто не слушает запросы.

Правила, добавленные командой 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. Не делайте этого. Правила межсетевого экрана Docker выполняют гораздо больше функций, чем просто публикация портов. Правило masquerade обеспечивает контейнерам исходящий доступ в Интернет через адрес хоста, поэтому при отключении интеграции контейнеры не смогут скачивать образы, обращаться к зеркалам репозиториев или вызывать любые внешние API (интерфейсы прикладного программирования). Правила 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 политику по умолчанию 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 в файле daemon.json для Docker?

Нет. Этот параметр удаляет все правила межсетевого экрана и 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 полностью отключено.