Если вы ставите Docker на сервер с включённым UFW, почти неизбежно ловите сюрприз: вы «закрыли» порт в UFW, но сервис в контейнере всё равно доступен снаружи. Причина не в «дырявом UFW», а в том, как Docker программирует NAT и фильтрацию в netfilter (iptables/nftables) и куда именно попадает трафик для published ports.
Ниже разберём связку UFW+Docker в реалиях 2026 года: почему правила в INPUT не всегда влияют на опубликованные порты, что делает FORWARD, зачем существует DOCKER-USER, и как получить предсказуемый «secure docker host» без внезапных открытий после деплоя.
Почему UFW «не видит» published ports Docker
Когда вы запускаете контейнер с публикацией порта, например:
docker run -d --name app -p 8080:80 nginx
Docker делает две ключевые вещи:
- добавляет DNAT: входящие подключения на порт 8080 хоста перенаправляет на IP контейнера в docker-сети;
- добавляет правила фильтрации в свои цепочки (семейство
DOCKER/DOCKER-USER), чтобы форвардинг до контейнера работал «из коробки».
Ключевой момент: соединение на опубликованный порт часто перестаёт быть «входом на локальный сокет хоста». После DNAT пакет начинает рассматриваться как трафик, который нужно маршрутизировать к контейнеру, а значит решающие правила оказываются в FORWARD, а не в INPUT. В результате ufw deny 8080/tcp может не дать ожидаемого эффекта, потому что вы запретили не тот путь пакета.
Практическое правило: если порт опубликован через
-p, думайте не только проINPUT, но и проFORWARDи docker-цепочки.
Как выглядит путь пакета (упрощённо)
- пакет приходит на внешний интерфейс (например,
eth0); - в таблице NAT срабатывает PREROUTING и DNAT на IP контейнера;
- дальше пакет проходит фильтрацию для маршрутизируемого трафика (обычно
FORWARD), где Docker заранее создал «разрешающие» правила под публикацию.
Именно из-за этого связка «UFW закрывает порт» и «Docker публикует порт» без доп. мер часто ведёт к открытому порту наружу.
iptables-nft и nftables в 2026: почему становится ещё легче запутаться
На современных дистрибутивах iptables-команды обычно работают через слой совместимости iptables-nft, который программирует правила в nftables. При этом и UFW, и Docker могут продолжать «говорить iptables-командами», но фактическое состояние окажется в nftables-таблицах.
Что важно для практики:
- UFW остаётся «оркестратором», но его модель цепочек/приоритетов не знает про Docker как «владельца» части правил;
- Docker по умолчанию активно управляет NAT и фильтрацией (если не отключено управление iptables в демоне);
- критично не «чем вы смотрите» (iptables/nft), а где стоит точка контроля и в каком порядке применяются правила.
Если хочется углубиться именно в механику и порядок цепочек, держите отдельный разбор: как Docker встраивается в iptables/nftables и где реально фильтровать трафик.

