Администратор включает UFW, запрещает все входящие соединения, а порт контейнера всё равно открыт из интернета. Это не ошибка конфигурации UFW: Docker управляет iptables сам, и трафик к опубликованным портам не доходит до правил, которые пишет UFW. Ниже — как увидеть проблему и как закрыть порты правильно.
Почему UFW не защищает опубликованный порт
UFW добавляет правила в цепочку INPUT: она обрабатывает пакеты, адресованные самому серверу. Когда вы публикуете порт ключом -p, Docker создаёт правило DNAT в таблице nat и перенаправляет пакеты контейнеру. Такой трафик идёт не на сам сервер, а транзитом через цепочку FORWARD, где первыми стоят правила Docker. Правила UFW из INPUT к нему не применяются.
| Цепочка | Кто пишет | Что обрабатывает |
|---|---|---|
| INPUT | UFW | Трафик к самому серверу |
| DOCKER-USER | Администратор | Транзитный трафик к контейнерам, проверяется первым |
| DOCKER | Docker | Разрешения для опубликованных портов |
Как проверить, что порт виден снаружи
Запустите тестовый контейнер с опубликованным портом и посмотрите на столбец PORTS: запись 0.0.0.0:8080 означает, что порт слушает на всех интерфейсах. Затем проверьте доступность с другого компьютера. Адрес в примере взят из диапазона для документации, подставьте адрес своего сервера.
docker run -d --name web -p 8080:80 nginx
docker ps
ufw status
nc -zv 203.0.113.10 8080
Если соединение прошло, а ufw status показывает запрет, вы столкнулись именно с этим поведением. Посмотреть правила Docker можно так:
iptables -L DOCKER-USER -n -v --line-numbers
iptables -t nat -L DOCKER -n
iptables -L FORWARD -n -v
ss -tlnp
Способ первый: публикация только на 127.0.0.1
Самый простой и надёжный путь — не открывать порт во внешнюю сеть, если он нужен только локально, например обратному прокси-серверу на этом же хосте. Укажите адрес привязки перед портом. Тогда внешний доступ к порту невозможен, а Nginx на хосте по-прежнему обращается к контейнеру через 127.0.0.1.
docker run -d --name web -p 127.0.0.1:8080:80 nginx
services:
web:
image: nginx
ports:
- "127.0.0.1:8080:80"
Адрес привязки по умолчанию можно задать и для всех контейнеров параметром ip в файле /etc/docker/daemon.json, после чего потребуется перезапуск Docker. Параметр iptables со значением false отключает управление правилами, но тогда ломается выход контейнеров в сеть, поэтому как постоянное решение его не рекомендуют.
Способ второй: правила в DOCKER-USER
Если порт должен быть доступен только с определённых адресов, добавляйте правила в цепочку DOCKER-USER: Docker не затирает её и проверяет раньше собственных правил. К моменту проверки пакет уже прошёл DNAT, поэтому порт назначения изменился. Чтобы отфильтровать по исходному порту, используйте модуль conntrack с параметром --ctorigdstport.
iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL -j DROP
iptables -I DOCKER-USER -i eth0 -s 198.51.100.7 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL -j ACCEPT
iptables -L DOCKER-USER -n -v --line-numbers
Правило ACCEPT вставлено позже DROP, поэтому оказалось выше и сработает первым. Эти правила не переживают перезагрузку сами по себе: сохраните их, например пакетом iptables-persistent, и проверьте результат после перезапуска Docker и сервера. Подробнее о цепочках — в статье iptables: настройка файрвола Linux с нуля.
Способ третий: изолированные сети Docker
Контейнерам, которым не нужен внешний доступ, порты публиковать не требуется. Базы данных и кэши достаточно подключить к общей пользовательской сети: контейнеры обращаются друг к другу по имени, а снаружи они не видны. Сеть с ключом --internal дополнительно лишена выхода в интернет.
docker network create --internal backend
docker run -d --name db --network backend mariadb
В Docker Compose то же делается блоком networks и параметром internal: true, подробности — в статье Docker Compose: запуск стека приложений. Базовая настройка файрвола описана в материале UFW Firewall на VPS, а установка Docker — в статье Docker на VPS: установка и запуск контейнеров.
Частые вопросы
Почему Docker открывает порты в обход UFW?
Docker сам добавляет правила в iptables: DNAT в таблице nat и разрешения в цепочке FORWARD. Трафик к контейнеру идёт транзитом, а не в цепочку INPUT, где работает UFW. Поэтому запрет в UFW для опубликованного порта не срабатывает, и нужны правила DOCKER-USER или привязка к 127.0.0.1.
Достаточно ли публикации на 127.0.0.1?
Если приложение нужно только процессам на этом же сервере, например Nginx в роли обратного прокси, привязки к 127.0.0.1 достаточно: порт не слушает внешние интерфейсы. Если доступ нужен избранным внешним адресам, используйте правила DOCKER-USER или закрытую сеть, например VPN.
Нужно ли публиковать порты для связи контейнеров между собой?
Нет. Контейнеры в одной пользовательской сети Docker обращаются друг к другу по имени службы и внутреннему порту, публикация не требуется. Порт публикуют только для доступа извне. Базы данных и очереди лучше держать в сети без публикации, тогда они недоступны из интернета.
Сохранятся ли правила DOCKER-USER после перезагрузки?
Сами по себе нет: правила iptables хранятся в памяти ядра. Сохраните их штатным способом дистрибутива, например пакетом iptables-persistent, и после перезагрузки проверьте командой iptables -L DOCKER-USER, что правила на месте и порядок не нарушен запуском Docker.