Безопасность и Защита 10 мин чтения 2026-09-12

Docker обходит UFW на Linux: как закрыть внутренние порты контейнеров от внешнего мира и защитить базы данных

Вы закрыли порты в UFW, но после запуска docker-compose базы данных MySQL, Redis и внутренние сервисы оказались открыты всему интернету? Разбираем, почему Docker манипулирует iptables в обход UFW, почему опция iptables: false ломает сеть и как надежно изолировать порты контейнеров.

Инженерная редакция SysKit Проверено инженерами SysKit.ru

Знакомая ситуация для любого администратора или 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 (транзитный трафик).
  • В цепочке FORWARD Docker услужливо размещает собственную цепочку 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. Но вместе с этим вы получаете катастрофические побочные эффекты:

  1. Контейнеры теряют доступ в интернет: Docker больше не создает правила маскарадинга (MASQUERADE) в таблице nat. В результате контейнеры не могут отправлять внешние запросы: отваливаются вызовы сторонних API (Telegram Bot API, платежные шлюзы, OpenAI), команды curl, отправка вебхуков и обновление пакетов через apt update.
  2. Ломается публикация любых портов: если вам все же потребуется открыть порт 80 или 443 для Nginx, Docker не создаст правила DNAT. Вам придется вручную писать и поддерживать десятки громоздких правил iptables для каждого сервиса и виртуальной подсети.
  3. Ломается межконтейнерная маршрутизация: изоляция между сетями перестает функционировать штатно.
Стратегия защиты Сложность внедрения Влияние на сеть контейнеров Рекомендация инженера
Привязка к 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:

  1. Никаких «голых» портов в Compose: проверьте все docker-compose.yml файлы. Замените "3306:3306" на "127.0.0.1:3306:3306", либо вовсе удалите секцию ports, если служба нужна только соседним контейнерам.
  2. Разделяйте сети frontend и backend: используйте как минимум две изолированные сети Docker: frontend-net (доступна Nginx и внешнему миру) и backend-net (доступна только бэкенду, базам данных и кэшу). Nginx не должен иметь прямого сетевого доступа к СУБД.
  3. Смените стандартные порты служб хоста: если для отладки требуется выставить порт наружу, не используйте стандартные порты 3306, 5432, 6379, 27017 — автоматические сканеры сканируют их в первую очередь.
  4. Используйте сложные пароли: даже при случайной утечке порта пароль длиной от 24 случайных символов защитит от мгновенного подбора по словарю.
  5. Проверяйте сетевой статус утилитой ss: регулярно выполняйте ss -tulpn и убеждайтесь, что в выводе нет нежелательных служб, слушающих на адресе 0.0.0.0.
  6. Просканируйте сервер внешним сканером: сразу после деплоя запустите проверку через Сканер портов онлайн, чтобы исключить человеческий фактор и забытые правила.

🎯 Разверните защищённый VDS с изолированной приватной сетью

Защитите свои контейнеры и базы данных: размещение в закрытом контуре (VPC), аппаратный файрвол, чистый IPv4 и сверхбыстрые NVMe-диски.

Подобрать безопасный VDS для Docker

Рекомендуемые VDS-провайдеры

Отказоустойчивые сервера с быстрыми NVMe и каналом до 1 Гбит/с

Выбор редакции
⚡

Timeweb Cloud

Надежные VDS с быстрой изолированной приватной сетью (VPC), защитой от сетевых атак на уровне L3/L4 и моментальным созданием снапшотов конфигураций.

Конфигурация
от 1 vCPU / 2 GB RAM / 30 GB NVMe
Enterprise & Highload
🔷

Selectel

Сертифицированная инфраструктура Tier III. Аппаратная изоляция сетевых контуров, приватные подсети без выхода наружу и выделенные фаерволы.

Конфигурация
от 2 vCPU / 4 GB RAM / 50 GB NVMe
Идеально для вебмастеров
🟠

Beget

Удобное развертывание виртуальных серверов с готовым Docker, автоматические ежедневные резервные копии и круглосуточная экспертная поддержка.

Конфигурация
от 1 vCPU / 2 GB RAM / 35 GB NVMe

Вопросы и ответы (FAQ)

Популярные вопросы вебмастеров и сисадминов по данной теме

Почему UFW показывает статус «deny», а порт Docker все равно доступен снаружи?
Потому что правила UFW помещаются в цепочку INPUT, а сетевой трафик к опубликованным портам Docker преобразуется правилами DNAT в цепочке PREROUTING и направляется в цепочку FORWARD в обход цепочки INPUT.
Безопасно ли использовать привязку портов 127.0.0.1:3306:3306 в docker-compose?
Да, это абсолютно безопасный и стандартный способ. При такой записи порт слушает исключительно на интерфейсе локальной петли хоста (lo) и физически недоступен для сетевых пакетов, приходящих с внешних сетевых интерфейсов eth0/ens3.
Почему нельзя просто отключить iptables в /etc/docker/daemon.json?
Параметр {"iptables": false} отключает правила маскарадинга (MASQUERADE). В результате все контейнеры полностью теряют доступ во внешний интернет: они не могут вызывать внешние API, отправлять уведомления и вебхуки, а также обновлять системные пакеты.
Как проверить, защищен ли мой сервер от сканирования портов контейнеров прямо сейчас?
Воспользуйтесь онлайн-инструментом SysKit «Сканер открытых портов онлайн» (/port-checker). Введите ваш внешний IP и проверьте порты 3306, 6379, 5432. Если чекер сообщает «Порт закрыт» или «Connection timed out», значит фаервол работает корректно.

Читайте также в блоге SysKit

Смежные руководства и полезные технические статьи