Ситуация, знакомая многим: свежий VPS, в UFW политика default deny incoming, открыты только 22, 80 и 443. Через несколько дней в базе появляется записка с требованием выкупа. Причина — строка ports: - "5432:5432" в compose-файле. Docker управляет правилами iptables напрямую и обходит UFW, поэтому порт становится доступен из интернета, даже если файрвол «всё закрыл». В статье разберём, как предотвратить это до деплоя и как проверить, что реально слушает на машине, без внешних утилит.

Почему UFW не защищает от Docker

Docker при публикации порта создаёт цепочки в iptables (DOCKER, DOCKER-USER), которые обрабатываются до правил UFW. В результате контейнерный порт доступен снаружи, хотя UFW показывает статус «active» и запрещает входящие. Это не баг, а особенность архитектуры: Docker управляет сетью независимо от файрвола. Типичная ошибка — полагаться только на UFW и не проверять, какие порты опубликованы в compose-файлах.

Решение — двухуровневое: статическая проверка compose-файлов до деплоя и динамический аудит того, что реально слушает на хосте. Ниже — два небольших скрипта на Python, которые можно встроить в CI или запускать локально перед выкладкой.

Скрипт 1: проверка compose-файлов до деплоя

Задача — найти в YAML-файлах сервисы, у которых в секции ports указан порт, привязанный ко всем интерфейсам (например, "5432:5432" или "5432"). Если такой найден, скрипт завершается с ненулевым кодом и выводит предупреждение. Это позволяет остановить пайплайн до того, как опасная конфигурация попадёт на сервер.

Используем PyYAML для разбора. Важно учитывать разные формы записи: строка с двоеточием, строка без двоеточия (только контейнерный порт), словарь с ключами target и published. Пример логики:

  • Загрузить YAML, проверить наличие ключа services.
  • Для каждого сервиса извлечь список ports.
  • Для каждой записи определить, опубликован ли порт на хост. Если да — проверить, указан ли IP-адрес. Если IP не указан или указан 0.0.0.0, это риск.
  • Собрать список проблемных сервисов и портов, вывести в stderr, вернуть код 1.

Типичная ошибка — игнорировать короткий синтаксис "5432", который тоже публикует порт на все интерфейсы. Другая — не учитывать, что в длинном синтаксисе можно задать host_ip: 127.0.0.1, и тогда порт доступен только локально. Скрипт должен различать эти случаи.

Пример вывода при обнаружении проблемы: Сервис db: порт 5432 опубликован на все интерфейсы (0.0.0.0:5432). Такой сигнал легко заметить в логах CI.

Скрипт 2: аудит реально слушающих портов через /proc

Даже если compose-файлы чисты, на машине могут остаться контейнеры, запущенные вручную, или другие процессы. Проверить, что реально слушает, можно без ss и netstat, читая /proc/net/tcp и /proc/net/tcp6. Формат: hexadecimal-адрес, hexadecimal-порт, состояние. Нас интересуют сокеты в состоянии 0A (LISTEN).

Алгоритм:

  • Прочитать файлы /proc/net/tcp и /proc/net/tcp6.
  • Пропустить заголовок, разобрать строки: локальный адрес и порт в hex.
  • Отфильтровать состояние 0A.
  • Преобразовать адрес: для IPv4 — четыре байта в обратном порядке, для IPv6 — 16 байт. Определить, является ли адрес loopback (127.0.0.1 или ::1) или внешним.
  • Вывести список портов, слушающих на не-loopback адресах.

Пример: если Postgres слушает на 0.0.0.0:5432, скрипт покажет это. Если на 127.0.0.1:5432 — не покажет, что корректно. Важно не путать hex-представление порта с десятичным: 5432 в hex — 0x1538, в файле будет 1538.

Ошибка — забыть про IPv6. Многие сервисы слушают и на ::, что эквивалентно всем интерфейсам. Скрипт должен обрабатывать оба файла.

Как встроить в процесс

Оба скрипта можно запускать локально перед коммитом или в CI. Первый — как линтер compose-файлов, второй — как пост-деплой проверку на сервере. Не стоит полагаться только на автоматику: периодически просматривайте вывод вручную, особенно после добавления новых сервисов.

Критерии выбора: если у вас небольшой проект, достаточно первого скрипта и правила «всегда указывать 127.0.0.1 для внутренних портов». Для продакшена с несколькими средами полезен второй скрипт как часть health-check. Не пытайтесь закрыть всё одним инструментом — комбинация статической и динамической проверки надёжнее.

Помните: безопасность — это процесс, а не одноразовая настройка. Регулярный аудит портов снижает риск, но не заменяет другие меры: обновления, минимальные привилегии, мониторинг.