Docker vs UFW: порт закрыт, но доступен всем

Настраиваешь UFW. Закрываешь все порты. Пишешь ufw deny 8080, и VPS кажется защищённым. Потом запускаешь Docker с -p 8080:80 - порт всё равно открыт для всего интернета.

Это архитектурная ловушка Linux. В июне 2024 Alok из realogs.in описал инцидент: MongoDB на порту 27017 в контейнере, UFW блокирует порт. БД доступна извне. Крипто-майнер нашёл её и украл данные.

Я деплоил свой блог через Coolify на российский VPS - Redis был доступен снаружи несмотря на UFW. Час дебага, пока не понял: Docker работает на другом уровне iptables.

Проблема за 30 секунд

Запусти это:

sudo ufw deny 8080
sudo ufw status
# Вывод: 8080 DENIED Anywhere

docker run -d -p 8080:80 nginx:alpine

curl http://YOUR_SERVER_IP:8080
# Вывод: Welcome to nginx!

UFW говорит “закрыто”. Порт доступен всем.

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

Linux обрабатывает пакеты через iptables в несколько этапов. Docker вставляет свои правила на этапе PREROUTING (NAT таблица) - когда пакет только пришёл на сервер. UFW работает на этапе INPUT (filter таблица) - когда решение уже принято.

Входящий пакет на 8080
  ↓
PREROUTING (nat) - Docker одобряет ЗДЕСЬ
  ↓
Routing decision
  ↓
INPUT (filter) - UFW проверяет ЗДЕСЬ (слишком поздно!)
  ↓
Контейнер получает пакет

К тому моменту, когда пакет доходит до UFW, Docker уже изменил его адрес на 172.17.0.2:80. Отправил в контейнер. UFW видит пакет - ничего не может сделать.

Srikanth K описал это как следствие устройства фильтрации пакетов в ядре Linux. Docker и UFW работают корректно, но на разных уровнях. Viktor Petersson опубликовал “The Dangers of UFW + Docker” 3 ноября 2014 года.

Решение #1: DOCKER-USER chain

В 2017 Docker добавил цепь DOCKER-USER в iptables (Docker CE 17.06). Она срабатывает ДО всех Docker-правил.

Docker гарантирует, что никогда не перезапишет её.

Отредактируй /etc/ufw/after.rules, добавь в конец:

# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:DOCKER-USER - [0:0]

# Пропустить трафик через UFW цепи
-A DOCKER-USER -j ufw-user-forward

# Разрешить established connections
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j RETURN

# Блокировать invalid пакеты
-A DOCKER-USER -m conntrack --ctstate INVALID -j DROP

# Разрешить Docker-to-Docker
-A DOCKER-USER -i docker0 -o docker0 -j ACCEPT

# Вернуть контроль UFW
-A DOCKER-USER -j RETURN

COMMIT
# END UFW AND DOCKER

Применить:

sudo ufw reload

Теперь UFW работает. Чтобы разрешить контейнер на порту 80:

sudo ufw route allow proto tcp from any to any port 80

Заблокировать MongoDB:

sudo ufw route deny proto tcp from any to any port 27017

Проверка:

sudo iptables -L DOCKER-USER -n -v

Решение #2: ufw-docker (автоматизация)

Не хочешь редактировать файлы вручную? Есть готовый скрипт с 6230 звёздами на GitHub:

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 ufw reload

Управление контейнерами через простые команды:

docker run -d --name web -p 8080:80 nginx:alpine
sudo ufw-docker allow web 80

Для пользователей Coolify проще скопировать первое решение. Занимает 2 минуты.

Решение #3: Bind к 127.0.0.1 + reverse proxy

Самое надёжное архитектурное решение.

Контейнеры слушают только localhost, весь внешний трафик идёт через nginx или Caddy на хосте.

# Вместо
docker run -p 8080:80 nginx

# Использовать
docker run -p 127.0.0.1:8080:80 nginx

Порт 8080 физически недоступен снаружи - даже если UFW сломается. На хосте поставь Caddy:

sudo apt install caddy

В /etc/caddy/Caddyfile:

example.com {
  reverse_proxy localhost:8080
}

UFW правила становятся простыми:

sudo ufw allow 80
sudo ufw allow 443

Что нельзя делать

Не отключай iptables в Docker. Это ломает DNS внутри контейнеров, inter-container communication и NAT. Docker сам не рекомендует этот подход в документации.

Не редактируй DOCKER цепи напрямую. Docker перезапишет все изменения при рестарте.

Не надейся на UFW без интеграции. UFW не видит Docker-трафик без DOCKER-USER.

Это критично прямо сейчас

28 февраля 2025 вышел Docker Engine v28 с улучшенной изоляцией контейнеров в локальных сетях. Порты, опубликованные через -p, всё равно доступны извне без дополнительной настройки.

На публичных VPS это серьёзный риск.

Задокументированы инциденты: MongoDB ransomware, crypto-mining malware, DDOS через открытые порты.

5 минут на конфигурацию DOCKER-USER сейчас - спокойный сон потом. Проверь свой VPS прямо сейчас:

sudo ufw status
docker ps --format "table {{.Ports}}"

Видишь опубликованные порты (0.0.0.0:8080->80/tcp), а UFW говорит DENIED? Ты в зоне риска.