MAATRIX / Блог / Ежемесячная сверка портов: как заметить сервис, который открылся сам

Ежемесячная сверка портов: как заметить сервис, который открылся сам

MAATRIX

Вы обновили пакет, поставили новую версию мониторингового агента или подняли тестовую базу — и через это действие сервер начал слушать порт, о котором вы не просили. Никто не открывал его специально: так сработал дефолтный конфиг, автозапуск сервиса или Docker, который переписал iptables в обход вашего firewall. Разовая проверка ловит такие вещи только если её сделать в правильный день. Регулярная сверка с эталонным списком портов ловит их всегда — и превращает «а что это вообще такое открыто» в рутинную десятиминутную задачу раз в месяц.

Зачем нужна регулярная сверка, а не разовая проверка

Разовый аудит открытых портов — полезная вещь, но у него есть слепое пятно: он показывает состояние на один конкретный момент. Через неделю после проверки вы обновили СУБД, и новая версия по умолчанию слушает 0.0.0.0 вместо 127.0.0.1. Через месяц коллега поднял Redis для отладки кэша и забыл его остановить. Ещё через две недели Docker-контейнер с -p 6379:6379 добавил правило в iptables напрямую, в обход UFW — и порт открылся, даже если вы уверены, что закрыли всё firewall'ом. Мы разбирали этот конкретный сценарий отдельно в статье про Docker, который пробивает firewall — механика там неочевидная и стоит отдельного чтения.

Дело не в разовой ошибке, а в том, что состояние сервера дрейфует. Каждое обновление пакета, каждая новая версия ПО, каждый временный сервис «на пять минут» немного меняет то, что слушает сеть. По отдельности эти изменения незаметны. Накопившись за несколько месяцев, они превращают сервер, который вы когда-то настроили аккуратно, в набор дверей, часть которых уже никто не помнит.

Есть и вторая причина: боты сканируют весь диапазон IPv4 на популярные порты постоянно, счёт идёт на часы, а не на недели. Разрыв между моментом, когда порт открылся случайно, и моментом, когда об этом узнаете вы, — это и есть окно риска. Задача регламента — сделать это окно предсказуемым: не «когда-нибудь заметим», а «максимум через месяц узнаем и разберёмся».

Практический подход строится на трёх вещах: у вас есть зафиксированный список того, что должно быть открыто; вы регулярно снимаете фактический список; вы автоматически сравниваете одно с другим и реагируете на разницу. Дальше — как это сделать без лишней бюрократии.

Эталонный список: что в нём должно быть и как составить первую версию

Эталонный список (baseline) — это не документация на пятьдесят строк, а простая таблица: какой порт, по какому протоколу, для какого сервиса, кто и когда его открыл и почему он вообще должен быть открыт наружу. Смысл не в полноте, а в том, чтобы через полгода не гадать, зачем открыт порт 8443.

Минимальный формат — обычный текстовый файл, который проще всего держать рядом с остальной документацией сервера (если у вас уже есть паспорт сервера — эталонный список портов логично вести как раздел в нём же, а не отдельным файлом, который забудут открыть):

# ports-baseline.txt — эталонный список открытых портов
# proto  port    service         scope        comment
tcp      22      sshd            external     управление, ограничено по IP в ufw
tcp      443     nginx           external     сайт и API
tcp      80      nginx           external     редирект на 443
tcp      5432    postgresql      internal     слушает только 127.0.0.1, наружу закрыт
udp      51820   wireguard       external     VPN для команды
tcp      9090    prometheus      internal     доступен только из VPN-подсети

Колонка scope здесь ключевая: она разделяет порты, которые действительно должны быть видны снаружи (SSH, веб-сервер, VPN), и порты, которые слушают локально или в закрытой сети и не должны отвечать на внешние запросы. Именно эта колонка потом определяет, чем вы сверяете каждую строку — внутренним срезом или внешним сканом.

Первую версию списка составляют не с нуля в уме, а по факту: снимаете текущий срез портов (команды — в следующем разделе), проходите по каждому и для каждого сознательно решаете — нужен, не нужен, закрыть. Это ровно тот же процесс, что описан в статье про проверку и закрытие открытых портов: сначала разбираетесь один раз, что где открыто и зачем, закрываете лишнее, а результат — то, что осталось и признано нужным, — фиксируете как эталон. Дальше эталонный список редактируется только осознанно: добавили сервис — добавили строку в тот же день, а не постфактум при следующей сверке.

Храните файл там же, где остальную инфраструктурную документацию — в приватном git-репозитории вместе с конфигами, или в том же месте, где паспорт сервера. Важно, чтобы у изменений была история: git log ports-baseline.txt через полгода честно покажет, кто и когда добавил строку про порт 8080 и что в комментарии написано «временно для отладки API».

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Снимаем текущий срез портов изнутри сервера