Главный рычаг: цепочка DOCKER-USER (точка контроля до правил Docker)
Docker предусмотрел «крючок» для администраторов — цепочку DOCKER-USER. Она вызывается до правил, которые Docker генерирует под published ports. Это удобное место, чтобы реализовать политику «запрещено по умолчанию, разрешаем точечно» для доступа к контейнерам.
Идея для задачи block docker ports такая:
- порты публикуете как обычно (или осознанно ограничиваете адресом);
- в
DOCKER-USERразрешаете только нужные источники/подсети/порты; - в конце ставите запрет, чтобы всё остальное не проходило в контейнеры снаружи.
Быстрая диагностика: что реально опубликовано и кто слушает
Два быстрых вопроса: «что Docker выставил наружу» и «есть ли слушающий сокет на хосте».
docker ps --format 'table {{.Names}} {{.Ports}}'
ss -lntp | head -n 50
Если видите публикацию вроде 0.0.0.0:8080->80/tcp, это точно внешний published port. И дальше ваша точка контроля почти всегда должна учитывать FORWARD и DOCKER-USER.
Forward policy и UFW: почему иногда всплывает ufw route
Docker-сценарий — это маршрутизация между внешним интерфейсом хоста и виртуальными сетями контейнеров, поэтому FORWARD становится центральным. UFW исторически сфокусирован на простом периметре (INPUT/OUTPUT) и может быть консервативным к routed-трафику.
На Docker-хосте обычно одновременно важны два слоя:
- разрешён ли IP forwarding в ядре (Docker обычно включает сам, иначе NAT контейнеров ломается);
- какая политика и правила стоят на
FORWARD(и тут появляется конфликт ожиданий между UFW и Docker).
Команда ufw route управляет именно routed-трафиком (правилами уровня FORWARD). Это может быть полезно, если вы хотите часть политики выражать «в терминах UFW», но в Docker-мире порядок правил становится критичен, а Docker всё равно добавит своё.
Если нужна предсказуемость, либо контролируйте доступ к контейнерам через
DOCKER-USER, либо публикуйте порты только на localhost и выпускайте наружу через один фронтенд.
Две рабочие стратегии: localhost-публикация и фильтрация DOCKER-USER
Стратегия A: публикуем только на localhost
Самый простой и часто самый надёжный способ уменьшить площадь атаки: публиковать сервисы контейнеров только на 127.0.0.1, а наружу отдавать через один контролируемый фронтенд (reverse proxy на хосте или выделенный edge-контейнер).
Пример:
docker run -d --name app -p 127.0.0.1:8080:80 nginx
Из интернета соединение на 8080 не примется, потому что публикация привязана к loopback. UFW при этом может оставаться простым: открыть только SSH и порты фронтенда (80/443), а «внутренности» не торчат наружу.
Минусы: не всегда применимо (например, нужен прямой TCP-доступ извне к разным сервисам без прокси), и требуется аккуратная архитектура.
Стратегия B: режем внешний доступ к контейнерам в DOCKER-USER
Если вам действительно нужно публиковать порты на внешние интерфейсы (например, база доступна только из офисной подсети), используйте DOCKER-USER как явную точку контроля.
Типовой шаблон логики:
- разрешить
RELATED,ESTABLISHED, чтобы не ломать существующие соединения; - разрешить конкретные источники к конкретным published ports;
- запретить остальное, что идёт «снаружи в контейнеры».
Пример для iptables-интерфейса (правила добавляйте аккуратно и проверяйте на тестовом окне; все команды однострочные):
iptables -C DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || iptables -I DOCKER-USER 1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
iptables -C DOCKER-USER -i eth0 -p tcp -s 203.0.113.10 --dport 5432 -j ACCEPT 2>/dev/null || iptables -I DOCKER-USER 2 -i eth0 -p tcp -s 203.0.113.10 --dport 5432 -j ACCEPT
iptables -C DOCKER-USER -i eth0 -j DROP 2>/dev/null || iptables -A DOCKER-USER -i eth0 -j DROP
Смысл: разрешили доступ к опубликованному порту 5432 только с одного IP, а всё остальное, что приходит с внешнего интерфейса и пытается уйти в контейнеры, отрезали.
Важно: не «глушите» весь FORWARD без условий. Иначе легко сломать исходящий трафик контейнеров и межконтейнерную связность. Фильтруйте именно вход в контейнеры (как минимум по входному интерфейсу -i и по порту --dport).
Как совместить UFW и Docker без сюрпризов: практические правила
1) Зафиксируйте модель экспозиции портов
- Модель edge proxy: наружу открыты только 80/443 (и, возможно, 22), контейнеры слушают localhost или вообще без публикации, доступ внутри docker-сетей.
- Модель «порты наружу»: контейнеры публикуют на
0.0.0.0. Тогда обязателен контроль черезDOCKER-USERили отдельную явную фильтрацию routed-трафика.
2) Разделите зоны ответственности
Рабочая схема для продакшна:
- UFW: периметр хоста (SSH, веб-фронтенд, мониторинг).
- Docker: NAT и маршрутизация контейнеров.
DOCKER-USER: политика доступа к published ports контейнеров.
3) Отлавливайте случайные публикации из Compose
Частая причина «внезапно открытого» сервиса — кто-то добавил ports: в docker-compose.yml. Для продакшна полезно договориться:
- по умолчанию публикуем только
127.0.0.1:HOSTPORT:CONTAINERPORT; - или используем
exposeи отдаём наружу через прокси; - или каждое внешнее открытие подтверждаем правилами в
DOCKER-USER(и документируем).
Типовые ошибки и быстрые проверки
Ошибка: «Сделал ufw deny, но порт открыт»
Для published ports это ожидаемо: трафик может проходить через FORWARD и docker-цепочки, а не через INPUT. Проверяйте реальный путь пакета и точку контроля.
Ошибка: «Запретил FORWARD целиком — и контейнеры потеряли интернет»
Исходящий трафик контейнеров тоже идёт через форвардинг на хосте. Если цель — «закрыть вход извне», фильтруйте по входному интерфейсу, источникам и портам, и оставляйте RELATED,ESTABLISHED.
Ошибка: «nftables показывает одно, iptables — другое»
Убедитесь, что вы не поддерживаете параллельно два набора правил и что понимаете, какой интерфейс (iptables vs nft) фактически управляет netfilter на вашей системе. Самый надёжный подход — держать одну понятную точку контроля (обычно DOCKER-USER) и регулярно проверять фактическую экспозицию портов.

Мини-чеклист для secure Docker host
- Явно решите, какие сервисы должны быть доступны извне, и исключите случайную публикацию портов.
- По умолчанию публикуйте порты на
127.0.0.1, если нет строгой необходимости слушать внешний интерфейс. - Если published ports должны быть внешними, внедрите политику доступа в
DOCKER-USER(разрешаем нужное, остальное режем). - Периодически сверяйте фактическую экспозицию:
docker psиss -lntp. - Документируйте, какие порты опубликованы, откуда разрешён доступ и где это выражено (UFW или
DOCKER-USER).
Итоги
Конфликт UFW и Docker возникает не потому, что UFW «плохой», а потому что Docker активно использует NAT и форвардинг, меняя путь пакета: опубликованные порты часто решаются в FORWARD и docker-цепочках, а не в INPUT. В 2026 году, когда повсеместны iptables-nft и nftables, особенно важно держать одну предсказуемую точку контроля и регулярно проверять фактическую экспозицию.
Для большинства серверов самый практичный подход — публиковать сервисы контейнеров только локально и отдавать наружу через контролируемый фронтенд. Если же нужны внешние published ports, используйте DOCKER-USER как место, где вы гарантированно можете закрыть доступ «по умолчанию» и разрешить ровно то, что нужно.


