diff --git a/editorial/agent-rewrites/277.json b/editorial/agent-rewrites/277.json index 75c6d56..0e272ca 100644 --- a/editorial/agent-rewrites/277.json +++ b/editorial/agent-rewrites/277.json @@ -3,5 +3,5 @@ "slug": "editorial-2020-04-field-reverse-proxy", "title": "Reverse proxy: как отделить 502, 504, неверную схему и адрес клиента", "excerpt": "504, ссылка с http вместо https и одинаковый IP в логах — разные ветки диагностики. Разбираем один proxy-hop, безопасный access-log и порядок проверки без правок вслепую.", - "contentHtml": "

Клиент получает 504. Приложение строит ссылку с http, хотя пользователь открыл сайт по HTTPS. В access-log все пользователи приходят с одним IP. Эти симптомы часто называют одной проблемой reverse proxy и начинают увеличивать таймауты. Цена ошибки — медленный ответ для всех маршрутов, неверные redirect и потеря реального адреса клиента в расследовании. Иногда такая правка ещё и позволяет внешнему клиенту подменить forwarded-заголовок.

\n

Reverse proxy создаёт отдельный HTTP-hop между клиентом и приложением. Внешний TLS может завершиться на proxy, а до приложения пойдёт обычный HTTP. Приложение увидит адрес proxy как непосредственный peer. Это ожидаемо. Ошибка появляется, когда код принимает внутреннюю схему за внешнюю или считает первый элемент X-Forwarded-For достоверным без проверки источника.

\n

Тезис: status показывает ветку, а не причину

\n

Один status не объясняет, где сломался запрос. 504 указывает, что proxy не получил нужный ответ вовремя. Это ещё не доказательство проблем базы, GC или внешнего API. 502 означает, что proxy не смог отдать корректный ответ upstream в своей конфигурации. Причина может быть в маршруте, соединении, формате ответа или недоступном процессе.

\n

Разбор нужно вести по одному URL и одному вопросу. Для 504 вопрос звучит так: «получил ли proxy заголовки ответа upstream до истечения лимита чтения?» Для схемы: «какой hop завершил TLS и какое поле читает приложение?» Для адреса: «какой источник имеет право заменить адрес непосредственного peer?» Такие вопросы отделяют наблюдение от догадки.

\n

Сначала фиксируем путь запроса

\n

Нарисуйте минимальную цепочку: клиент, внешний балансировщик или Nginx, затем upstream-приложение. Отдельно отметьте место TLS termination. Если перед вашим Nginx есть ещё один proxy, он становится частью договора. Запишите, кто добавляет или перезаписывает Forwarded, X-Forwarded-For и X-Forwarded-Proto. Не смешивайте заголовок из внешнего запроса с тем, что сформировал доверенный hop.

\n

Для первой проверки выберите /health, техническую страницу или отдельный стендовый endpoint без пользовательских данных. Добавьте безопасный диагностический токен, если приложение умеет связать его с записью в журнале. Запрос должен быть повторяемым. Если лог приложения не содержит токен, запишите это как неизвестное. Не восстанавливайте отсутствующие факты по времени ответа.

\n
Карта диагностики reverse proxy
СимптомПричинаПроверкаДействие
504 на одном endpointProxy не дождался чтения ответа upstreamСопоставить request_time, upstream_header_time, upstream_response_time и лог приложения по одному токенуИсправить задержку upstream или отдельно пересмотреть лимит этого endpoint
502 после изменения proxy_passНеверный маршрут, URI или некорректный ответ upstreamПроверить итоговый location, адрес upstream и upstream_statusИсправить одну границу маршрута; не лечить 502 увеличением read timeout
Приложение строит ссылку с httpTLS завершился раньше, а приложение читает внутреннюю схемуСверить внешний маршрут, $scheme proxy и поле, которое читает фреймворкЗафиксировать один доверенный forwarded-сигнал и его источник
У всех пользователей один IPПриложение видит peer proxy или real IP настроен без доверенной границыПроверить прямой доступ к приложению и список доверенных proxyНастроить real IP только для известных источников; не брать первый header вслепую
\n

В таблице нет действия «перезапустить всё». Перезапуск может убрать временный эффект, но не доказывает причину. Не меняйте одновременно таймаут, пул соединений, retry и код обработки заголовков. Иначе следующий запрос не покажет, какая правка повлияла на результат.

\n

Что именно измеряет proxy

\n

Для upstream полезны четыре времени. upstream_connect_time показывает время соединения. upstream_header_time — время до заголовков ответа. upstream_response_time — время до завершения чтения ответа. request_time включает обработку запроса на стороне proxy. Значение - не равно нулю: соответствующая стадия могла не завершиться или upstream мог не ответить.

\n

Учебный формат журнала должен сохранять эту развилку и не собирать лишние данные. Не добавляйте authorization, cookie, тело запроса и реальный IP, если они не нужны для конкретной проверки. Пример ниже синтетический. Его значения не описывают production-систему.

\n
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;
\n
# Синтетическая строка для чтения полей, не реальный access-log\nGET /health status=504 request=3.001 upstream=<backend> upstream_status=-\nconnect=0.001 header=- response=3.001
\n

Эта строка поддерживает гипотезу: proxy быстро установил соединение, но не получил заголовки ответа до своего лимита. Она не называет причину задержки. Следующий шаг — проверить конфигурацию конкретного location и запись приложения для того же запроса. Если upstream успел выполнить работу, увеличение таймаута только скроет задержку и увеличит число одновременно занятых соединений.

\n

Схема, адрес и заголовки требуют разных правил

\n

На внутреннем hop-е $scheme может быть http, даже если внешний клиент использовал HTTPS. Приложение должно получить внешний факт через согласованный заголовок. Но заголовок безопасен только тогда, когда внешний клиент не может напрямую передать его приложению и когда proxy передаёт его по понятному правилу.

\n

Для адреса действует другая граница. set_real_ip_from описывает доверенные источники, а не всех возможных клиентов. Если приложение доступно в обход proxy, клиент может отправить собственный X-Forwarded-For. В этом случае чтение первого значения превращает пользовательский ввод в идентификатор клиента. Сначала закройте обход или определите его в архитектуре, затем настройте цепочку proxy. Не смешивайте Forwarded и X-Forwarded-* как будто они всегда образуют одну достоверную последовательность.

\n
Граница доверия между клиентом, reverse proxy и приложением для схемы и адреса клиента
Схему и адрес клиента проверяют через разные договоры: источник forwarded-заголовка и список доверенных proxy должны быть известны заранее.
\n

Учебная конфигурация

\n

Ниже приведён ограниченный пример для изолированного стенда. Имена, адрес, порт и значения не относятся к рабочему контуру. В рабочей системе нужно проверить итоговую конфигурацию после всех include, а не только фрагмент из одного файла.

\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 $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}
\n

Эта конфигурация показывает механизм, но не выбирает правильные значения для нагрузки. proxy_read_timeout ограничивает паузу между чтениями ответа. Для streaming endpoint это не обязательно полная длительность передачи. Если приложение отправляет части ответа с большими паузами, критерий готовности должен учитывать этот режим отдельно.

\n

Порядок проверки

\n
  1. Зафиксируйте один симптом, один URL без чувствительных данных и цену повторения ошибки.
  2. Отметьте на схеме клиент, каждый proxy-hop, upstream и место TLS termination.
  3. Проверьте итоговые proxy_pass, proxy_set_header и таймауты конкретного location.
  4. Включите безопасный access-log с request_time и upstream-полями на согласованном стенде.
  5. Выполните один повторяемый запрос с диагностическим токеном; сопоставьте proxy-log и лог приложения.
  6. Измените только подтверждённую границу, повторите тот же маршрут и сравните те же поля.
  7. Если результат не изменился, откатите правку и перейдите к следующей гипотезе из таблицы.
\n

Для проверки заголовков используйте только подстановки стенда. Команда ниже не запускалась и не подтверждает доступность адреса.

\n
# Учебный запрос; заменить только URL и Host изолированного стенда\ncurl -i --max-time 5 \\\n  '<PROXY_URL>/health' \\\n  -H 'Host: <HOST>' \\\n  -H 'X-Debug-Token: proxy-study-2020-04'
\n

Проверяйте не только статус 200. Для схемы сравните значение, которое использовал код, с договором proxy. Для адреса убедитесь, что приложение получило значение только от доверенной цепочки. Для 504 сопоставьте границу времени с upstream и журналом приложения. Если исходная запись отсутствует, оставьте это неизвестным и не объявляйте гипотезу доказанной.

\n

Ограничения и критерий готовности

\n

Статья не заменяет документацию конкретного фреймворка, балансировщика или версии Nginx. Она не выбирает таймаут без данных о нагрузке и не делает forwarded-заголовок достоверным сам по себе. Примеры журнала, времени, адреса 127.0.0.1 и URL являются учебными. Реальные Nginx, curl, browser, staging и production для этого материала не запускались.

\n

Проверка готова, когда для одного стендового маршрута зафиксированы цепочка hop-ов, место TLS termination, источник каждого forwarded-поля и безопасные proxy-времена. Повторный запрос даёт тот же ожидаемый контракт. Изменение одной причины меняет ожидаемый сигнал, а неверная гипотеза не маскируется перезапуском. В рабочем контуре дополнительно проверяют отсутствие прямого обхода proxy, политику журналирования и план отката.

\n

Проверяемые источники

\n" + "contentHtml": "

