Files
progcode/editorial/agent-rewrites/277.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
18 KiB
JSON
Raw 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": 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 и начинают увеличивать таймауты. Цена ошибки — медленный ответ для всех маршрутов, неверные redirect и потеря реального адреса клиента в расследовании. Иногда такая правка ещё и позволяет внешнему клиенту подменить 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 не получил нужный ответ вовремя. Это ещё не доказательство проблем базы, 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>log_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\naccess_log /path/to/proxy-boundary.log proxy_boundary;</code></pre>\n<pre><code># Синтетическая строка для чтения полей, не реальный access-log\nGET /health status=504 request=3.001 upstream=&lt;backend&gt; 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-header-boundary-2020.svg' alt='Граница доверия между клиентом, reverse proxy и приложением для схемы и адреса клиента' loading='lazy' /><figcaption>Схему и адрес клиента проверяют через разные договоры: источник forwarded-заголовка и список доверенных proxy должны быть известны заранее.</figcaption></figure>\n<h2>Учебная конфигурация</h2>\n<p>Ниже приведён ограниченный пример для изолированного стенда. Имена, адрес, порт и значения не относятся к рабочему контуру. В рабочей системе нужно проверить итоговую конфигурацию после всех <code>include</code>, а не только фрагмент из одного файла.</p>\n<pre><code># Учебный пример; не переносить без проверки топологии\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 $proxy_add_x_forwarded_for;\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>Эта конфигурация показывает механизм, но не выбирает правильные значения для нагрузки. <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 '&lt;PROXY_URL&gt;/health' \\\n -H 'Host: &lt;HOST&gt;' \\\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> и 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_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>"
}