Знакомая ситуация для любого администратора или DevOps-инженера: вы развернули свежий VDS на Ubuntu, аккуратно настроили системный фаервол ufw default deny incoming, открыли строго порты 22 (SSH), 80 и 443 для веб-трафика и проверили статус командой ufw status — все остальные входящие соединения заблокированы. Затем вы запускаете проект через Docker Compose, в котором крутятся MySQL (3306), Redis (6379) или панель управления, и уверены, что базы доступны только локально. Однако уже через пару дней в Redis обнаруживается подозрительный ключ с криптомайнером, а в логах базы данных видны тысячи попыток брутфорса со всего мира.
Причина кроется в фундаментальной архитектурной особенности сетевого стека Linux: Docker манипулирует правилами iptables напрямую в обход цепочек UFW. Для входящих пакетов из интернета правила UFW просто не существуют.
В этом руководстве мы детально разберем, почему возникает пробитие фаервола, как быстро проверить безопасность своих контейнеров и какие проверенные методы позволяют надежно запереть внутренние порты без нарушения сетевой связности Docker.
1. Анатомия проблемы: почему Docker игнорирует правила UFW
Чтобы понять, как Docker пробивает фаервол, нужно взглянуть на то, как ядро Linux обрабатывает входящий сетевой трафик и в какие точки внедряются UFW и Docker-демон.
Фаервол UFW (Uncomplicated Firewall) является надстройкой над iptables (или современной подсистемой nftables). По умолчанию большинство правил, которые вы создаете через команду sudo ufw allow ... или sudo ufw deny ..., помещаются в пользовательские цепочки внутри системной цепочки INPUT таблицы filter. Цепочка INPUT отвечает за пакеты, адресованные локальным процессам самого хоста.
Когда же вы публикуете порт контейнера (например, указывая ports: - "3306:3306" в Compose или флаг -p 3306:3306 в Docker CLI), демон Docker работает совершенно иначе:
- Docker создает правила в таблице трансляции сетевых адресов nat в цепочке PREROUTING.
- Входящий пакет из внешнего интерфейса (например,
eth0) попадает в цепочкуPREROUTINGдо того, как ядро примет решение о маршрутизации (Routing Decision). - Правило DNAT (Destination NAT) немедленно переписывает IP-адрес назначения с внешнего IP сервера на приватный IP-адрес контейнера в виртуальном мосту (например,
172.18.0.4:3306). - Поскольку адрес назначения изменился на чужой IP (IP контейнера), ядро направляет пакет не в цепочку
INPUT, а в цепочку FORWARD (транзитный трафик). - В цепочке
FORWARDDocker услужливо размещает собственную цепочку DOCKER с разрешающим правилом (ACCEPT) для всех опубликованных портов.
В результате цепочка INPUT, где добросовестно дежурит UFW с запретом 3306 порта, просто никогда не получает эти сетевые пакеты. Они перехватываются на этапе PREROUTING и беспрепятственно улетают напрямую в контейнер.
⚠️ Официальная позиция Docker: «Это не баг, а фича»
Команда разработчиков Docker не считает такое поведение уязвимостью. С точки зрения сетевой модели Linux, Docker выполняет маршрутизацию виртуальных сетей (L3 Routing & NAT), поэтому использование цепочек PREROUTING и FORWARD является штатным поведением. Однако для неподготовленного администратора это превращается в скрытую брешь в безопасности периметра.
2. Экспресс-аудит за 60 секунд: проверка открытых портов из интернета
Проверим прямо сейчас, находится ли ваш боевой VDS под угрозой. Аудит состоит из двух шагов: локальной проверки сокетов и внешнего сканирования.
Сначала подключитесь к серверу по SSH и посмотрите, на каких интерфейсах слушают процессы:
# Проверяем все слушающие TCP-порты и процессы
sudo ss -tulpn | grep -E '3306|6379|5432|27017|9000'
Обратите внимание на столбец Local Address:Port. Если вы видите привязку к 0.0.0.0:3306 или :::3306 с процессом docker-proxy, этот порт открыт на всех сетевых интерфейсах машины:
tcp LISTEN 0 4096 0.0.0.0:3306 0.0.0.0:* users:(("docker-proxy",pid=14820,fd=4))
tcp LISTEN 0 4096 [::]:3306 [::]:* users:(("docker-proxy",pid=14826,fd=4))
Далее посмотрим реальные правила трансляции в таблице nat, которые Docker добавил в ядро:
# Выводим цепочку DOCKER в таблице nat
sudo iptables -t nat -L DOCKER -n -v
Если в выводе присутствует строка вида DNAT tcp -- * * 0.0.0.0/0 172.18.0.4 to:172.18.0.4:3306, порт вашей базы открыт любому узлу в интернете.
Теперь проведем решающую проверку снаружи — так, как видит ваш сервер сканер злоумышленника. Воспользуйтесь нашим бесплатным инструментом Сканер открытых портов онлайн. Введите внешний IP-адрес вашего VDS (узнать его можно через Мой IP адрес и данные клиента) и укажите порты 3306, 6379, 5432. Если чекер рапортует об открытом порте при активном UFW — сервер уязвим.
3. Способ №1: изоляция на уровне Compose и привязка к localhost
Самый простой, надежный и рекомендуемый способ защиты — принудительная привязка опубликованного порта к локальной петле (Loopback Interface) 127.0.0.1.
В 95% случаев сисадмины публикуют порты баз данных лишь для того, чтобы подключиться к ним со своего ноутбука через GUI-клиенты (DBeaver, DataGrip, TablePlus) или для локального бэкапа на хосте.
По умолчанию в docker-compose.yml пишут краткую запись:
services:
database:
image: mysql:8.4
ports:
- "3306:3306" # ОПАСНО: эквивалентно "0.0.0.0:3306:3306" и ":::3306"
Замените ее на явное указание IP-адреса локального интерфейса:
services:
database:
image: mysql:8.4
ports:
- "127.0.0.1:3306:3306" # БЕЗОПАСНО: доступно только с самого хоста
После перезапуска контейнеров (docker compose up -d) Docker создаст правило DNAT, срабатывающее исключительно на адрес 127.0.0.1. Любые входящие пакеты из внешней сети eth0 будут игнорироваться ядром.
💡 Как безопасно подключаться к MySQL/PostgreSQL с рабочего компьютера
Если порт привязан к 127.0.0.1, вы не сможете подключиться к нему напрямую по публичному IP сервера. Используйте стандартный SSH-туннель (Port Forwarding). Большинство современных клиентов (DBeaver, DataGrip, HeidiSQL) поддерживают SSH-туннели «из коробки» во вкладке настроек подключения (SSH Tunnel). Либо поднимите туннель одной командой в терминале: ssh -N -L 3307:127.0.0.1:3306 user@vds_ip, после чего подключайтесь к localhost:3307 на своем ПК.
4. Способ №2: взаимодействие контейнеров во внутренних сетях без публикации портов
Задайте себе вопрос: «А действительно ли порт СУБД должен быть виден на хост-системе?»
Если у вас типовой веб-стек (Nginx + PHP-FPM / Node.js + MySQL + Redis), то веб-приложению совершенно не требуется, чтобы база данных выставляла порты в операционную систему хоста. Контейнеры одного Compose-проекта по умолчанию объединены в общую пользовательскую сеть bridge.
Внутри пользовательских сетей Docker работает встроенный DNS-сервер (по адресу 127.0.0.11). Контейнеры связываются друг с другом напрямую по именам сервисов и их стандартным внутренним портам:
services:
backend:
image: my-app:latest
environment:
DB_HOST: db # Имя сервиса базы в Docker-сети
DB_PORT: 3306 # Стандартный внутренний порт
REDIS_HOST: redis
networks:
- internal-net
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: app_db
POSTGRES_PASSWORD: secret_password
# БЛОК PORTS ПОЛНОСТЬЮ УДАЛЕН! База недоступна извне и с хоста.
networks:
- internal-net
redis:
image: redis:7-alpine
# БЛОК PORTS ПОЛНОСТЬЮ УДАЛЕН!
networks:
- internal-net
networks:
internal-net:
driver: bridge
При такой архитектуре порты 5432 и 6379 физически не открываются в таблицах iptables хоста. Ни один внешний сканер и ни один ботнет не смогут даже послать TCP SYN-пакет в сторону вашей базы данных.
⚠️ Разница между директивами ports и expose
Не путайте директивы в docker-compose.yml: секция ports транслирует порт на хост-машину через iptables. Секция expose лишь документирует порт и делает его доступным связанным контейнерам (Linked Containers), не открывая доступ на внешних сетевых интерфейсах VDS.
5. Опасный миф: почему нельзя слепо включать iptables: false в daemon.json
На многих форумах и в устаревших статьях можно встретить «универсальный совет»: добавить в конфигурационный файл /etc/docker/daemon.json строчку {"iptables": false} и перезапустить Docker.
Действительно, эта опция полностью запрещает демону Docker прикасаться к правилам iptables. Но вместе с этим вы получаете катастрофические побочные эффекты:
- Контейнеры теряют доступ в интернет: Docker больше не создает правила маскарадинга (MASQUERADE) в таблице nat. В результате контейнеры не могут отправлять внешние запросы: отваливаются вызовы сторонних API (Telegram Bot API, платежные шлюзы, OpenAI), команды
curl, отправка вебхуков и обновление пакетов черезapt update. - Ломается публикация любых портов: если вам все же потребуется открыть порт 80 или 443 для Nginx, Docker не создаст правила DNAT. Вам придется вручную писать и поддерживать десятки громоздких правил iptables для каждого сервиса и виртуальной подсети.
- Ломается межконтейнерная маршрутизация: изоляция между сетями перестает функционировать штатно.
| Стратегия защиты | Сложность внедрения | Влияние на сеть контейнеров | Рекомендация инженера |
|---|---|---|---|
| Привязка к 127.0.0.1 | Минимальная (1 мин) |
Нулевое (полная работоспособность). | Идеально для 95% задач на одиночном VDS. |
| Внутренняя сеть bridge | Минимальная (2 мин) |
Порт не выставляется на хост. | Лучшая практика архитектуры (Zero Exposed Ports). |
| iptables: false в daemon.json | Опасная ловушка |
Ломает исходящий NAT и доступ в интернет. | Категорически не рекомендуется в продакшене. |
| Цепочка DOCKER-USER / ufw-docker | Средняя (5 мин) |
Сохраняет весь NAT и подключает UFW. | Профессиональный выбор для сложных инфраструктур. |
6. Профессиональное решение: настройка цепочки DOCKER-USER и утилита ufw-docker
Что делать, если вам обязательно нужно публиковать порт контейнера наружу (например, кастомный TCP-порт приложения, VPN-шлюз или панель управления), но вы хотите управлять доступом к нему строго через правила UFW?
Разработчики Docker предусмотрели специальную цепочку под названием DOCKER-USER. Все правила, добавленные в цепочку DOCKER-USER, выполняются в самом начале цепочки FORWARD до того, как отработают внутренние правила Docker.
Самый элегантный и поддерживаемый способ подружить UFW и Docker — популярная утилита с открытым исходным кодом ufw-docker. Она встраивает корректные хуки фильтрации в /etc/ufw/after.rules, заставляя UFW контролировать транзитный трафик в контейнеры.
Установим утилиту на сервер:
# Скачиваем исполняемый скрипт ufw-docker
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
# Интегрируем правила ufw-docker в конфигурацию UFW
sudo ufw-docker install
Команда install аккуратно модифицирует файл /etc/ufw/after.rules, добавляя в цепочку DOCKER-USER правила, блокирующие доступ ко всем контейнерам извне по умолчанию, за исключением явно разрешенных.
Перезапустим фаервол UFW для применения изменений:
sudo systemctl restart ufw
Теперь вы можете гибко управлять доступом к конкретным контейнерам с помощью привычного синтаксиса:
# Разрешить публичный доступ к веб-порту конкретного контейнера
sudo ufw-docker allow my-nginx 80/tcp
# Разрешить доступ к порту базы данных ТОЛЬКО с доверенного статического IP-адреса офиса
sudo ufw-docker allow my-postgres 5432/tcp 198.51.100.25
# Проверить текущие правила фильтрации контейнеров
sudo ufw-docker status
После этого даже если в docker-compose.yml указано ports: - "5432:5432", фаервол отсечет любые попытки подключения от неавторизованных IP-адресов.
7. Чек-лист сетевой безопасности Docker на боевом VDS
Закрепим материал итоговым чек-листом для аудита каждого сервера, на котором развернут Docker:
- Никаких «голых» портов в Compose: проверьте все
docker-compose.ymlфайлы. Замените"3306:3306"на"127.0.0.1:3306:3306", либо вовсе удалите секциюports, если служба нужна только соседним контейнерам. - Разделяйте сети frontend и backend: используйте как минимум две изолированные сети Docker:
frontend-net(доступна Nginx и внешнему миру) иbackend-net(доступна только бэкенду, базам данных и кэшу). Nginx не должен иметь прямого сетевого доступа к СУБД. - Смените стандартные порты служб хоста: если для отладки требуется выставить порт наружу, не используйте стандартные порты 3306, 5432, 6379, 27017 — автоматические сканеры сканируют их в первую очередь.
- Используйте сложные пароли: даже при случайной утечке порта пароль длиной от 24 случайных символов защитит от мгновенного подбора по словарю.
- Проверяйте сетевой статус утилитой ss: регулярно выполняйте
ss -tulpnи убеждайтесь, что в выводе нет нежелательных служб, слушающих на адресе0.0.0.0. - Просканируйте сервер внешним сканером: сразу после деплоя запустите проверку через Сканер портов онлайн, чтобы исключить человеческий фактор и забытые правила.
🎯 Разверните защищённый VDS с изолированной приватной сетью
Защитите свои контейнеры и базы данных: размещение в закрытом контуре (VPC), аппаратный файрвол, чистый IPv4 и сверхбыстрые NVMe-диски.
Подобрать безопасный VDS для DockerРекомендуемые VDS-провайдеры
Отказоустойчивые сервера с быстрыми NVMe и каналом до 1 Гбит/с
Timeweb Cloud
Надежные VDS с быстрой изолированной приватной сетью (VPC), защитой от сетевых атак на уровне L3/L4 и моментальным созданием снапшотов конфигураций.
Selectel
Сертифицированная инфраструктура Tier III. Аппаратная изоляция сетевых контуров, приватные подсети без выхода наружу и выделенные фаерволы.
Beget
Удобное развертывание виртуальных серверов с готовым Docker, автоматические ежедневные резервные копии и круглосуточная экспертная поддержка.