Задача выглядела тривиально: один VPS, три домена, несколько веб-сервисов и один нестандартный сервис, которому нужен тот же 443. На деле это превратилось в многослойную схему, где каждый слой пришлось объяснять следующему.
Начал с базы: Nginx как reverse proxy, отдельные vhost’ы, сертификаты Let’s Encrypt, автоматический renewal через certbot. Всё стандартно, пока не появился второй listener на 443. Два процесса не могут слушать один порт, и тут в игру вступает stream-модуль Nginx. Он работает на TCP-уровне, читает SNI через ssl_preread и маршрутизирует трафик по имени без расшифровки TLS. Это дало возможность повесить на 443 «умный» роутер, который разводит соединения по бэкендам в зависимости от SNI.
Дальше — внутренний сервис, который должен был работать на том же порту, но не пересекаться с веб-частью. Он был посажен на localhost, а снаружи до него доходил только через stream-роутер. Чтобы Nginx и внутренние приложения корректно видели реальный IP клиента, пришлось настроить PROXY protocol: stream-слой добавляет заголовок, а http-слой его читает через real_ip_header. Без этого все клиенты выглядели как 127.0.0.1, и логи теряли смысл.
Отдельная история — fallback. Когда основной протокол не опознан, соединение должно уходить на другой бэкенд. Но fallback в таких схемах работает не на всех транспортах: XHTTP, например, не поддерживает стандартный fallback, потому что поток уже разобран как HTTP. Это ограничение пришлось учитывать при проектировании и выбирать транспорт, который умеет корректно уступать соединение.
Параллельно — мониторинг. Сбор статистики через API, хранение снимков в PostgreSQL, агрегация окон за сутки и месяц, отдельный дашборд, который читает готовые агрегаты и отдаёт их через FastAPI. Здесь важным оказалось разделить ответственность: сборщик пишет сырые данные, агрегатор считает окна, API только отдаёт результат. Иначе получается каша из дублирующихся запросов и расходящихся цифр.
Ещё один слой — безопасность. Firewall с белым списком по IP, rate limiting на уровне Nginx, отдельный сервис для управления правилами через Telegram. Ключевой момент: правило на 22/tcp должно быть привязано к конкретному источнику, иначе сканеры видят порт как открытый. С whitelist’ом UFW возвращает filtered, и это уже другая история для внешнего наблюдателя.
Когда схема собралась, оказалось, что она держится на нескольких небольших, но принципиальных решениях: разнести слои по портам, явно передавать реальный IP, не смешивать fallback с транспортами, которые его не поддерживают, и держать статистику в одном источнике истины. После этого сервер перестал быть набором сервисов и стал системой, которую можно отлаживать по слоям.