8 lines
19 KiB
JSON
8 lines
19 KiB
JSON
{
|
||
"index": 277,
|
||
"slug": "editorial-2020-04-field-reverse-proxy",
|
||
"title": "Reverse proxy: как отделить 502, 504, неверную схему и адрес клиента",
|
||
"excerpt": "504, ссылка с http вместо https и одинаковый IP в логах — разные ветки диагностики. Разбираем один proxy-hop, безопасный access-log и порядок проверки без правок вслепую.",
|
||
"contentHtml": "<p>Клиент получает 504. Приложение строит ссылку с <code>http</code>, хотя пользователь открыл сайт по HTTPS. В access log все пользователи приходят с одним IP. Эти симптомы часто называют одной проблемой reverse proxy и начинают увеличивать таймауты. Цена ошибки — медленный ответ для всех маршрутов, неверные перенаправления и потеря реального адреса клиента в расследовании. Иногда такая правка ещё и позволяет внешнему клиенту подменить forwarded-заголовок.</p>\n<p>Reverse proxy создаёт отдельный HTTP-hop между клиентом и приложением. Внешний TLS может завершиться на proxy, а до приложения пойдёт обычный HTTP. Приложение увидит адрес proxy как непосредственный peer. Это ожидаемо. Ошибка появляется, когда код принимает внутреннюю схему за внешнюю или считает первый элемент <code>X-Forwarded-For</code> достоверным без проверки источника.</p>\n<h2>Тезис: status показывает ветку, а не причину</h2>\n<p>Один status не объясняет, где сломался запрос. В этой цепочке 504 означает, что proxy не получил ответ upstream в пределах своего лимита ожидания; это ещё не доказательство проблем базы, GC или внешнего API. 502 означает, что proxy не получил корректный ответ upstream или не смог выбрать сервер. Причина может быть в маршруте, соединении, формате ответа или недоступном процессе.</p>\n<p>Разбор нужно вести по одному URL и одному вопросу. Для 504 вопрос звучит так: «получил ли proxy заголовки ответа upstream до истечения лимита чтения?» Для схемы: «какой hop завершил TLS и какое поле читает приложение?» Для адреса: «какой источник имеет право заменить адрес непосредственного peer?» Такие вопросы отделяют наблюдение от догадки.</p>\n<h2>Сначала фиксируем путь запроса</h2>\n<p>Нарисуйте минимальную цепочку: клиент, внешний балансировщик или Nginx, затем upstream-приложение. Отдельно отметьте место TLS termination. Если перед вашим Nginx есть ещё один proxy, он становится частью договора. Запишите, кто добавляет или перезаписывает <code>Forwarded</code>, <code>X-Forwarded-For</code> и <code>X-Forwarded-Proto</code>. Не смешивайте заголовок из внешнего запроса с тем, что сформировал доверенный hop.</p>\n<p>Для первой проверки выберите <code>/health</code>, техническую страницу или отдельный стендовый endpoint без пользовательских данных. Добавьте безопасный диагностический токен, если приложение умеет связать его с записью в журнале. Запрос должен быть повторяемым. Если лог приложения не содержит токен, запишите это как неизвестное. Не восстанавливайте отсутствующие факты по времени ответа.</p>\n<div class='table-scroll'><table><caption>Карта диагностики reverse proxy</caption><thead><tr><th scope='col'>Симптом</th><th scope='col'>Причина</th><th scope='col'>Проверка</th><th scope='col'>Действие</th></tr></thead><tbody><tr><td>504 на одном endpoint</td><td>Proxy не дождался чтения ответа upstream</td><td>Сопоставить <code>request_time</code>, <code>upstream_header_time</code>, <code>upstream_response_time</code> и лог приложения по одному токену</td><td>Исправить задержку upstream или отдельно пересмотреть лимит этого endpoint</td></tr><tr><td>502 после изменения <code>proxy_pass</code></td><td>Неверный маршрут, URI или некорректный ответ upstream</td><td>Проверить итоговый <code>location</code>, адрес upstream и <code>upstream_status</code></td><td>Исправить одну границу маршрута; не лечить 502 увеличением read timeout</td></tr><tr><td>Приложение строит ссылку с <code>http</code></td><td>TLS завершился раньше, а приложение читает внутреннюю схему</td><td>Сверить внешний маршрут, <code>$scheme</code> proxy и поле, которое читает фреймворк</td><td>Зафиксировать один доверенный forwarded-сигнал и его источник</td></tr><tr><td>У всех пользователей один IP</td><td>Приложение видит peer proxy или real IP настроен без доверенной границы</td><td>Проверить прямой доступ к приложению и список доверенных proxy</td><td>Настроить real IP только для известных источников; не брать первый header вслепую</td></tr></tbody></table></div>\n<p>В таблице нет действия «перезапустить всё». Перезапуск может убрать временный эффект, но не доказывает причину. Не меняйте одновременно таймаут, пул соединений, retry и код обработки заголовков. Иначе следующий запрос не покажет, какая правка повлияла на результат.</p>\n<h2>Что именно измеряет proxy</h2>\n<p>Для upstream полезны четыре времени. <code>upstream_connect_time</code> показывает время соединения. <code>upstream_header_time</code> — время до заголовков ответа. <code>upstream_response_time</code> — время до завершения чтения ответа. <code>request_time</code> включает обработку запроса на стороне proxy. Значение <code>-</code> не равно нулю: соответствующая стадия могла не завершиться или upstream мог не ответить.</p>\n<p>Учебный формат журнала должен сохранять эту развилку и не собирать лишние данные. Не добавляйте authorization, cookie, тело запроса и реальный IP, если они не нужны для конкретной проверки. Пример ниже синтетический. Его значения не описывают production-систему.</p>\n<pre><code># http {}\nlog_format proxy_boundary '$request_method $uri status=$status '\n 'request=$request_time upstream=$upstream_addr '\n 'upstream_status=$upstream_status '\n 'connect=$upstream_connect_time '\n 'header=$upstream_header_time '\n 'response=$upstream_response_time';\n\n# server {} или location {}\naccess_log /path/to/proxy-boundary.log proxy_boundary;</code></pre>\n<pre><code># Синтетическая строка для чтения полей, не реальный access log\nGET /health status=504 request=3.001 upstream=<backend> upstream_status=-\nconnect=0.001 header=- response=3.001</code></pre>\n<p>Эта строка поддерживает гипотезу: proxy быстро установил соединение, но не получил заголовки ответа до своего лимита. Она не называет причину задержки. Следующий шаг — проверить конфигурацию конкретного <code>location</code> и запись приложения для того же запроса. Если upstream успел выполнить работу, увеличение таймаута только скроет задержку и увеличит число одновременно занятых соединений.</p>\n<h2>Схема, адрес и заголовки требуют разных правил</h2>\n<p>На внутреннем hop-е <code>$scheme</code> может быть <code>http</code>, даже если внешний клиент использовал HTTPS. Приложение должно получить внешний факт через согласованный заголовок. Но заголовок безопасен только тогда, когда внешний клиент не может напрямую передать его приложению и когда proxy передаёт его по понятному правилу.</p>\n<p>Для адреса действует другая граница. <code>set_real_ip_from</code> описывает доверенные источники, а не всех возможных клиентов. Если приложение доступно в обход proxy, клиент может отправить собственный <code>X-Forwarded-For</code>. В этом случае чтение первого значения превращает пользовательский ввод в идентификатор клиента. Сначала закройте обход или определите его в архитектуре, затем настройте цепочку proxy. Не смешивайте <code>Forwarded</code> и <code>X-Forwarded-*</code> как будто они всегда образуют одну достоверную последовательность.</p>\n<figure><img src='/assets/editorial/2020/reverse-proxy-diagnosis-2020.svg' alt='Пять шагов диагностики reverse proxy: симптом, безопасный маршрут, конфигурация, сопоставление логов и повтор одной правки' loading='lazy' /><figcaption>Разбор ведут по одной ветке: сначала фиксируют симптом и цену, затем сопоставляют конфигурацию, proxy-log и лог приложения, меняют одну границу и повторяют запрос.</figcaption></figure>\n<h2>Учебная конфигурация</h2>\n<p>Ниже приведён ограниченный пример для изолированного стенда. Имена, адрес, порт и значения не относятся к рабочему контуру. Сеть <code>192.0.2.0/24</code> — заполнитель для известного upstream-proxy; её нельзя оставлять без сверки с топологией.</p>\n<pre><code># Учебный пример; не переносить без проверки топологии\n# Только если запросы приходят от этой доверенной сети proxy\nset_real_ip_from 192.0.2.0/24;\nreal_ip_header X-Forwarded-For;\nreal_ip_recursive on;\n\nupstream app_backend {\n server 127.0.0.1:3000;\n}\n\nserver {\n listen 8080;\n server_name _;\n\n location / {\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $remote_addr;\n proxy_set_header X-Forwarded-Proto $scheme;\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>В этом варианте Nginx принимает адрес из <code>X-Forwarded-For</code> только от сети, названной в <code>set_real_ip_from</code>, а затем передаёт приложению вычисленный <code>$remote_addr</code>. Если внешний балансировщик завершает TLS раньше этого Nginx, одной строки <code>proxy_set_header X-Forwarded-Proto $scheme</code> недостаточно: нужен отдельный договор для значения схемы от известного hop-а. При прямом доступе клиента к этому серверу real IP-настройка не должна считаться защитой.</p>\n<p>Эта конфигурация показывает механизм, но не выбирает правильные значения для нагрузки. <code>proxy_read_timeout</code> ограничивает паузу между чтениями ответа. Для streaming endpoint это не обязательно полная длительность передачи. Если приложение отправляет части ответа с большими паузами, критерий готовности должен учитывать этот режим отдельно.</p>\n<h2>Порядок проверки</h2>\n<ol><li>Зафиксируйте один симптом, один URL без чувствительных данных и цену повторения ошибки.</li><li>Отметьте на схеме клиент, каждый proxy-hop, upstream и место TLS termination.</li><li>Проверьте итоговые <code>proxy_pass</code>, <code>proxy_set_header</code> и таймауты конкретного <code>location</code>.</li><li>Включите безопасный access log с <code>request_time</code> и upstream-полями на согласованном стенде.</li><li>Выполните один повторяемый запрос с диагностическим токеном; сопоставьте proxy-log и лог приложения.</li><li>Измените только подтверждённую границу, повторите тот же маршрут и сравните те же поля.</li><li>Если результат не изменился, откатите правку и перейдите к следующей гипотезе из таблицы.</li></ol>\n<p>Для проверки заголовков используйте только подстановки стенда. Команда ниже не запускалась и не подтверждает доступность адреса.</p>\n<pre><code># Учебный запрос; заменить только URL и Host изолированного стенда\ncurl -i --max-time 5 \\\n '<PROXY_URL>/health' \\\n -H 'Host: <HOST>' \\\n -H 'X-Debug-Token: proxy-study-2020-04'</code></pre>\n<p>Проверяйте не только статус 200. Для схемы сравните значение, которое использовал код, с договором proxy. Для адреса убедитесь, что приложение получило значение только от доверенной цепочки. Для 504 сопоставьте границу времени с upstream и журналом приложения. Если исходная запись отсутствует, оставьте это неизвестным и не объявляйте гипотезу доказанной.</p>\n<h2>Ограничения и критерий готовности</h2>\n<p>Статья не заменяет документацию конкретного фреймворка, балансировщика или версии Nginx. Она не выбирает таймаут без данных о нагрузке и не делает forwarded-заголовок достоверным сам по себе. Примеры журнала, времени, адреса <code>127.0.0.1</code>, сети <code>192.0.2.0/24</code> и URL являются учебными. Реальные Nginx, curl, browser, staging и production для этого материала не запускались.</p>\n<p>Проверка готова, когда для одного стендового маршрута зафиксированы цепочка hop-ов, место TLS termination, источник каждого forwarded-поля и безопасные proxy-времена. Повторный запрос даёт тот же ожидаемый контракт. Изменение одной причины меняет ожидаемый сигнал, а неверная гипотеза не маскируется перезапуском. В рабочем контуре дополнительно проверяют отсутствие прямого обхода proxy, политику журналирования и план отката.</p>\n<h2>Проверяемые источники</h2>\n<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> — официальная документация директив proxy_pass, proxy_set_header и proxy timeout.</li><li><a href='https://nginx.org/en/docs/http/ngx_http_upstream_module.html' target='_blank' rel='noopener noreferrer'>Nginx: ngx_http_upstream_module</a> — официальная документация переменных upstream_addr, upstream_status и upstream-времени.</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://www.rfc-editor.org/rfc/rfc7239' target='_blank' rel='noopener noreferrer'>RFC 7239: Forwarded HTTP Extension</a> — стандартный формат forwarded-информации и границы доверия к ней.</li></ul>"
|
||
}
|