Docker-порт на 127.0.0.1 открылся из LAN после reload: как найти причину и проверить защиту
Контейнер запущен с `-p 127.0.0.1:18080:80`. Локально отвечает, с соседнего компьютера порт сервера недоступен. Аудит пройден. Потом администратор выполняет `firewall-cmd --reload`, и до приложения можно добраться другим маршрутом. Я, Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш», покажу, где возникает этот обход и какую проверку я закладываю в приёмку инфраструктуры: она должна подтверждать защиту после изменения правил, пока само приложение продолжает работать.
Сначала проверьте версию и адрес, который тестируете
У этого сценария есть конкретный номер: CVE-2025-54388. Регрессия затрагивает Moby/Docker Engine 28.2.0–28.3.2; исправление вышло в 28.3.3 29 июля 2025 года. Речь о Linux Engine, работающем в сетевом пространстве имён хоста, с bridge-сетями и интеграцией с firewalld. Проверять нужно версию сервера командой `docker version --format '{{.Server.Version}}'`: версия установленного клиента ничего не доказывает. Для пакета с исправлениями от поставщика дополнительно смотрю его бюллетень. [Бюллетень Moby](https://github.com/moby/moby/security/advisories/GHSA-x4rx-4gw3-53p4).
Самая неприятная деталь — адрес публикации может остаться прежним. После reload соседняя машина получает доступ к IP контейнера и внутреннему опубликованному порту, если направляет пакеты через Docker-хост. Запрос к `192.168.50.10:18080` при этом способен по-прежнему завершаться ошибкой. Поэтому заключение «просканировали IP сервера, всё закрыто» здесь слабое: проверили только один путь к приложению.
Паниковать из-за любого Docker тоже незачем. Эта CVE сама по себе не открывает неопубликованные порты и не означает автоматическую доступность из интернета. Для обхода нужен подходящий сетевой путь. Версии до 28.2.0 не затронуты именно этой регрессией, но откатываться на них я не советую: документация отдельно предупреждает о доступе соседей по L2 к localhost-публикациям до 28.0.0. Это другой дефект, которому reload не требуется. [Предупреждение Docker о старых версиях](https://docs.docker.com/engine/network/port-publishing/).
Что именно теряется при reload firewalld
Публикация Docker — это сочетание перенаправления и фильтрации пакетов. Трафик к адресу контейнера проходит через маршрутизацию хоста; запрет входа в сам хост через INPUT не заменяет проверку этого пути. При штатной интеграции Docker создаёт в firewalld зону `docker` с target `ACCEPT` и политику `docker-forwarding`, разрешающую пересылку в неё. Такое состояние ожидаемо: дополнительные ограничения обеспечивает Docker. Увидеть ACCEPT и объявить его причиной инцидента было бы слишком поспешно. [Интеграция Docker с firewalld](https://docs.docker.com/engine/network/packet-filtering-firewalls/).
Обычный reload применяет permanent-конфигурацию как новую runtime-конфигурацию и сохраняет состояние соединений. В затронутой связке удаляются и правила Docker, после чего демон восстанавливает их по уведомлению firewalld. Ошибка заключалась в пропуске правил сетевых endpoints. Исправление добавляет их восстановление перед восстановлением правил портов. Поэтому работающий NAT и правильная строка в `docker ps` вполне совместимы с дырой в фильтрации. [Описание reload](https://firewalld.org/documentation/howto/reload-firewalld.html), [патч Moby № 50506](https://github.com/moby/moby/pull/50506/files).
Для обычной NAT bridge-сети в рассматриваемой ветке важен запрет в таблице raw, цепочке PREROUTING. Для нашего будущего стенда он выглядит так: `-A PREROUTING -d 172.30.88.10/32 ! -i br-fwtest -j DROP`. Пакет, уже адресованный контейнеру и пришедший не с его bridge, отбрасывается до DNAT. После исчезновения запрета прямой пакет доходит до разрешения опубликованного TCP/80 в filter. Это объяснение следует из правил и патча; приложение не обязано менять свой bind. [Официальный пример правил Moby 28.3.2](https://github.com/moby/moby/blob/v28.3.2/integration/network/bridge/iptablesdoc/generated/usernet-portmap-lo.md).
Стенд «Вектор»: два узла и один намеренно локальный сервис
Разберу модельную инфраструктуру ООО «Вектор», производственной компании на 120 рабочих мест. Название условное; это учебная реконструкция по подтверждённому дефекту, не отчёт об испытаниях заказчика. Сюжет простой: служебный веб-интерфейс доступен через reverse proxy на хосте, а его контейнерный порт предназначен только для localhost. Чтобы поведение бизнес-приложения не мешало диагностике, в стенде его роль выполняет стандартная страница nginx. Ниже указаны ожидаемые результаты, которые нужно получить и записать при собственном прогоне.
Я выделяю Docker-хосту 2 vCPU, 2 ГБ RAM и 20 ГБ диска, проверяющей VM — 1 vCPU и 1 ГБ RAM. Это выбранные размеры, а не минимальные требования продукта. На сервере беру Debian 12 amd64, firewalld 1.3.3, iptables 1.8.9 с выводом `(nf_tables)` и Docker Engine 28.3.2. Первые две версии подтверждаются пакетами Debian. Сервер имеет адрес `192.168.50.10/24`, проверяющая машина — `192.168.50.20/24`; обе находятся в отдельной лабораторной LAN. [Пакет firewalld](https://packages.debian.org/bookworm/firewalld), [пакет iptables](https://packages.debian.org/bookworm/net/iptables).
В `/etc/firewalld/firewalld.conf` фиксирую `FirewallBackend=nftables` и `FlushAllOnReload=yes`; LAN-интерфейс сервера находится в зоне public. Firewalld должен работать до запуска Docker. У Docker оставляю управление iptables включённым, `allow-direct-routing` выключенным, trusted-интерфейсы не задаю. Здесь нет противоречия: backend firewalld и backend Docker — разные настройки, а iptables-nft остаётся интерфейсом iptables. Правила direct/passthrough firewalld обрабатывает через iptables даже при собственном nftables backend. [Параметры firewalld](https://firewalld.org/documentation/man-pages/firewalld.conf.html).
Создаю отдельную сеть без пересечений с существующими маршрутами и контейнер с фиксированным адресом. Старые версии Engine и образа нужны только для воспроизведения; для рабочего сервера это не спецификация обновления. Команды выполняются на Docker-хосте: ```bash docker network create --driver bridge --subnet 172.30.88.0/24 \ --opt com.docker.network.bridge.name=br-fwtest \ --opt com.docker.network.bridge.gateway_mode_ipv4=nat fwtest docker run -d --name fw-local --restart unless-stopped \ --network fwtest --ip 172.30.88.10 \ -p 127.0.0.1:18080:80/tcp nginx:1.28.0-alpine docker inspect fw-local --format '{{json .NetworkSettings.Ports}}' curl --noproxy '*' --connect-timeout 2 --max-time 3 http://127.0.0.1:18080/ ```
Как поймать изменение, которое пропускает обычный аудит
Сначала на соседней VM добавляю маршрут только к тестовому контейнеру. Он временный и не затрагивает весь диапазон Docker. Перед добавлением проверяю отсутствие конфликтующего маршрута; команда `ip route get` после добавления должна показать Docker-хост как следующий узел. Затем проверяю оба адреса. Маршрут оставляю неизменным до конца всех фаз: иначе можно принять потерю маршрута за исправление firewall. ```bash sudo ip route add 172.30.88.10/32 via 192.168.50.10 ip route get 172.30.88.10 curl --noproxy '*' --connect-timeout 2 --max-time 3 http://192.168.50.10:18080/ curl --noproxy '*' --connect-timeout 2 --max-time 3 http://172.30.88.10:80/ ```
До reload оба внешних запроса в заданной конфигурации должны завершаться без подключения, а локальный запрос на сервере — получать страницу nginx. Это исходная точка. Если прямой запрос уже успешен, я останавливаю сравнение и выясняю причину: включён direct routing, изменён gateway mode, присутствуют сторонние правила или взята другая версия. Подгонять такой результат под CVE неправильно. Если локальная страница не открылась, отрицательные внешние пробы пока вообще не имеют диагностической ценности.
На сервере сохраняю полный набор правил, выполняю reload и снимаю второй набор. Отдельный nft-снимок нужен, чтобы видеть и правила самого firewalld. Восстановление Docker идёт по уведомлению: кроме немедленного снимка, повторяю сбор после стабилизации правил и проверяю журнал. Для опыта достаточно обычного reload; `--complete-reload` здесь лишний. ```bash sudo iptables-save > /tmp/fw-before.v4 sudo nft list ruleset > /tmp/fw-before.nft sudo firewall-cmd --reload sudo iptables-save > /tmp/fw-after.v4 sudo nft list ruleset > /tmp/fw-after.nft diff -u /tmp/fw-before.v4 /tmp/fw-after.v4 sudo journalctl -u docker -u firewalld --since '5 minutes ago' --no-pager ```
Теперь повторяю те же внешние запросы. Характерный результат уязвимого стенда: `172.30.88.10:80` отдаёт страницу, хотя `192.168.50.10:18080` продолжает молчать и localhost работает. В raw-снимке отсутствует запрет для endpoint. Именно сочетание изменения доступности и потери правила связывает наблюдение с механизмом CVE. Один текстовый diff слабее: порядок правил тоже может меняться. Ещё одна частая ошибка — считать HTTP 403 доказательством закрытого порта. Любой HTTP-ответ означает, что сетевое подключение уже удалось.
Что я исправляю в первую очередь
Мой основной выбор — обновление Docker Engine до поддерживаемой исправленной сборки. Менять синтаксически правильную публикацию `127.0.0.1:18080:80` ради этой CVE не требуется. В лаборатории сравниваю 28.3.2 с 28.3.3, сохраняя firewalld, образ и адресацию: так видна роль исправления Engine. В эксплуатации выбираю актуальный пакет из согласованного репозитория и после обновления снова проверяю именно запущенный Server. Наличие нового пакета на диске ещё не означает, что старый dockerd завершён. [Исправление в Docker 28.3.3](https://docs.docker.com/engine/release-notes/28/#2833).
Если обновить немедленно нельзя, официальный временный обход — перезапустить Docker daemon после reload либо пересоздать bridge-сети. Из этих вариантов я обычно выбираю управляемый `sudo systemctl restart docker`: меньше ручной работы с подключениями сетей. Но сначала учитываю остановку нагрузки, restart policies и готовность приложения после старта. `docker restart` одного контейнера не подменяет рекомендованный перезапуск демона. На уязвимой версии следующий reload снова возвращает риск, поэтому такой обход нельзя считать завершённым исправлением. [Временные меры Moby](https://github.com/moby/moby/security/advisories/GHSA-x4rx-4gw3-53p4).
Отключение управления firewall через `"iptables": false` я сразу исключаю: Docker предупреждает о поломке сетевого поведения и возможной доступности контейнерных портов без замещающих правил. Ручную защиту через DOCKER-USER рассматриваю как отдельную политику с собственным тестом восстановления, а не как бессрочную заплатку одной командой. Запрет только INPUT или удаление порта из public-зоны не проверяет маршрут к контейнеру. [Ограничения управления правилами Docker](https://docs.docker.com/engine/network/packet-filtering-firewalls/).
Rootless и контейнеры, подключённые исключительно к internal-сетям, имеют другие границы воздействия этой CVE. Однако срочно переносить туда работающий сервис я не предлагаю: нужно проверить его реальные сетевые зависимости. На перспективу я сокращаю публикации: если backend нужен только соседнему reverse proxy в Docker, его порт можно вообще не публиковать на хост. Но изоляцию от посторонних контейнеров всё равно проектирую отдельно; отсутствие `-p` не заменяет сегментацию внутри bridge.
Как доказать, что порт снова закрыт
После исправления я проверяю одновременно работоспособность сервиса и недоступность запрещённого пути. На сервере localhost должен отвечать. С LAN выполняю новые TCP-подключения к контейнеру при сохранённом маршруте. Для этого удобен `nc` из netcat-openbsd; браузер с открытой вкладкой хуже, потому что может использовать старое соединение. Обычный reload сохраняет состояние соединений, поэтому проверка новой сессии принципиальна. [Поведение reload firewalld](https://firewalld.org/documentation/howto/reload-firewalld.html). ```bash # На проверяющей VM: nc -vz -w 2 172.30.88.10 80 nc -vz -w 2 192.168.50.10 18080 ```
Одного таймаута я не принимаю. Он может означать неправильный маршрут, недоступный сервер или фильтрацию до Docker-хоста. На сервере проверяю восстановленное правило и его счётчик до и после внешних попыток. Команда `-C` только проверяет наличие правила; `!` здесь — аргумент iptables. Это точный образец для описанного стенда, не универсальная строка для всех версий. ```bash sudo iptables -t raw -C PREROUTING \ -d 172.30.88.10/32 ! -i br-fwtest -j DROP sudo iptables -t raw -L PREROUTING -n -v -x --line-numbers ```
Если картина не сходится, запускаю захват на LAN-интерфейсе сервера, например `sudo tcpdump -ni ens18 'src host 192.168.50.20 and dst host 172.30.88.10 and tcp dst port 80'`; имя интерфейса заменяю фактическим. Мне нужно увидеть входящие SYN и рост соответствующего DROP-счётчика без установления TCP-соединения. Так отрицательный сетевой результат получает объяснение. Отсутствие пакетов на сервере не подтверждает восстановление его защиты. Сообщение `Connection refused` тоже требует разбора: ответ мог прислать закрытый сервис.
Затем снова выполняю reload и повторяю проверку после восстановления правил. Между этим reload и пробами Docker не перезапускаю: иначе проверка опять скроет дефект. Мой минимальный регрессионный прогон — три таких цикла и по десять новых внешних попыток после каждого, при работающем localhost. Тридцать неуспешных подключений — выбранный объём проверки, не математическая гарантия. Ожидаемый итог реконструкции «Вектора»: приложение работает локально, прямой LAN-доступ блокируется после каждого reload, запрет endpoint восстанавливается. Это критерий завершения работ, а не выдуманный протокол полученных замеров.
- До reload на 28.3.2: localhost доступен; LAN_IP:18080 и container_IP:80 недоступны.
- После reload на 28.3.2 при воспроизведении дефекта: localhost доступен; прямой container_IP:80 становится доступен.
- После исправления и повторных reload: localhost доступен; оба внешних пути недоступны, блокировка подтверждается правилами и трафиком.
Как я меняю регламент, чтобы ошибка не вернулась
Я привязываю сетевую проверку к событиям: обновлению Engine, reload firewalld, пересозданию Docker-сетей и перезагрузке сервера. В акте приёмки сохраняю версии, адреса источника и назначения, маршрут, снимки правил и результаты разрешённых и запрещённых подключений. Фразы «firewalld active» для этого недостаточно. Контейнерные IP в реальной системе могут измениться, поэтому перед каждой проверкой их получаю заново через inspect. Иначе мониторинг годами проверяет адрес, на котором уже никто не работает.
На системах 2026 года отдельно выясняю backend Docker. Его собственная поддержка nftables появилась в 29.0.0 и в актуальной документации помечена experimental; цепочки DOCKER-USER там нет. Поэтому команды диагностики raw/iptables из нашего стенда нельзя механически переносить на native nftables. Проверка реального подключения остаётся полезной, но структуру правил и пользовательские ограничения нужно читать для выбранного backend. Переключение backend посреди устранения инцидента я считаю неоправданным усложнением. [Docker с nftables](https://docs.docker.com/engine/network/firewall-nftables/).
Если используется IPv6, добавляю отдельные адреса, маршруты и внешние пробы: результат IPv4 на IPv6 не распространяется. Проверяю также доступ из административных VLAN и VPN, если оттуда есть маршрут к серверу. После лабораторного прогона удаляю добавленный маршрут командой `sudo ip route del 172.30.88.10/32 via 192.168.50.10` на проверяющей VM. На косметику вывода `docker ps` можно не тратить время; я предпочитаю потратить его на повторное подключение после того события, которое уже однажды изменило доступ.
Частые вопросы
Почему сканирование LAN-адреса сервера не нашло проблему?
Обход использует IP контейнера и внутренний порт через маршрут на Docker-хост. Проверка только адреса сервера и host port этот путь не охватывает.
Если после исправления curl получает HTTP 403, порт закрыт?
Нет. HTTP-ответ подтверждает установленное соединение. Для порта, запрещённого из LAN, это провал сетевой проверки независимо от авторизации приложения.
Можно ли проверить всё с самого Docker-хоста?
Локально можно проверить приложение и правила, но внешний путь требует отдельного источника. Я использую соседнюю VM и подтверждаю поступление её пакетов на сервер.
Я помогу проверить публикации контейнеров и подготовить регрессионные проверки после изменений firewall. Оставьте заявку в rf-buh на разбор вашей конфигурации и план исправлений.
Бесплатная консультация →

