Проблема: порт открыт всему интернету, хотя UFW «закрыт»
docker run -p 8080:8080 или ports: ["8080:8080"] в compose открывает порт всему интернету, даже если в UFW нет разрешающего правила. Так служебные сервисы — экспортёры метрик, админки, внутренние API — регулярно торчат наружу, хотя админ уверен, что firewall их закрывает.
Почему UFW не срабатывает: Docker создаёт правила в цепочке DOCKER (NAT + filter), которая обрабатывается до пользовательских цепочек UFW. Пакет попадает в контейнер раньше, чем доходит до правил ufw-before-input:
PREROUTING → DOCKER (filter) → ACCEPT → контейнер
↑ UFW сюда вообще не попадает
Проверить снаружи (с другого хоста): timeout 3 bash -c "</dev/tcp/<адрес>/8080" && echo OPEN.
Фикс: цепочка DOCKER-USER
Docker оставляет пустую цепочку DOCKER-USER, которая обрабатывается до его собственных авто-правил. Туда добавляем allow для нужных источников и DROP остального:
iptables-save > /root/iptables-before.txt # снапшот
iptables -I DOCKER-USER 1 -s <доверенный-IP> -p tcp -m multiport --dports 8080,9090 -j ACCEPT
iptables -I DOCKER-USER 2 -s 127.0.0.1 -p tcp -m multiport --dports 8080,9090 -j ACCEPT
iptables -I DOCKER-USER 3 -s 172.16.0.0/12 -p tcp -m multiport --dports 8080,9090 -j ACCEPT
iptables -A DOCKER-USER -p tcp -m multiport --dports 8080,9090 -j DROP
apt-get install -y iptables-persistent
iptables-save > /etc/iptables/rules.v4 # пережить reboot
Проверка: снаружи порт таймаутит (DROP), изнутри docker-сети работает. Счётчики iptables -L DOCKER-USER -n -v рассказывают правду: ACCEPT растёт при легитимных обращениях, DROP — при сканах.
Более чистые альтернативы, если готовы пересоздать контейнеры:
- биндить порт на loopback:
ports: ["127.0.0.1:8080:8080"]— но тогда удалённый доступ только через ssh-туннель или VPN; - объединить потребителя и служебный сервис в одну docker-сеть и ходить по имени контейнера (
service:8080) — тогдаports:вообще не нужен.
Подводные камни
- DOCKER-USER обрабатывает только forwarded-трафик — контейнеры за docker-bridge. Для контейнеров с
network_mode: hostцепочка не работает: там действует обычный INPUT/UFW. - Всегда снапшот
iptables-saveдо правок иiptables-persistentпосле — иначе правила не переживут reboot. - Не забудьте loopback и docker-подсети в allow, иначе сломаете взаимодействие между собственными контейнерами.
Вывод. Публикация порта в Docker обходит UFW: цепочка DOCKER идёт раньше его правил. Правильное место для ограничений — DOCKER-USER (allow нужных источников, DROP остального) с сохранением через iptables-persistent. Для network_mode: host эта цепочка не действует. А идеально — вообще не публиковать служебные сервисы наружу, связывая их с потребителями по внутренней docker-сети.