Базовый инструмент — ss, современная замена netstat. Он показывает, что реально слушает сокеты на сервере, с привязкой к процессу:

ss -tulpn

Разбор флагов: -t — TCP-сокеты, -u — UDP, -l — только слушающие (listening), -p — имя процесса и PID, -n — не резолвить имена в DNS/сервисы (быстрее и точнее). Типичный вывод:

Netid  State   Recv-Q  Send-Q   Local Address:Port   Peer Address:Port  Process
tcp    LISTEN  0       128      0.0.0.0:22            0.0.0.0:*          users:(("sshd",pid=812,fd=3))
tcp    LISTEN  0       511      0.0.0.0:443            0.0.0.0:*          users:(("nginx",pid=1204,fd=6))
tcp    LISTEN  0       128      127.0.0.1:5432          0.0.0.0:*          users:(("postgres",pid=990,fd=5))

Обратите внимание на адрес перед портом: 0.0.0.0 значит «слушает на всех интерфейсах, включая внешний», 127.0.0.1 — только локально, снаружи такой порт не достучаться напрямую (если, конечно, его не пробросил Docker или NAT в обход обычного стека). Именно строки с 0.0.0.0 и внешним IP — кандидаты на первоочередную проверку.

Если нужен голый список портов для скрипта сверки, вывод удобно причесать:

ss -tulnH | awk '{print $1, $5}' | rev | cut -d: -f1 | rev | sort -un

Альтернатива ss, если её почему-то нет в системе или вы привыкли к другому выводу — lsof:

lsof -i -P -n | grep LISTEN

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

Отдельно стоит свериться с правилами firewall — список слушающих сокетов не равен списку того, что реально достижимо снаружи:

ufw status numbered
# или
iptables -L -n -v --line-numbers

Если у вас есть Docker, обязательно проверьте таблицу NAT отдельно — Docker добавляет туда свои правила напрямую, и они могут разрешать доступ к порту, который в UFW выглядит закрытым:

iptables -t nat -L DOCKER -n

Взгляд снаружи: nmap с другого хоста

Внутренний срез отвечает на вопрос «что слушает сокеты», но не на вопрос «что реально достижимо из интернета». Это разные вещи: облачный firewall провайдера, iptables на самом сервере, правила Docker и NAT — каждый слой может по-своему исказить картину. Единственный способ увидеть сервер так, как его видит атакующий, — просканировать его снаружи, с другой машины.

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

nmap -Pn -p- --open -oN scan-$(date +%Y-%m).txt 203.0.113.10

Разбор: -Pn — не пинговать перед сканом (некоторые провайдеры блокируют ICMP, и без этого флага nmap решит, что хост недоступен, и не станет сканировать), -p- — все 65535 портов, а не только популярные, --open — показать только открытые, -oN — сохранить результат в файл с меткой месяца, чтобы было с чем сравнивать в следующий раз. Полный скан всех портов занимает заметно дольше, чем скан top-1000 (nmap без -p- использует именно его), но для ежемесячной сверки это оправданная плата — забытый сервис почти никогда не сидит на популярном порту.

Если скорость важна, а полный диапазон гоняете реже, разумный компромиссный вариант — TCP SYN-скан с ограничением скорости (требует root на сканирующей машине, зато заметно быстрее обычного TCP-connect скана):

nmap -sS -Pn -p- --min-rate 1000 --open <ip>

Для UDP-портов (VPN на WireGuard/OpenVPN и подобные протоколы) отдельно нужен -sU — он на порядок медленнее TCP-скана, поэтому его чаще гоняют по конкретному списку известных UDP-портов, а не по всему диапазону:

nmap -sU -Pn -p 51820,123,53 <ip>

Опционально — определение версии сервиса на найденных портах, чтобы быстро понять, что именно откликается на неожиданном номере:

nmap -sV -Pn -p 8080,9200 <ip>

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

Автоматизация: скрипт сверки и запуск раз в месяц

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

Базовая схема скрипта: снять текущий список портов, отсортировать, сравнить с эталоном через comm или diff, при расхождении — отправить уведомление.

#!/usr/bin/env bash
# /opt/scripts/port-diff.sh — ежемесячная сверка портов с эталоном

BASELINE="/opt/server-docs/ports-baseline-numbers.txt"   # только номера портов, по одному на строку, отсортированы
CURRENT="/tmp/ports-current-$(date +%F).txt"

ss -tulnH | awk '{print $1"/"$5}' | rev | cut -d/ -f1 | rev | sort -un > "$CURRENT"

NEW_PORTS=$(comm -13 "$BASELINE" "$CURRENT")

if [ -n "$NEW_PORTS" ]; then
    MESSAGE="Сверка портов $(hostname) $(date +%F): найдены порты, которых нет в эталоне:
