0% прочитано

155 помилок 503, але production працював: як HTTPS default server змінив розслідування

Під час розслідування сплеску трафіку 155 upstream errors спочатку виглядали як проблема доступності основного застосунку. Але error log показав інше: запити потрапляли в окремий virtual host через поведінку HTTPS default server. Це змінило і діагноз, і захист.

24 жовтня 2026 р. 11 хв читанняNginx

Під час розслідування нового traffic spike я побачила в Nginx error log 155 записів upstream connection refused.

Перша гіпотеза була очевидною: можливо, масові requests створили навантаження, через яке основний production application почав відповідати 503.

Це звучало правдоподібно, але виявилося неправильним.

Усі 155 зафіксованих upstream failures належали іншому virtual host. Основний production site у цей час продовжував працювати.

Shared Nginx error log показує помилки Nginx, а не автоматично помилки одного конкретного application. Перед висновком про відключення треба встановити, якому virtual host належить кожен error.

Чому 155 помилок 503 спочатку виглядали серйозно

Час збігався з незвичайним великим трафіком. У тому самому investigation window Nginx бачив сотні запитів від автоматизованих клієнтів.

На фоні цього connection refused природно прочитати як "backend не витримав".

Але кореляція в часі ще не означає кореляцію в причині.

Я перестала рахувати errors і почала читати їх контекст

Один error рядок без контексту легко веде до неправильного висновку
Не зупиняйтеся на: відмовлено у з'єднанні з висхідним потоком Також визначте: ім'я серверазапитуваний хостшлях запитуціль висхідного потокупозначку часуклієнта

Ключовим стало не саме повідомлення connection refused, а те, до якого upstream Nginx намагався підключитися.

Після групування стало видно: усі 155 помилки вели до бекенду іншого сервісу, який тимчасово не слухав свій локальний upstream.

Production Laravel бекенд не був джерелом цих 155 failures.

Наступне питання було складнішим: чому публічні сканери взагалі потрапили в цей віртуальний хост

Сам бекенд був локальним. Але локальний upstream не робить сервіс приватним автоматично.

Якщо Nginx має публічний HTTPS віртуальний хост, який проксить реквести до цього upstream, зовнішній клієнт взаємодіє не з локальним портом напряму, а з Nginx.

Тобто питання приватності треба ставити на публічній точці входу, а не тільки на адресу прив'язки бекенда.

В HTTPS не було явного default_server

Невідомий Host усе одно має кудись потрапити
Надходить HTTPS-запит відоме ім'я хоста?  |  +-- так -> відповідний віртуальний хост  |  +-- ні -> HTTPS-сервер за замовчуванням  Немає явного default_server?  |  vперший відповідний сокет прослуховування може стати сокетом за замовчуванням

У конфігурації не було окремого HTTPS catch-all із default_server.

Першим віртуальний хост для відповідного HTTPS лістенера був саме приватний сервіс. Коли scanner звертався до публічного IP або використовував невідомий hostname, Nginx міг обрати цей server block як default.

Саме тому частина зовнішнього автоматичного трафіку опинилася в сервісі, який я не очікувала бачити в цьому розслідуванні.

Це не означало успішний злам

Важливо не перебільшувати результат.

Я встановила, що публічні запити могли маршрутизуватися до неочікуваного віртуального хоста і що його бекенд у частині випадків був недоступний.

Я не знайшла докази успішного виконання шкідливого коду або компромісу самого сервісу.

Перша зміна - явний універсальний HTTPS

Узагальнений HTTPS дефолтний сервер
server {    listen 443 ssl default_server;     server_name _;     ssl_reject_handshake on;}

Мета цього блоку сервера проста: невідомий HTTPS hostname не повинен випадково успадковувати віртуальний хост, специфічний для програми.

ssl_reject_handshake on дозволяє відхилити handshake ще до маршрутизації запиту у приватний застосунок.

Друга зміна - окремий контроль доступу для приватного сервісу