Клиент получает 504. Приложение строит ссылку с http, хотя пользователь открыл сайт по HTTPS. В access log все пользователи приходят с одним IP. Эти симптомы часто называют одной проблемой reverse proxy и начинают увеличивать таймауты. Цена ошибки — медленный ответ для всех маршрутов, неверные перенаправления и потеря реального адреса клиента в расследовании. Иногда такая правка ещё и позволяет внешнему клиенту подменить forwarded-заголовок.

\n

Reverse proxy создаёт отдельный HTTP-hop между клиентом и приложением. Внешний TLS может завершиться на proxy, а до приложения пойдёт обычный HTTP. Приложение увидит адрес proxy как непосредственный peer. Это ожидаемо. Ошибка появляется, когда код принимает внутреннюю схему за внешнюю или считает первый элемент X-Forwarded-For достоверным без проверки источника.

\n

Тезис: status показывает ветку, а не причину

\n

Один status не объясняет, где сломался запрос. В этой цепочке 504 означает, что proxy не получил ответ upstream в пределах своего лимита ожидания; это ещё не доказательство проблем базы, GC или внешнего API. 502 означает, что proxy не получил корректный ответ upstream или не смог выбрать сервер. Причина может быть в маршруте, соединении, формате ответа или недоступном процессе.

\n

Разбор нужно вести по одному URL и одному вопросу. Для 504 вопрос звучит так: «получил ли proxy заголовки ответа upstream до истечения лимита чтения?» Для схемы: «какой hop завершил TLS и какое поле читает приложение?» Для адреса: «какой источник имеет право заменить адрес непосредственного peer?» Такие вопросы отделяют наблюдение от догадки.

\n

Сначала фиксируем путь запроса

\n

Нарисуйте минимальную цепочку: клиент, внешний балансировщик или Nginx, затем upstream-приложение. Отдельно отметьте место TLS termination. Если перед вашим Nginx есть ещё один proxy, он становится частью договора. Запишите, кто добавляет или перезаписывает Forwarded, X-Forwarded-For и X-Forwarded-Proto. Не смешивайте заголовок из внешнего запроса с тем, что сформировал доверенный hop.

\n

Для первой проверки выберите /health, техническую страницу или отдельный стендовый endpoint без пользовательских данных. Добавьте безопасный диагностический токен, если приложение умеет связать его с записью в журнале. Запрос должен быть повторяемым. Если лог приложения не содержит токен, запишите это как неизвестное. Не восстанавливайте отсутствующие факты по времени ответа.

\n
Карта диагностики reverse proxy
СимптомПричинаПроверкаДействие
504 на одном endpointProxy не дождался чтения ответа upstreamСопоставить request_time, upstream_header_time, upstream_response_time и лог приложения по одному токенуИсправить задержку upstream или отдельно пересмотреть лимит этого endpoint
502 после изменения proxy_passНеверный маршрут, URI или некорректный ответ upstreamПроверить итоговый location, адрес upstream и upstream_statusИсправить одну границу маршрута; не лечить 502 увеличением read timeout
Приложение строит ссылку с httpTLS завершился раньше, а приложение читает внутреннюю схемуСверить внешний маршрут, $scheme proxy и поле, которое читает фреймворкЗафиксировать один доверенный forwarded-сигнал и его источник
У всех пользователей один IPПриложение видит peer proxy или real IP настроен без доверенной границыПроверить прямой доступ к приложению и список доверенных proxyНастроить real IP только для известных источников; не брать первый header вслепую
\n

В таблице нет действия «перезапустить всё». Перезапуск может убрать временный эффект, но не доказывает причину. Не меняйте одновременно таймаут, пул соединений, retry и код обработки заголовков. Иначе следующий запрос не покажет, какая правка повлияла на результат.

\n

Что именно измеряет proxy

\n

Для upstream полезны четыре времени. upstream_connect_time показывает время соединения. upstream_header_time — время до заголовков ответа. upstream_response_time — время до завершения чтения ответа. request_time включает обработку запроса на стороне proxy. Значение - не равно нулю: соответствующая стадия могла не завершиться или upstream мог не ответить.

\n

Учебный формат журнала должен сохранять эту развилку и не собирать лишние данные. Не добавляйте authorization, cookie, тело запроса и реальный IP, если они не нужны для конкретной проверки. Пример ниже синтетический. Его значения не описывают production-систему.

\n
# 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;
\n
# Синтетическая строка для чтения полей, не реальный access log\nGET /health status=504 request=3.001 upstream=<backend> upstream_status=-\nconnect=0.001 header=- response=3.001
\n

Эта строка поддерживает гипотезу: proxy быстро установил соединение, но не получил заголовки ответа до своего лимита. Она не называет причину задержки. Следующий шаг — проверить конфигурацию конкретного location и запись приложения для того же запроса. Если upstream успел выполнить работу, увеличение таймаута только скроет задержку и увеличит число одновременно занятых соединений.