$NEW_PORTS"
    echo "$MESSAGE" | mail -s "Port drift: $(hostname)" ops@example.com
    # или через Telegram-бота, если он уже используется для алертов:
    # curl -s -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
    #   -d chat_id="${TG_CHAT_ID}" -d text="$MESSAGE"
fi

Здесь comm -13 — ключевая команда: она сравнивает два отсортированных файла и выводит строки, которые есть только во втором файле (флаг -1 подавляет строки, уникальные для первого, -3 — общие строки, остаётся только «новое в текущем срезе, чего нет в эталоне»). Это именно то, что нужно: не полный diff с кучей совпадений, а чистый список отклонений.

Запуск — через cron раз в месяц, в спокойное время:

# crontab -e
0 4 1 * * /opt/scripts/port-diff.sh >> /var/log/port-diff.log 2>&1

Если предпочитаете systemd timer вместо cron — тот же принцип, юнит типа oneshot и таймер с OnCalendar=monthly. Разницы в результате нет, выбирайте то, что уже используется у вас для остальных плановых задач.

Отдельно стоит завести такую же сверку для внешнего скана nmap — она тяжелее по времени выполнения, поэтому её разумно запускать не в общем скрипте, а отдельной задачей, и сравнивать не с той же числовой базой, а с сохранённым файлом прошлого скана (diff scan-2026-07.txt scan-2026-08.txt).

Что делать при расхождении: три сценария реакции

Сверка нашла порт, которого нет в эталоне. Дальше — не паника, а короткий разбор по трём веткам.

Сценарий 1 — легитимный сервис, который забыли занести в эталон. Кто-то из команды осознанно поднял новый сервис (например, добавили Prometheus exporter или второй домен на другом порту), но не обновил baseline. Решение: проверить с автором изменения, что порт действительно нужен и в каком scope (внешний/внутренний), закрыть его firewall'ом до нужного уровня доступа и добавить строку в эталон день в день, с датой и обоснованием.

Сценарий 2 — забытый тестовый или отладочный сервис. Классика — python3 -m http.server, Redis без пароля для локальной отладки, панель управления, оставленная открытой «на пару дней». Решение: остановить процесс, закрыть порт правилом firewall, убедиться, что автозапуска (systemd unit, cron, docker-compose с restart: always) для него не осталось — иначе он вернётся после следующей перезагрузки.

Сценарий 3 — порт, который никто не признаёт своим. Никто в команде не открывал его сознательно, процесс на этом порту не опознаётся или выглядит подозрительно (нетипичное имя, короткое время работы, странный PID). Это повод остановиться и разобраться серьёзнее: посмотреть, какой бинарник и с какими правами запущен процесс (ps aux | grep <pid>, ls -la /proc/<pid>/exe), проверить логи авторизации за период, когда порт мог появиться, и рассматривать это как потенциальный инцидент, а не как рутинную находку, пока не убедитесь в обратном. О том, как выглядит история со взломом именно через забытый порт, мы разбирали на конкретном случае в статье сервер взломали через забытый порт — там видно, как небольшая невнимательность превращается в серьёзную проблему, если её не заметить вовремя.

Общее правило для всех трёх сценариев: расхождение закрывается либо действием (порт закрыт, сервис остановлен), либо осознанным изменением эталона (порт добавлен с обоснованием) — но никогда не игнорируется молча. Если сверка находит один и тот же «лишний» порт из месяца в месяц без решения — это верный признак, что регламент существует только на бумаге.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Насколько глубоко нужен nmap -p- каждый месяц, если это медленно?

Если полный скан всех 65535 портов занимает неудобно много времени, чередуйте: раз в месяц — быстрый скан top-1000 (nmap без -p-), раз в квартал — полный -p-. Отказываться от полного скана совсем не стоит: забытые сервисы редко сидят на популярных портах.

Обязательно ли сканировать снаружи, если и так есть ss изнутри?

Да, если важна защита от сценария с Docker или другим механизмом, который меняет iptables в обход основного firewall, — внутренний срез в таком случае ничего не покажет, а снаружи порт уже открыт. Внешний скан — единственный способ поймать именно этот класс расхождений.

Что делать с динамическими портами (например, диапазон для контейнеров)?

Заносите в эталон не конкретный номер, а диапазон и правило («TCP 30000-31000 — диапазон контейнеров, норма») и исключайте его из сравнения в скрипте отдельным условием, а не перечисляйте все номера построчно.

Раз в месяц — это точно достаточно?

Для большинства небольших и средних серверов да. Если сервер часто меняется — много деплоев, временных сервисов, несколько человек с доступом — сдвиньте частоту до двух недель или встройте лёгкую версию проверки (ss без внешнего nmap) в уже существующий еженедельный регламент обслуживания сервера.

Нужно ли пугаться, если сверка находит расхождение в первый же месяц?

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

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →