Добавить в корзинуПозвонить
Найти в Дзене

Docker и UFW: почему контейнеры могут обходить брандмауэр и как это исправить

Когда на сервере появляется Docker, привычная логика защиты часто перестаёт работать так, как ожидает администратор. На экране UFW может быть написано «deny», а порт контейнера при этом всё равно остаётся доступным извне. Причина не в «поломке» UFW, а в том, как Docker встраивается в сетевой стек Linux и использует Netfilter для NAT и маршрутизации трафика. Суть проблемы в том, что UFW по умолчанию ориентируется на цепочку INPUT, то есть на трафик, который идёт к локальным процессам самого хоста. Docker же при публикации порта, например через -p 8080:80, сначала меняет адрес назначения через DNAT, а затем отправляет пакет уже не в INPUT, а в FORWARD. В итоге UFW может честно показать запрет, но пакет до контейнера всё равно дойдёт. Это особенно неприятно в продакшене: администратор уверен, что порт закрыт, а на деле сервис торчит в интернет. Именно поэтому безопасность контейнеров нельзя строить только на привычных «allow/deny» правилах для хоста. Самый наглядный тест — запустить конте
Оглавление

Когда на сервере появляется Docker, привычная логика защиты часто перестаёт работать так, как ожидает администратор. На экране UFW может быть написано «deny», а порт контейнера при этом всё равно остаётся доступным извне. Причина не в «поломке» UFW, а в том, как Docker встраивается в сетевой стек Linux и использует Netfilter для NAT и маршрутизации трафика.

Docker переводит трафик в другую ветку обработки, и стандартное правило UFW в INPUT может просто не сработать
Docker переводит трафик в другую ветку обработки, и стандартное правило UFW в INPUT может просто не сработать

Почему стандартный ufw deny не спасает

Суть проблемы в том, что UFW по умолчанию ориентируется на цепочку INPUT, то есть на трафик, который идёт к локальным процессам самого хоста. Docker же при публикации порта, например через -p 8080:80, сначала меняет адрес назначения через DNAT, а затем отправляет пакет уже не в INPUT, а в FORWARD. В итоге UFW может честно показать запрет, но пакет до контейнера всё равно дойдёт.

Это особенно неприятно в продакшене: администратор уверен, что порт закрыт, а на деле сервис торчит в интернет. Именно поэтому безопасность контейнеров нельзя строить только на привычных «allow/deny» правилах для хоста.

Как проверить проблему на практике

Самый наглядный тест — запустить контейнер с веб-сервером и пробросить порт на хост. Затем добавить запрет в UFW и проверить доступ с другой машины. Во многих типовых конфигурациях ответ от контейнера всё равно будет приходить, потому что трафик проходит через цепочку FORWARD, а не INPUT. В статье также показано, что при анализе правил iptables можно увидеть автоматически добавленные Docker-правила в цепочке DOCKER.

docker run -d --name test-web -p 8080:80 nginx:alpine
sudo ufw deny 8080/tcp
sudo ufw status
curl http://<IP_сервера>:8080
sudo iptables -L DOCKER -n -v
Видимый запрет не всегда означает фактическую блокировку трафика
Видимый запрет не всегда означает фактическую блокировку трафика

Рабочее решение: цепочка DOCKER-USER

Наиболее практичный и при этом официальный путь — использовать цепочку DOCKER-USER. Docker создаёт её специально для того, чтобы администратор мог вставить собственную фильтрацию до автоматических правил Docker. Предлагается добавить правила в /etc/ufw/after.rules, а затем перезагрузить UFW. После этого трафик к опубликованным портам контейнеров будет попадать под контроль администратора, а не обходить брандмауэр по умолчанию.

sudo ufw reload
Контроль над транзитным трафиком возвращается администратору
Контроль над транзитным трафиком возвращается администратору

Как теперь открывать доступ к контейнерам

После внедрения такого подхода обычная команда ufw allow <порт> уже не решает задачу для Docker-контейнеров, потому что она продолжает работать с INPUT. Для контейнерного трафика нужно использовать ufw route, то есть явно разрешать маршрутизацию через хост к внутреннему интерфейсу Docker. Важная деталь: если порт опубликован как -p 8080:80, то в правиле обычно указывают именно внутренний порт контейнера, а не внешний порт хоста.