Узагальнений Basic Auth патерн
location / {    auth_basic "Restricted";    auth_basic_user_file /path/to/htpasswd;     proxy_pass http://private_backend;}

Сервер за замовчуванням вирішує проблему unknown-host routing. Але він не замінює контролю доступу для правильного hostname.

Тому самому сервісу я додала Basic Auth. Тепер навіть клієнт, який знає валідний hostname, спочатку проходить окрему межу автентифікації.

Дефолтний сервер і Basic Auth вирішують різні проблеми

Два незалежні рівні захисту
Невідоме ім'я хоста   |   vHTTPS сервер_за_замовченням  Телефон відхилено  Відоме ім'я приватного хоста   |   v приватний віртуальний хост   |   v

Після зміни я перевірила кожен очікуваний шлях доступу

Перевірка після посилення захисту
Production очікується -> 200 Development очікується -> 401 Приватний сервіс без облікових данихочікується -> 401 Приватний сервіс після автентифікаціїочікується -> відповідь програми Невідоме ім'я хоста HTTPSочікується -> доступ TLS відхилено

Для приватного сервісу після успішної Basic Auth кореневий роут повертав 404. Це було очікувано: автентифікація пройшла, але застосунок не визначав /.

Цей відмінність теж важлива. 401 означає контроль доступу, 404 після автентифікації - уже роутинг застосунку.

Валідація Nginx має бути чистою, не просто успішною

Після першої зміни перевірка конфігурації Nginx проходила, але залишала warning через різні параметри listen 443 ssl http2 для server blocks на одному socket.

Я вирівняла параметри й повторила nginx -t, доки валідація не стала чистою.

Налаштування Production не варто завершувати станом "працює, але warning залишимо".

Зміни конфігурації я робила з бекапом і rollback path

Мінімальний production флоу змін
1. скопіювати поточну конфігурацію2. відредагувати одну межу3. nginx -t4. перезавантажити5. перевірити публічну поведінку6. перевірити захищену поведінку7. зберегти копію відкату, доки не стане стабільною

Це був фікс безпеки, але я не хотіла, щоб сам фікс став outage.

Тому кожна зміна мала rollback copy, валідацію і перевірку очікуваного статус коду після перезавантаження.

Захист сканера не замінює правильну virtual-host межу

До цього на сервері вже працювали path-based правила і Fail2ban, які я описувала в матеріалі про Nginx і Fail2ban для sensitive-path scanners.

Вони добре вирішують питання підозрілих шляхів. Але якщо unknown-host запит уже потрапив у неправильний віртуальний хост, це інша проблема.

Саме тому рівні захисту не варто сприймати як взаємозамінні.

Початковий сигнал трафіку і першопричинавиявилися різними речами

Поштовхом до розслідування був автоматичний трафік, схожий на випадки з попередньому матеріалі про автоматизований трафік із browser-like User-Agent.

Але конкретні 503 не були доказом того, що саме цей трафік "поклав production". Вони вивели мене на окрему слабкість конфігурації маршрутизації віртуального хоста HTTPS.

Це хороший приклад, чому під час розслідування інциденту не можна дозволяти першій правдоподібній історії стати фінальним висновком.

Що я тепер перевіряю, коли бачу upstream помилки у спільних Nginx логах

  • Якому віртуальному хосту належить error?

  • Який upstream реально був недоступний?

  • Чи production застосунок у цей час справді повертав невдачі?

  • Який server block є HTTPS дефолтним для невідомих hostnames?

  • Що відбувається при direct-IP або unknown-host запитах?

  • Чи приватний сервісмає власний контроль доступу?

  • Чи nginx -t проходить без warnings?

  • Чи перевірені всі очікувані результати HTTP/TLS після перезавантаження?

  • Чи є rollback path для зміна конфігурації?

155 errors змінили не доступність, а моє розуміння межі

На початку розслідування 155 upstream невдачі виглядали як можливий симптом production відключення.

Після розбору стало зрозуміло, що основний застосунок працював, а помилки належали іншому віртуальному хосту, який отримував зовнішній трафік через неявну HTTPS дефолтну поведінку сервера.

Виправленням стало не "лікування production", а правильне оформлення межі: явний дефолтний сервер для невідомих хостів і окремий контроль доступу приватного сервісу.

Спільний log показує, що помилка сталася десь у Nginx. Він не доводить, який застосунок впав, доки ми не встановимо контекст.