Ситуация, знакомая многим: свежий 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. Не пытайтесь закрыть всё одним инструментом — комбинация статической и динамической проверки надёжнее.
Помните: безопасность — это процесс, а не одноразовая настройка. Регулярный аудит портов снижает риск, но не заменяет другие меры: обновления, минимальные привилегии, мониторинг.