sudo ufw route allow proto tcp from any to any port 80
sudo ufw route allow proto tcp from any to 172.17.0.2 port 80
sudo ufw route allow proto tcp from 203.0.113.50 to any port 3306
sudo ufw route delete allow proto tcp from any to any port 80

Такой подход удобен, когда нужно открыть доступ только к нужному сервису или даже к конкретному контейнеру, а всё остальное оставить закрытым.

Автоматизация через ufw-docker

Если не хочется вручную редактировать правила, имеется инструмент ufw-docker от Chaifeng. Он ставит нужные цепочки и правила автоматически, а затем позволяет управлять доступом через более удобный интерфейс. Это снижает риск ошибки и избавляет от постоянной ручной работы с динамическими IP-адресами контейнеров.

sudo wget -O /usr/local/bin/ufw-docker https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
sudo chmod +x /usr/local/bin/ufw-docker
sudo ufw-docker install
sudo systemctl restart ufw

Более безопасные архитектурные приёмы

Можно не публиковать чувствительные сервисы наружу вообще. Например, базу данных можно привязать только к 127.0.0.1, чтобы порт был доступен лишь локально на хосте. Тогда даже при ошибках в правилах UFW сервис не окажется открыт всему интернету.

docker run -d -p 127.0.0.1:5432:5432 postgres:16-alpine

Для Docker Compose это выглядит так:

services:
db:
image: postgres:16
ports:
- "127.0.0.1:5432:5432"

Ещё один сильный паттерн — обратный прокси. Внешние порты 80 и 443 публикует только один контейнер, например Nginx, Traefik или Caddy, а все внутренние приложения живут в закрытых docker-сетях и наружу не торчат вовсе. Такой подход упрощает управление TLS, лимитами запросов и дополнительной аутентификацией.

Единая точка входа проще в защите и обслуживании
Единая точка входа проще в защите и обслуживании

Почему не стоит просто отключать iptables

Иногда предлагают радикальный вариант — запретить Docker самому менять iptables через "iptables": false в daemon.json. На бумаге это выглядит как способ убрать конфликт с UFW, но на практике ломает сетевую магию Docker: исчезает маскарадинг, перестаёт работать публикация портов, усложняется изоляция сетей и вся маршрутизация NAT начинает требовать ручной настройки. Для обычного продакшена это слишком дорогой компромисс.

Что меняется с nftables и Docker 29+

В современных дистрибутивах Linux всё активнее используется nftables, а Docker 29 и новее уже поддерживает его как backend, хотя этот режим пока еще существует как экспериментальный. В таком сценарии привычной цепочки DOCKER-USER может уже не быть в прежнем виде, и фильтрацию приходится строить через отдельные таблицы nftables с нужным приоритетом. При этом многие системы всё ещё работают через слой совместимости iptables-nft, так что старые подходы могут продолжать действовать, но отладка становится сложнее.

Переход на nftables не отменяет безопасность, но меняет инструменты контроля
Переход на nftables не отменяет безопасность, но меняет инструменты контроля

Безопасность контейнеров — это не один приём, а система

Закрыть порты — только часть защиты. Контейнерам нужно ограничивать память и CPU, использовать --read-only, убирать лишние capabilities, запускать процессы не от root, брать минимальные базовые образы и сканировать их на CVE в CI/CD. Отдельно полезен регулярный аудит: Docker Bench for Security, внешнее сканирование через Nmap и инструменты вроде CrowdSec или Fail2ban помогают не пропустить ошибки конфигурации и подозрительную активность.

Итог

Если Docker используется на сервере с UFW, нельзя полагаться только на ufw deny. Нужно понимать, что контейнерный трафик часто проходит мимо стандартных правил хоста. Самый надёжный путь — перевести контроль в DOCKER-USER, использовать ufw route для точечного разрешения, закрывать внутренние сервисы на 127.0.0.1, а публичные приложения выводить наружу через reverse proxy. Именно такой подход даёт предсказуемую и управляемую безопасность в контейнерной инфраструктуре.

Если вам понравился материал, не забудьте поставить палец вверх 👍 и поделиться статьёй с друзьями. Подписывайтесь на мой Telegram-канал, чтобы первыми узнавать о новых статьях и полезных материалах. А также загляните на сайт RoadIT.ru, где я собираю заметки о командах Linux, HowTo-гайды и много другой интересной информации. Спасибо за внимание!