8 lines
19 KiB
JSON
8 lines
19 KiB
JSON
{
|
||
"index": 278,
|
||
"slug": "editorial-2020-04-mechanism-reverse-proxy",
|
||
"title": "Reverse proxy под капотом: два соединения, заголовки и таймауты",
|
||
"excerpt": "Приложение видит не внешний запрос, а новый upstream-hop. Разбираем, как proxy меняет Host, forwarded-заголовки и время ожидания, и как проверить границу без догадок.",
|
||
"contentHtml": "<p>Проблема часто выглядит как ошибка приложения. Пользователь открыл HTTPS, а код строит ссылку с http. В журнале все посетители имеют один адрес. После добавления proxy часть запросов заканчивается 502 или 504. Цена ошибки — не только один неудачный ответ. Неверная схема ломает редиректы и cookie. Ошибка в адресе клиента ломает лимиты, аудит и расследование. Слишком большой таймаут дольше держит соединения и маскирует зависший upstream.</p>\n<p>Тезис статьи простой: reverse proxy не является прозрачным проводом. Он принимает внешний HTTP-запрос, завершает один hop и создаёт новый запрос к upstream. На новой границе меняются peer address, часть заголовков, момент подключения и правила ожидания. Поэтому приложение нужно настраивать не на «оригинальный запрос вообще», а на явный договор: какие поля proxy передаёт, откуда они пришли, чему приложение доверяет и какой сигнал подтверждает каждый вывод.</p>\n<h2>Механизм: у одного запроса есть два hop-а</h2>\n<p>Клиент устанавливает соединение с proxy. Proxy выбирает <code>server</code> и <code>location</code>, проверяет маршрут, может завершить TLS, изменить URI, добавить заголовки и буферизовать тело. Затем proxy устанавливает отдельное соединение с upstream. Для приложения непосредственным соседом становится proxy, а не браузер. Это нормально. Ошибка возникает, когда код принимает внутренний peer или клиентский header за внешний факт без проверки границы.</p>\n<p>Схема работает так же. Если TLS завершается на Nginx, внешний hop может быть HTTPS, а соединение Nginx с приложением — HTTP. Значение <code>$scheme</code> на этом Nginx описывает именно вход в него. Если TLS завершился раньше, оно может не совпасть со схемой, которую видел клиент. В таком случае нужен один доверенный источник исходной схемы. Нельзя позволять приложению выбирать между несколькими заголовками по ситуации.</p>\n<p>То же относится к адресу. <code>$remote_addr</code> на Nginx обозначает непосредственную удалённую сторону входного соединения. Если перед Nginx стоит балансировщик, это может быть адрес балансировщика. <code>X-Forwarded-For</code> может содержать цепочку, которую сформировал предыдущий proxy или прислал клиент. Само имя заголовка не делает значение достоверным.</p>\n<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>Приложение строит ссылку с <code>http</code></td><td>TLS завершился на proxy, а приложение не получило согласованный признак схемы</td><td>Сверить место TLS termination, <code>$scheme</code> и поле, которое читает приложение</td><td>Оставить один доверенный forwarded-сигнал и закрыть прямой обход приложения</td></tr><tr><td>Все пользователи имеют один адрес</td><td>Приложение видит peer proxy или неверно разбирает forwarded-цепочку</td><td>Нарисовать hop-ы и проверить доверенные источники до включения realip</td><td>Задать список доверенных proxy; не брать первый элемент заголовка вслепую</td></tr><tr><td>502 появился после изменения маршрута</td><td>Неверный <code>proxy_pass</code>, URI, порт или недоступный upstream</td><td>Проверить итоговый <code>location</code>, адрес upstream и его status</td><td>Исправить маршрут; не увеличивать <code>proxy_read_timeout</code></td></tr><tr><td>504 появляется только на одном endpoint</td><td>Proxy не получил следующий байт ответа в пределах read timeout</td><td>Сопоставить <code>request_time</code>, upstream-времена и безопасную запись приложения</td><td>Найти паузу в upstream или изменить лимит только для этого типа endpoint</td></tr><tr><td>Потоковый ответ обрывается при долгой паузе</td><td><code>proxy_read_timeout</code> ограничивает паузу между чтениями, а не всю передачу</td><td>Измерить интервалы между частями ответа</td><td>Выделить отдельную policy для streaming; не переносить общий лимит на другие маршруты</td></tr></tbody></table>\n<p>Таблица отделяет наблюдение от диагноза. Status 504 говорит о результате ожидания proxy, но не называет базу, GC, внешний API или код обработчика. Status 502 говорит о проблеме на пути к корректному upstream-ответу, но не выбирает между маршрутом, портом и самим процессом. Один сигнал даёт ветку проверки, а не готовое объяснение.</p>\n<h2>Учебная конфигурация границы</h2>\n<p>Ниже приведён минимальный пример для изолированного стенда. Имена, адрес <code>127.0.0.1</code> и значения таймаутов учебные. Пример показывает места договора, а не готовую production-конфигурацию.</p>\n<pre><code>upstream app_backend {\n server 127.0.0.1:3000;\n}\n\nserver {\n listen 8080;\n server_name _;\n\n location / {\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_set_header Connection "";\n\n proxy_connect_timeout 3s;\n proxy_send_timeout 10s;\n proxy_read_timeout 15s;\n proxy_pass http://app_backend;\n }\n}</code></pre>\n<p><code>proxy_set_header Host $host</code> задаёт значение явно. В этом примере <code>X-Real-IP $remote_addr</code> передаёт приложению адрес непосредственного peer текущего Nginx; он не становится адресом клиента сам по себе. <code>X-Forwarded-For</code> расширяет цепочку, а не доказывает, что каждый её элемент заслуживает доверия. <code>X-Forwarded-Proto $scheme</code> описывает схему входного hop-а именно этого Nginx. Если перед ним есть другой TLS-терминатор, контракт должен учитывать его. Нельзя копировать строку <code>https</code> только потому, что внешняя страница открывается по HTTPS.</p>\n<p>Таймауты отвечают на разные паузы. <code>proxy_connect_timeout</code> относится к установлению соединения с upstream. <code>proxy_send_timeout</code> относится к последовательным операциям записи запроса. <code>proxy_read_timeout</code> относится к паузе между последовательными операциями чтения ответа. Поэтому длительность ответа и read timeout — разные величины. Streaming может длиться дольше лимита, если upstream регулярно отправляет данные. Ответ без следующего байта может оборваться раньше, чем команда ожидает.</p>\n<figure><img src=\"/assets/editorial/2020/reverse-proxy-header-boundary-2020.svg\" alt=\"Схема границы reverse proxy: внешний запрос приходит в Nginx, proxy формирует Host и forwarded-заголовки для отдельного upstream-hop, приложение читает только согласованные поля, а access-log связывает наблюдения\" loading=\"lazy\" /><figcaption>Header становится полезным сигналом только вместе с источником и правилом доверия. Строка без границы не доказывает ни схему, ни адрес клиента.</figcaption></figure>\n<h2>Как связать два hop-а в журнале</h2>\n<p>Для первого разбора достаточно access-log с URI, итоговым status, <code>$request_time</code>, адресом upstream, его status и временами подключения, получения заголовков и ответа. Такой формат отвечает на узкий вопрос: proxy не подключился к upstream или подключился, но не дождался заголовков. Не нужно записывать cookie, authorization, тело запроса или полный query string. Лишние данные не делают гипотезу точнее.</p>\n<pre><code>log_format proxy_boundary "$request_method $uri status=$status request=$request_time upstream=$upstream_addr upstream_status=$upstream_status\n connect=$upstream_connect_time header=$upstream_header_time response=$upstream_response_time";\n\naccess_log /path/to/proxy-boundary.log proxy_boundary;</code></pre>\n<p>Синтетическая строка ниже показывает, как читать поля. Она учебная и не получена от реального Nginx.</p>\n<pre><code>GET /health status=504 request=15.001 upstream=<backend> upstream_status=-\nconnect=0.001 header=- response=15.001</code></pre>\n<p>Строка поддерживает гипотезу: соединение установилось быстро, но этот Nginx не получил заголовки ответа до заданного <code>proxy_read_timeout 15s</code>. Она не доказывает причину внутри приложения. Следующий шаг — проверить конкретный endpoint и безопасный токен в логе приложения. Если записи нет, это неизвестное, а не повод сочинять её содержание.</p>\n<h2>Порядок проверки</h2>\n<ol><li>Записать один симптом, один безопасный URL и цену ошибки. Не объединять неверную схему, адрес и 504 в одну причину.</li><li>Нарисовать реальные hop-ы: клиент, TLS termination, Nginx, возможный предыдущий proxy и upstream. Отдельно указать, кто имеет право выставлять forwarded-поля.</li><li>Проверить итоговый <code>location</code>, <code>proxy_pass</code> и все <code>proxy_set_header</code>. Смотреть нужно собранную конфигурацию, а не только фрагмент include-файла.</li><li>Разделить connect, send и read timeout. Для выбранного endpoint указать, какую паузу ограничивает каждое значение.</li><li>Выполнить один учебный запрос к маршруту без пользовательских данных. Сверить status и ответные заголовки с access-log и записью приложения по безопасному токену.</li><li>Изменить одну подтверждённую границу и повторить тот же запрос. Если результат не изменился, сохранить отрицательный вывод и перейти к следующей строке таблицы.</li><li>Зафиксировать откат и новый сигнал наблюдения. Увеличение таймаута без ожидаемого изменения в журнале не считается объяснённым исправлением.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Эта модель не делает forwarded-заголовок безопасным. Доверие задаёт топология и конфигурация. В Nginx realip-модуле адрес клиента меняется только для источников, которые явно разрешены через <code>set_real_ip_from</code>. Если прямой запрос может обойти доверенный proxy, приложение должно считать такие поля недоверенными или закрыть этот путь на сети.</p>\n<p>Модель также не выбирает таймауты за команду. Значение зависит от типа endpoint, бюджета клиента, лимита приложения, поведения upstream и числа одновременных соединений. Длинный timeout может уменьшить число быстрых ошибок, но увеличить очередь и расход ресурсов. Retry добавляет ещё одну попытку и может повторить неидемпотентное действие. Его нельзя добавлять как универсальное средство против 504.</p>\n<p>Отрицательный путь обязателен. Если после явного <code>Host</code> приложение всё ещё строит неверную ссылку, проверяют поле, которое читает код, и предыдущий TLS-терминатор. Если после изменения read timeout status не изменился, проверяют маршрут, доступность upstream и клиентский timeout. Если адрес остаётся адресом proxy, проверяют доверенную цепочку и прямой доступ. «Перезапустили — стало нормально» не подтверждает ни одну из этих гипотез.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Граница готова, если команда может повторить один безопасный запрос и получить доказательства по каждому hop-у: внешний status и схема, итоговый набор переданных headers, запись proxy с request/upstream-временами и согласованная запись приложения. Для адреса известен список доверенных proxy. Для каждого timeout названа конкретная пауза. Отрицательный тест с недоверенным прямым header не меняет схему, права или адрес клиента. Если хотя бы одно условие не проверено, конфигурация ещё не готова к переносу в рабочий контур.</p>\n<p>Все конфигурации, команды и значения времени в статье учебные. Nginx, curl, staging и production здесь не запускались. Перед применением нужно проверить версию Nginx, реальную топологию, владельца конфигурации, политику журналирования и возможность безопасного отката.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://nginx.org/en/docs/http/ngx_http_proxy_module.html\" target=\"_blank\" rel=\"noopener noreferrer\">Nginx: ngx_http_proxy_module</a> — официальная документация по <code>proxy_pass</code>, <code>proxy_set_header</code> и таймаутам proxy.</li><li><a href=\"https://nginx.org/en/docs/http/ngx_http_realip_module.html\" target=\"_blank\" rel=\"noopener noreferrer\">Nginx: ngx_http_realip_module</a> — официальная документация по доверенным источникам и замене адреса клиента.</li><li><a href=\"https://nginx.org/en/docs/http/ngx_http_log_module.html\" target=\"_blank\" rel=\"noopener noreferrer\">Nginx: ngx_http_log_module</a> — официальная документация по <code>log_format</code>, <code>access_log</code> и журналированию запросов.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc7239\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 7239: Forwarded HTTP Extension</a> — спецификация формата forwarded-информации и ограничений доверия к ней.</li></ul>"
|
||
}
|