Files

2 lines
24 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"index":23,"slug":"editorial-2027-05-mechanism-http-tls-guide","title":"Почему HTTPS ошибается в разных местах: карта границ DNS, TCP, TLS и HTTP","excerpt":"Ошибка сертификата, таймаут и 404 могут выглядеть одинаково для пользователя, но требуют разных проверок. Разбираем порядок запроса, доверие к серверу, роль proxy и воспроизводимую запись результата.","contentHtml":"<p>В браузере одна красная страница, в логе клиента другая строка, а инженер уже меняет таймаут или отключает проверку сертификата. Такой ремонт часто начинается раньше факта: запрос мог остановиться на DNS, TCP или TLS и не дойти до HTTP. В обратной ситуации сертификат проверен, но приложение или proxy вернули <code>404</code>, и поиски проблемы в TLS только уводят в сторону.</p><p>У запроса есть наблюдаемая последовательность: имя превращается в адрес, TCP принимает соединение, TLS проверяет защищённый канал и identity сервера, затем HTTP передаёт метод, цель и поля. Диагностика становится воспроизводимой, если на каждой границе задать свой вопрос, записать факт и не переносить вывод на следующий уровень. Эта схема не обещает мгновенно назвать виновника; она сужает место поиска и подсказывает следующий безопасный тест.</p><h2>Один URL — несколько независимых границ</h2><p>Начните с URI, например <code>https://api.example.test/orders</code>. Из него клиент получает схему, имя, порт и путь. DNS отвечает, какой адрес связан с именем. TCP отвечает, удалось ли открыть соединение с адресом и портом. Для HTTPS поверх TCP запускается TLS. Только после успешного TLS клиент отправляет HTTP-сообщение и получает статус, поля и тело.</p><p>Порядок важен не как учебная условность, а как ограничение доказательств. Если имя не разрешилось, вы ещё не проверяли порт. Если TCP не установлен, у приложения нет основания считать запрос полученным. Если TLS остановился на проверке identity, HTTP-заголовки и статус не объясняют отказ. Если статус <code>404</code> уже прочитан, TLS-обмен прошёл для этого соединения, но это ещё не доказательство, что ответ сформировал нужный origin.</p><div class=\"table-scroll\"><table><caption>Что доказывает наблюдение на каждой границе</caption><thead><tr><th scope=\"col\">Граница</th><th scope=\"col\">Наблюдение</th><th scope=\"col\">Допустимый вывод</th><th scope=\"col\">Чего пока нет</th></tr></thead><tbody><tr><td>DNS</td><td>Получен A или AAAA-ответ</td><td>Имя разрешилось в один из адресов</td><td>Доступность порта и соответствие сертификата</td></tr><tr><td>TCP</td><td>Соединение установлено</td><td>Адрес принял соединение на выбранном порту</td><td>Успешная проверка TLS и HTTP-маршрут</td></tr><tr><td>TLS</td><td>Клиент принял защищённый канал</td><td>Проверки identity и доверия прошли для этого клиента</td><td>Нужный handler получил запрос</td></tr><tr><td>HTTP</td><td>Получены статус и поля</td><td>HTTP-обмен состоялся</td><td>Тело создал origin, а не cache или proxy</td></tr><tr><td>Приложение</td><td>Доверенный лог связал request id с handler</td><td>Известен компонент, обработавший вход</td><td>Так же ли настроены другие адреса и регионы</td></tr></tbody></table></div><figure><img src=\"/assets/editorial/2027/http-tls-guide-2027-symptom-boundary-matrix.svg\" alt=\"Матрица симптомов HTTP и TLS: ошибка имени относится к TLS, 404 к HTTP-маршруту, а 503 требует проверки отвечающего компонента.\" loading=\"lazy\" /><figcaption>Симптом указывает на границу проверки, но не всегда на конкретного виновника: источник ответа подтверждается только доверенным логом.</figcaption></figure><p>Доверенный лог здесь важнее знакомого заголовка. Клиент может получить <code>Server: familiar-product</code> или <code>X-Request-Id: local-001</code>, но оба поля могли добавить или изменить по пути. Доказательство источника появляется, когда компонент под вашим контролем создал идентификатор и записал его вместе со временем, маршрутом и результатом.</p><h2>Как читать ошибку без догадки о виновнике</h2><p>Ошибка до HTTP-статуса ограничивает поиск транспортом. Сообщение о несовпадении имени сертификата требует проверить hostname и сертификат; оно не доказывает, что порт недоступен. Отказ TCP требует проверить маршрут, firewall, listener или балансировщик; он не говорит, какой HTTP-путь должен существовать. Таймаут без раздельных фаз оставляет место остановки неизвестным — не подставляйте туда удобную версию.</p><p>HTTP-ошибка уже принадлежит сообщению, которое кто-то сформировал. <code>404 Not Found</code> означает, что получен HTTP-ответ с таким статусом, но не называет внутренний маршрут и не доказывает origin. <code>503 Service Unavailable</code> сообщает о недоступности для данного отвечающего компонента; это может быть proxy, gateway или приложение. Ищите request id и запись того слоя, который действительно отправил ответ клиенту.</p><table><caption>Симптом и первый тест</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Гипотеза с ограниченной уверенностью</th><th scope=\"col\">Первый тест</th><th scope=\"col\">Не делать сразу</th></tr></thead><tbody><tr><td>Нет статуса, ошибка сертификата</td><td>Не прошла проверка имени, цепочки или срока</td><td>Сверить hostname, SAN, trust store и срок действия</td><td>Не добавлять <code>Host</code> и не включать <code>--insecure</code> постоянно</td></tr><tr><td>Имя разрешилось, connect отклонён</td><td>Порт, маршрут или listener не принимает соединение</td><td>Сохранить адрес, порт и тип TCP-ошибки</td><td>Не проверять URL и бизнес-логику как первопричину</td></tr><tr><td>TLS успешен, получен <code>404</code></td><td>Путь, метод, authority или версия маршрута не совпали</td><td>Сверить запрос с логом входного proxy и известным endpoint</td><td>Не перевыпускать сертификат без нового факта</td></tr><tr><td>Получен <code>503</code></td><td>Отвечающий компонент не обслуживает запрос или не дождался upstream</td><td>Сопоставить слой, время ответа, <code>Via</code> и upstream-лог</td><td>Не увеличивать timeout и не повторять запись вслепую</td></tr><tr><td>Повтор даёт другой ответ</td><td>Кэш, балансировка, редирект или меняющееся состояние</td><td>Сравнить адрес, <code>Age</code>, <code>ETag</code>, <code>Cache-Control</code> и время</td><td>Не объединять ответы разных попыток в одну причину</td></tr></tbody></table><h2>TLS: что именно проверяет клиент</h2><p>Для HTTPS недостаточно сказать «сертификат действующий». Клиент строит reference identity из имени или IP в URI и сопоставляет её с именами в сертификате. Затем он проверяет цепочку к доверенному anchor и период действия. Эти проверки отвечают на разные вопросы: сертификат может быть выдан правильным центром, но не для этого имени; имя может совпасть, но цепочка не доверена локальному клиенту; оба условия могут пройти, а срок — закончиться.</p><p>Подключение к IP вместо имени часто меняет проверяемую identity. Если сертификат выпущен для DNS-имени, сам факт, что IP указывает на тот же сервер, не делает IP подходящим именем. HTTP-поле <code>Host</code> не исправляет ошибку задним числом: оно появляется на уровне HTTP, после установления TLS. В HTTP/2 и HTTP/3 роль имени и порта в запросе обычно представляет <code>:authority</code>; при диагностике учитывайте фактическую версию протокола.</p><p>Флаг <code>--insecure</code> или его аналог может быть полезен в коротком разрешённом эксперименте: он показывает, что удалённая сторона способна отправить данные без обычной проверки identity. Но такой ответ не доказывает безопасность канала и не является исправлением. Результат эксперимента нужно явно пометить как полученный с отключённой проверкой и повторить обычным клиентом после исправления доверия.</p><h2>HTTP, proxy и кэш: ответ не всегда пришёл от origin</h2><p>HTTP-маршрут выбирается с учётом цели запроса. В HTTP/1.1 это видно через <code>Host</code>, а в HTTP/2 и HTTP/3 — через <code>:authority</code>. Один адрес может обслуживать несколько имён, поэтому несоответствие authority, пути или метода способно привести к <code>404</code> при полностью рабочем TLS. Уточняйте, какой компонент принимал соединение и какое значение он использовал для маршрутизации.</p><p>Посредник может завершить запрос сам, передать его дальше или вернуть результат из кэша. Поле <code>Via</code> по стандарту помогает увидеть промежуточные протоколы и получателей, но оно не является криптографической подписью тела. <code>Age</code> и валидаторы вроде <code>ETag</code> помогают заметить кэширование, а <code>Cache-Control</code> описывает правила хранения и повторного использования. Это признаки цепочки, а не самостоятельное доказательство конкретного origin.</p><p>Для расследования сохраняйте метод, нормализованный путь, authority, порт, момент, длительность, статус, размер тела и безопасный request id. Секреты и персональные данные в такой записи не нужны: удалите <code>Authorization</code>, cookie и чувствительные query-параметры. Если доступен только внешний ответ, напишите «источник не установлен». Такая формулировка полезнее уверенного, но неподтверждённого назначения владельца.</p><h2>Воспроизводимый пример: сначала HTTP, затем отрицательный путь</h2><p>Пример запускается локально и намеренно не моделирует TLS. Он показывает, что статус относится к HTTP-маршруту, а вручную заданный request id не подтверждает доверенное происхождение. Сохраните код в <code>check-http.mjs</code> и запустите на Node.js с поддержкой встроенного <code>fetch</code>.</p><pre><code>import { createServer } from 'node:http';\n\nconst server = createServer((request, response) =&gt; {\n const knownRoute = request.method === 'GET' &amp;&amp; request.url === '/health';\n response.writeHead(knownRoute ? 200 : 404, {\n 'content-type': 'application/json; charset=utf-8',\n 'x-request-id': 'local-001',\n });\n response.end(JSON.stringify({\n method: request.method,\n path: request.url,\n route: knownRoute ? 'health' : 'missing',\n }));\n});\n\nserver.listen(0, '127.0.0.1', async () =&gt; {\n const { port } = server.address();\n for (const path of ['/health', '/missing']) {\n const response = await fetch('http://127.0.0.1:' + port + path);\n console.log(response.status, await response.json());\n }\n server.close();\n});</code></pre><p>Ожидаемый результат — <code>200</code> для <code>GET /health</code> и <code>404</code> для <code>GET /missing</code>. В обоих ответах будет <code>local-001</code>, потому что пример присваивает его сервером. Это воспроизводит локальный HTTP-контракт, но не доказывает, что аналогичное поле в production создано доверенным входным компонентом.</p><p>Отрицательный тест: замените путь на <code>/health?token=demo</code>. Сервер вернёт <code>404</code>, потому что демонстрационный контракт сравнивает полный URL. Это не универсальное правило маршрутизации, а специально видимое условие примера. После теста не переносите его буквально в приложение: реальный роутер может отдельно разбирать path и query. Ценность эксперимента в том, что изменение одного условия меняет наблюдаемый результат.</p><h2>Порядок диагностики</h2><ol><li>Запишите схему, hostname, порт, метод, безопасный путь, время и режим proxy. Замаскируйте токены, cookie и персональные параметры.</li><li>Проверьте DNS и сохраните выбранный адрес, тип записи и момент. Если адресов несколько, повторите наблюдение для каждого значимого маршрута.</li><li>Проверьте TCP connect к нужному порту. При отказе сначала разбирайте сеть, listener, firewall или балансировщик.</li><li>Для HTTPS обычным клиентом проверьте hostname или IP identity, SAN, цепочку доверия и срок действия. Запишите конкретное свойство, которое не прошло.</li><li>Только после успешного TLS снимите метод, path, authority, статус и ограниченный набор заголовков. Ошибке до TLS не приписывайте HTTP-статус.</li><li>Установите отвечающий слой: свяжите request id с доверенным логом proxy, gateway или handler. Если связи нет, оставьте источник ответа неизвестным.</li><li>Проверьте <code>Via</code>, <code>Age</code>, <code>ETag</code>, <code>Cache-Control</code> и длительность, если подозреваете intermediary или cache. Сравните холодный и повторный запрос.</li><li>Сформулируйте одну подтверждённую причину и один следующий тест. Для <code>POST</code> сначала проверьте идемпотентность или ключ дедупликации; ответ <code>503</code> сам по себе не делает повтор безопасным.</li></ol><h2>Ограничения применимости</h2><p>Модель описывает порядок доказательств, а не конкретную реализацию сети. Она не заменяет packet capture, распределённую трассировку, настройки CDN, корпоративного proxy, DNS-кэш, региональную балансировку или наблюдение внутри закрытого сегмента. У клиента могут быть скрыты отдельные фазы, поэтому итоговый timeout нельзя разложить на DNS, TCP и TLS без дополнительного измерения.</p><p>Локальный сервер не подтверждает работу реального origin, сертификата, балансировщика или кэша. Поле <code>Via</code> может быть скрыто политикой, а request id от внешней стороны может отсутствовать или быть перезаписан. Сопоставляйте только те записи, которым доверяете в своей цепочке. Нельзя переносить учебный порт, hostname, заголовок и ожидаемый статус в production без отдельного контракта.</p><p>Для операции, меняющей заказ, деньги, подписку или иной ресурс, сетевой сбой оставляет результат неопределённым: сервер мог применить действие и потерять ответ. RFC различает идемпотентные методы и остальные, но бизнес-операция может иметь дополнительные побочные эффекты. Без ключа операции, проверки состояния или явного правила сервиса безопаснее остановить автоматический повтор и передать решение владельцу контракта.</p><h2>Критерий готовности</h2><p>Диагностика готова, если для одного endpoint есть две очищенные записи: успешная и ошибочная. В каждой указаны время, hostname, порт, метод, безопасный путь, этап, статус или текст ошибки, длительность и ожидаемый следующий тест. Для HTTPS отдельно зафиксированы identity, цепочка и срок; для HTTP — authority, маршрут, источник ответа и признаки кэша, если они доступны.</p><p>Исправление считается подтверждённым после изменения одного условия и повторного запуска с обычной проверкой сертификата. Ожидаемый результат должен быть сформулирован заранее: например, известный маршрут возвращает <code>200</code>, отсутствующий — <code>404</code>, а ошибка до TLS не получает выдуманный HTTP-статус. Если логи не связывают ответ с доверенным компонентом, честный результат проверки — «источник не установлен», а не имя предполагаемого сервиса.</p><h2>Проверяемые источники</h2><ul><li><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html\" target=\"_blank\" rel=\"noopener noreferrer\">IETF RFC 9110: HTTP Semantics</a> — схема <code>https</code>, проверка identity, <code>Host</code> и <code>:authority</code>, посредники, кэш, статусы и свойства методов.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc8446.html\" target=\"_blank\" rel=\"noopener noreferrer\">IETF RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3</a> — формат TLS 1.3, сообщения handshake, сертификаты и ограничения отключённой аутентификации.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc6125.html\" target=\"_blank\" rel=\"noopener noreferrer\">IETF RFC 6125: Representation and Verification of Domain-Based Application Service Identity</a> — правила сопоставления имени сервиса с идентичностью сертификата.</li></ul>"}