На одном сервере у нас живут полтора десятка проектов разных команд: сайты на WordPress, старый магазин на другой платформе, новое приложение с поиском и распознаванием картинок. Всё в Docker, всё за обратным прокси, который принимает 80 и 443 и раздаёт запросы по контейнерам. На словах наружу смотрит только прокси. Мы решили проверить это не по конфигам, а снаружи, — и нашли пять дыр, о которых никто не знал.

Это первая часть цикла «Общий сервер: безопасность и восстановление».

Как проверяли

Два независимых взгляда, которые должны совпасть. Снаружи — скан портов с обычной машины в другой сети. Изнутри — список того, что контейнеры публикуют на хост:

# какие контейнеры пробрасывают порты на хост и на какой адрес
docker ps --format '{{.Names}}\t{{.Ports}}' | grep -- '->'

# то же надёжнее — по каждому контейнеру, включая остановленные
for c in $(docker ps -aq); do
  docker inspect -f '{{.Name}} {{range $p, $b := .HostConfig.PortBindings}}{{$p}}→{{range $b}}{{.HostIp}}:{{.HostPort}} {{end}}{{end}}' "$c"
done | grep '→'

Из сорока контейнеров порты на хост публиковали ровно шесть: прокси (80 и 443 — так и задумано) и ещё пять. Внешний скан показал открытыми ровно эти же порты, ни одним больше. Когда два метода сходятся, можно перестать гадать.

Что нашлось

СервисЧто было снаружиЧем грозит
векторная база (поиск похожих картинок)HTTP-API без ключа, чтение, запись и удалениелюбой снесёт или подменит коллекцию, приложение ляжет
PostgreSQLпорт 5432 на всех адресах, в pg_hba.conf — host all all allпостоянный подбор паролей и любая уязвимость до проверки пароля
три приложениясвой голый HTTP-порт мимо проксибез TLS, без общих правил прокси; у одного маршрут через прокси уже был — порт просто лишний
хостfirewall выключен, цепочка DOCKER-USER пустабарьер только у хостера

К векторной базе достаточно было одного запроса без всякой авторизации: GET /collections вернул имя коллекции. В логах приложения с голым портом уже был стук чужого сканера. Это не теория: открытый порт находят за часы.

Почему firewall не спас бы

Первая мысль — «включим firewall, и всё». С Docker это не так. Публикуя порт (ports: "5432:5432"), Docker сам дописывает правила в iptables, в таблицу nat и свою цепочку DOCKER. Пакет на опубликованный порт уходит в контейнер раньше, чем до него доберутся правила ufw или firewalld в цепочке INPUT. Получается, что ufw показывает «deny 5432», а порт открыт.

Опубликованный порт Docker обходит firewall хоста. Закрывать нужно в самом compose (не публиковать или публиковать на 127.0.0.1) либо правилами в цепочке DOCKER-USER — её Docker проверяет до своих.

# запретить доступ к опубликованным портам контейнеров отовсюду, кроме своей сети
iptables -I DOCKER-USER -i eth0 ! -s 203.0.113.0/24 -j DROP
# (правило должно пережить перезагрузку: iptables-persistent / свой юнит)

Но правильнее не публиковать порт вовсе. Контейнерам между собой он не нужен — об этом следующая часть.

Откуда это берётся

Ни одна из дыр не была злым умыслом. Сервер собирали по общему стандарту: образы из реестра, базы внутри сети Docker, наружу — только через прокси. Новые сервисы ставил другой человек, по привычке «как на локальной машине»: ports для всего, что надо открыть хоть кому-то, база с доступом отовсюду «чтобы подключаться из IDE». На ноутбуке разработчика это безобидно, на сервере в интернете — нет.

Когда спросили, зачем базе открытый порт, ответ был «мне нужно к ней подключаться». То есть доступ нужен одному человеку, а открыт он всему интернету. Это разные задачи, и для первой публиковать порт не нужно.

Что сделать на своём сервере

  1. снаружи — скан портов с машины в другой сети (не с самого сервера: изнутри видно всё);
  2. изнутри — docker inspect по всем контейнерам, включая остановленные: остановленный с портом поднимется после перезагрузки;
  3. сравнить два списка. Расхождение — повод разбираться, а не «наверное, хостер закрыл»;
  4. для каждого порта, кроме прокси, ответить на вопрос «кто ходит на него снаружи». Если никто — убрать ports, если человек — SSH-туннель, если сервис из той же сети Docker — по имени контейнера;
  5. базы и хранилища без ключа не держать даже внутри сети: ключ у векторной базы задаётся одной переменной окружения;
  6. после правки — повторить внешний скан. Порт должен стать closed или filtered, а не «наверное закрылся».

Тот же разбор стоит повторять при каждом новом сервисе от новой команды. Дыры такого рода никто не замечает сам: всё работает, жалоб нет, а узнаёшь о них только тогда, когда проверяешь снаружи.

Не уверены, что у вас торчит наружу?

Пришлите вывод docker ps — разберу, что опубликовано лишнее и как закрыть, не сломав связи между сервисами.

· полезно?
АЧ
Алексей Чикун веб-разработчик, команда TRIOHEAD

Делаю сайты и сервисы целиком: от вёрстки до сервера. Пишу о том, что проверил на своих проектах.

Комментарии

// загружаю комментарии…

Комментарии публикуются после проверки. Ссылки и HTML не принимаются.