HTTP-коды прокси и TLS-рукопожатие
Собрал маленький стенд: прокси и бэкенд за ним. Прокси - посредник, который принимает запрос клиента и передаёт его дальше; бэкенд - то самое приложение, ради которого всё затевалось. Направил прокси на порт, где никто не слушает, - получил 502 за 13 миллисекунд. Заставил бэкенд думать 8 секунд при разрешённых двух - получил 504 ровно через 2018 миллисекунд, причём сам бэкенд в это время жив и на другие запросы отвечает. Два кода, две совершенно разные причины.
Стержень: код шлюза говорит, что случилось с бэкендом; защищённое соединение требует и правильного имени, и полной цепочки сертификатов.
// Формулировки: «чем 502 отличается от 504?», «как выбирается сертификат, если сайтов на адресе много?», «в браузере открывается, curl ругается»
502 против 504 и третий случай, который хуже обоих
502 означает, что прокси не смог получить от бэкенда осмысленный ответ: соединение отвергнуто, оборвано или пришёл мусор вместо заголовков. На стенде я направил прокси на порт, где никого нет, и получил 502 за 13 миллисекунд. Идти с этим надо в логи приложения и в его перезапуски: это упавший процесс, убитый рабочий, кончившиеся соединения.
504 означает, что бэкенд не ответил за отведённое прокси время. На стенде я поставил разрешённое время ожидания 2 секунды, а бэкенд отвечал 8: ровно на 2018 миллисекунде прокси отдал 504. Идти с этим надо в долгие запросы, очереди и загрузку. Заметь разницу в диагностике: при 502 бэкенд обычно лежит, при 504 он жив и просто не успевает.
// А теперь третий случай, который в жизни встречается чаще, чем хотелось бы. Я заставил бэкенд ответить заголовками, пообещать 100 байт тела и оборвать связь после четырёх. Прокси не отдал ни 502, ни 504: он честно ждал обещанное, и клиент отвалился по своему пределу через 20 секунд. Мораль: не всякая поломка превращается в код ошибки, и пределы ожидания нужны на каждом участке - у клиента, у прокси, у приложения. Полезно и смотреть, КТО именно отдал код: свой прокси, облачный балансировщик или внешняя сеть доставки содержимого, стоящая перед сайтом, - у каждого свои пределы, и они запросто короче, чем у приложения.
- 502
- бэкенд отверг соединение, оборвал его или ответил мусором
- 504
- бэкенд не ответил за время, отведённое прокси
Из чего складывается время защищённого запроса
Разложил обычный запрос к нашему сайту по стадиям. Поиск адреса по имени 0,0020 секунды, установка соединения 0,0021, установка шифрования 0,0365, первый байт ответа 0,0375. То есть само рукопожатие шифрования съело около 34 миллисекунд - почти всё время запроса, и это на канале, где соединение ставится за две миллисекунды.
Дальше самое полезное. Отправил три запроса подряд по одному соединению: первый занял 48,8 миллисекунды, второй 0,76, третий 0,71. Разница в шестьдесят раз, и вся она - в том, что рукопожатие делается один раз. Именно поэтому соединения переиспользуют, а не открывают на каждый запрос, и поэтому же прокси держит отдельный пул соединений к бэкендам.
// Рядом живёт ещё один незаметный расход. Запрос по незащищённому протоколу наш сервер не обслуживает, а отвечает переадресацией: код 301 и адрес с защищённым протоколом. Значит, клиент, который пришёл по старой ссылке, платит за лишний круг. Поэтому ссылки сразу пишут с защищённым протоколом, а браузеру говорят помнить это правило.
- рукопожатие
- обмен, в котором стороны договариваются о шифровании и проверяют сертификат
- переиспользование соединения
- слать следующие запросы по уже открытому соединению
Имя в рукопожатии и полная цепочка
Заголовок с именем сайта едет уже внутри шифрованного канала, а сертификат нужен раньше. Поэтому имя кладут открытым текстом в самое начало рукопожатия - это расширение SNI (server name indication). Проверил на своём стенде с двумя сайтами на одном адресе и порту: просишь shop.test - сервер предъявляет сертификат shop.test, просишь blog.test - сертификат blog.test, не называешь имени вовсе - получаешь сертификат сайта по умолчанию. Отсюда десятки сайтов на одном адресе, и отсюда же то, что имя сайта видно наблюдателю в сети, даже когда содержимое зашифровано.
Второе - цепочка. Сертификат сайта подписан промежуточным центром, тот - корневым, и только корневой лежит в хранилище доверия у клиента. Построил такую цепочку сам и поднял два сервера: первый отдаёт только сертификат сайта, второй - сертификат вместе с промежуточным. Клиент, который доверяет корню, на первом падает с «unable to get local issuer certificate», а на втором спокойно получает страницу. Цепочку без промежуточного звена просто не из чего собрать.
// Почему тогда браузер открывает такой сайт, а curl падает? Я проверил и это: если дать клиенту промежуточный сертификат заранее, он открывает страницу и на первом сервере. Браузеры как раз держат такой запас и умеют дотянуть недостающее звено по ссылке из самого сертификата. А curl, Java и Python этого не делают. Поэтому «в браузере работает» ничего не доказывает, а сервер обязан отдавать полную цепочку. Проверка одной командой: openssl s_client с ключом showcerts покажет, сколько сертификатов сервер прислал. Наш прод отдаёт четыре.
- SNI
- имя сайта, отправленное открытым текстом в начале рукопожатия
- промежуточный сертификат
- звено между сертификатом сайта и корневым; сервер обязан его отдавать
- хранилище доверия
- список корневых сертификатов, которым клиент верит без вопросов
Как отвечать: «В браузере сайт открывается, curl падает с ошибкой доверия. Что не так?»
Почти наверняка сервер отдаёт только свой сертификат без промежуточного. Клиент строит цепочку от сертификата сайта до корня из своего хранилища, и без среднего звена она не собирается - curl говорит «unable to get local issuer certificate». Браузер это прощает: у него есть запас ранее виденных промежуточных сертификатов и умение дотянуть недостающий по ссылке из самого сертификата. Я специально ставил такой стенд: сервер с одним сертификатом падает у клиента, который знает только корень, и открывается у того, кому промежуточный дали заранее. Лечится тем, что в конфигурации веб-сервера указывают файл с полной цепочкой, а проверяется командой openssl s_client с ключом showcerts - она покажет, сколько сертификатов реально прислал сервер.
Ответ объясняет, почему разные клиенты ведут себя по-разному, и даёт и проверку, и лечение. Проверенный на стенде пример отличает практика от пересказа.
На чём валятся
- −Путают 502 и 504 и ищут причину не в том месте: при 502 бэкенд лежит, при 504 он жив и не успевает.
- −Отдают только сертификат сайта и считают, что проблема в клиенте.
- −Отключают проверку сертификата вместо починки цепочки.
- −Забывают, что предел ожидания у балансировщика может быть короче, чем у приложения.
- −Считают, что защищённое соединение скрывает и имя сайта. Имя едет открытым текстом.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 6, остальные разбираются в тренажёре.
- Прокси отдаёт клиентам то 502, то 504. В чём разница для диагностики?A)502 — ошибка клиента, 504 — ошибка сервера приложенияB)502 отдаёт сам сервис, 504 добавляет балансировщикC)502 — обрыв или мусор от апстрима, 504 — молчаниеD)502 про TLS, 504 про исчерпание пула соединений
показать ответ и разбор
+C)502 — обрыв или мусор от апстрима, 504 — молчание// разбор: 502 Bad Gateway — прокси до апстрима достучался, но получил обрыв или невалидный ответ: приложение упало, воркер умер, апстрим закрыл соединение. 504 Gateway Timeout — апстрим не ответил за отведённое время: висит на запросе к БД, перегружен, залип. Первое ведёт к логам и падениям приложения, второе — к таймаутам, длинным запросам и очередям.
- На одном IP живут десятки HTTPS-сайтов с разными сертификатами. Как сервер понимает, какой сертификат отдать?A)По заголовку Host из первого HTTP-запросаB)По записи PTR обратной зоны для адреса клиентаC)По отдельному порту, выделенному каждому доменуD)По расширению SNI в ClientHello, до шифрования
показать ответ и разбор
+D)По расширению SNI в ClientHello, до шифрования// разбор: Заголовок Host едет уже внутри зашифрованного соединения, а сертификат нужен раньше — потому имя хоста клиент кладёт в SNI открытым текстом в ClientHello. Сервер выбирает по нему сертификат и виртуальный хост. Отсюда два следствия: имя сайта видно наблюдателю в сети, а клиент без поддержки SNI получит сертификат по умолчанию и ошибку имени.
- В браузере сайт открывается, а curl и приложение падают с «unable to get local issuer certificate». Что не так на сервере?A)Сертификат выписан на другое имя, браузер это игнорируетB)Сервер отдаёт только листовой сертификат, без промежуточныхC)Сертификат просрочен, но браузер держит его в кэшеD)Сервер требует клиентский сертификат, а curl его не шлёт
показать ответ и разбор
+B)Сервер отдаёт только листовой сертификат, без промежуточных// разбор: Клиент проверяет цепочку до корня из своего хранилища. Браузеры умеют дотягивать недостающий промежуточный сертификат по ссылке из расширения AIA (или берут из своего кэша), а curl, Java и Python — нет: им нужна полная цепочка от сервера. Лечится сборкой fullchain в конфиге веб-сервера. Проверка: openssl s_client -connect host:443 -showcerts.
- Мониторинг показал всплеск именно 5xx (не 4xx). На чьей стороне искать причину первым делом?A)5xx — это ошибки клиента, значит дело в неверных запросахB)И 4xx, и 5xx означают сетевые обрывы, смотреть надо каналC)5xx — это редиректы, клиент просто ушёл не тудаD)5xx — ошибки сервера, копать у себя; 4xx были бы про запрос клиента
показать ответ и разбор
+D)5xx — ошибки сервера, копать у себя; 4xx были бы про запрос клиента// разбор: Классы статусов HTTP: 4xx — ошибка клиента (плохой запрос, авторизация, not found) — смотреть на вызывающего/вход; 5xx — ошибка сервера (необработанное исключение, упавшая зависимость, таймаут) — смотреть на свой сервис и его зависимости. Всплеск 5xx = сервер сыпется; всплеск 4xx = клиенты шлют плохие запросы (или ты сменил API). Направление разбора разное — разноси дашборды по классам.
- TLS терминируется на балансировщике, дальше идёт http. Приложение зациклилось на редиректе http→https. Почему?A)Сертификат на балансировщике невалиден, браузер ходит по кругуB)Приложение видит http и редиректит; нужен X-Forwarded-ProtoC)Надо включить TLS и на самом приложении, иначе редирект вечныйD)Балансировщик вырезает заголовок Location из ответа сервера
показать ответ и разбор
+B)Приложение видит http и редиректит; нужен X-Forwarded-Proto// разбор: TLS снят на балансировщике, до приложения доходит plain http; приложение, видя «не https», редиректит на https — запрос снова приходит через LB как http → петля. LB должен передавать X-Forwarded-Proto: https, а приложение — доверять этому заголовку при определении схемы. Тогда оно понимает, что исходный запрос уже был https, и перестаёт редиректить.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.