\n

Схема, адрес и заголовки требуют разных правил

\n

На внутреннем hop-е $scheme может быть http, даже если внешний клиент использовал HTTPS. Приложение должно получить внешний факт через согласованный заголовок. Но заголовок безопасен только тогда, когда внешний клиент не может напрямую передать его приложению и когда proxy передаёт его по понятному правилу.

\n

Для адреса действует другая граница. set_real_ip_from описывает доверенные источники, а не всех возможных клиентов. Если приложение доступно в обход proxy, клиент может отправить собственный X-Forwarded-For. В этом случае чтение первого значения превращает пользовательский ввод в идентификатор клиента. Сначала закройте обход или определите его в архитектуре, затем настройте цепочку proxy. Не смешивайте Forwarded и X-Forwarded-* как будто они всегда образуют одну достоверную последовательность.

\n
Пять шагов диагностики reverse proxy: симптом, безопасный маршрут, конфигурация, сопоставление логов и повтор одной правки
Разбор ведут по одной ветке: сначала фиксируют симптом и цену, затем сопоставляют конфигурацию, proxy-log и лог приложения, меняют одну границу и повторяют запрос.
\n

Учебная конфигурация

\n

Ниже приведён ограниченный пример для изолированного стенда. Имена, адрес, порт и значения не относятся к рабочему контуру. Сеть 192.0.2.0/24 — заполнитель для известного upstream-proxy; её нельзя оставлять без сверки с топологией.

\n
# Учебный пример; не переносить без проверки топологии\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}
\n

В этом варианте Nginx принимает адрес из X-Forwarded-For только от сети, названной в set_real_ip_from, а затем передаёт приложению вычисленный $remote_addr. Если внешний балансировщик завершает TLS раньше этого Nginx, одной строки proxy_set_header X-Forwarded-Proto $scheme недостаточно: нужен отдельный договор для значения схемы от известного hop-а. При прямом доступе клиента к этому серверу real IP-настройка не должна считаться защитой.

\n

Эта конфигурация показывает механизм, но не выбирает правильные значения для нагрузки. proxy_read_timeout ограничивает паузу между чтениями ответа. Для streaming endpoint это не обязательно полная длительность передачи. Если приложение отправляет части ответа с большими паузами, критерий готовности должен учитывать этот режим отдельно.

\n

Порядок проверки

\n
  1. Зафиксируйте один симптом, один URL без чувствительных данных и цену повторения ошибки.
  2. Отметьте на схеме клиент, каждый proxy-hop, upstream и место TLS termination.
  3. Проверьте итоговые proxy_pass, proxy_set_header и таймауты конкретного location.
  4. Включите безопасный access log с request_time и upstream-полями на согласованном стенде.
  5. Выполните один повторяемый запрос с диагностическим токеном; сопоставьте proxy-log и лог приложения.
  6. Измените только подтверждённую границу, повторите тот же маршрут и сравните те же поля.
  7. Если результат не изменился, откатите правку и перейдите к следующей гипотезе из таблицы.
\n

Для проверки заголовков используйте только подстановки стенда. Команда ниже не запускалась и не подтверждает доступность адреса.

\n
# Учебный запрос; заменить только URL и Host изолированного стенда\ncurl -i --max-time 5 \\\n  '<PROXY_URL>/health' \\\n  -H 'Host: <HOST>' \\\n  -H 'X-Debug-Token: proxy-study-2020-04'
\n

Проверяйте не только статус 200. Для схемы сравните значение, которое использовал код, с договором proxy. Для адреса убедитесь, что приложение получило значение только от доверенной цепочки. Для 504 сопоставьте границу времени с upstream и журналом приложения. Если исходная запись отсутствует, оставьте это неизвестным и не объявляйте гипотезу доказанной.

\n

Ограничения и критерий готовности

\n

Статья не заменяет документацию конкретного фреймворка, балансировщика или версии Nginx. Она не выбирает таймаут без данных о нагрузке и не делает forwarded-заголовок достоверным сам по себе. Примеры журнала, времени, адреса 127.0.0.1, сети 192.0.2.0/24 и URL являются учебными. Реальные Nginx, curl, browser, staging и production для этого материала не запускались.

\n

Проверка готова, когда для одного стендового маршрута зафиксированы цепочка hop-ов, место TLS termination, источник каждого forwarded-поля и безопасные proxy-времена. Повторный запрос даёт тот же ожидаемый контракт. Изменение одной причины меняет ожидаемый сигнал, а неверная гипотеза не маскируется перезапуском. В рабочем контуре дополнительно проверяют отсутствие прямого обхода proxy, политику журналирования и план отката.

\n

Проверяемые источники

\n" }