diff --git a/editorial/agent-rewrites/278.json b/editorial/agent-rewrites/278.json index e6a1ad0..6091722 100644 --- a/editorial/agent-rewrites/278.json +++ b/editorial/agent-rewrites/278.json @@ -3,5 +3,5 @@ "slug": "editorial-2020-04-mechanism-reverse-proxy", "title": "Reverse proxy под капотом: два соединения, заголовки и таймауты", "excerpt": "Приложение видит не внешний запрос, а новый upstream-hop. Разбираем, как proxy меняет Host, forwarded-заголовки и время ожидания, и как проверить границу без догадок.", - "contentHtml": "
Проблема часто выглядит как ошибка приложения. Пользователь открыл HTTPS, а код строит ссылку с http. В журнале все посетители имеют один адрес. После добавления proxy часть запросов заканчивается 502 или 504. Цена ошибки — не только один неудачный ответ. Неверная схема ломает редиректы и cookie. Ошибка в адресе клиента ломает лимиты, аудит и расследование. Слишком большой таймаут дольше держит соединения и маскирует зависший upstream.
\nТезис статьи простой: reverse proxy не является прозрачным проводом. Он принимает внешний HTTP-запрос, завершает один hop и создаёт новый запрос к upstream. На новой границе меняются peer address, часть заголовков, момент подключения и правила ожидания. Поэтому приложение нужно настраивать не на «оригинальный запрос вообще», а на явный договор: какие поля proxy передаёт, откуда они пришли, чему приложение доверяет и какой сигнал подтверждает каждый вывод.
\nКлиент устанавливает соединение с proxy. Proxy выбирает server и location, проверяет маршрут, может завершить TLS, изменить URI, добавить заголовки и буферизовать тело. Затем proxy устанавливает отдельное соединение с upstream. Для приложения непосредственным соседом становится proxy, а не браузер. Это нормально. Ошибка возникает, когда код принимает внутренний peer или клиентский header за внешний факт без проверки границы.
Схема работает так же. Если TLS завершается на Nginx, внешний hop может быть HTTPS, а соединение Nginx с приложением — HTTP. Значение $scheme на этом Nginx описывает именно вход в него. Если TLS завершился раньше, оно может не совпасть со схемой, которую видел клиент. В таком случае нужен один доверенный источник исходной схемы. Нельзя позволять приложению выбирать между несколькими заголовками по ситуации.
То же относится к адресу. $remote_addr на Nginx обозначает непосредственную удалённую сторону входного соединения. Если перед Nginx стоит балансировщик, это может быть адрес балансировщика. X-Forwarded-For может содержать цепочку, которую сформировал предыдущий proxy или прислал клиент. Само имя заголовка не делает значение достоверным.
| Симптом | Причина или гипотеза | Проверка | Действие |
|---|---|---|---|
Приложение строит ссылку с http | TLS завершился на proxy, а приложение не получило согласованный признак схемы | Сверить место TLS termination, $scheme и поле, которое читает приложение | Оставить один доверенный forwarded-сигнал и закрыть прямой обход приложения |
| Все пользователи имеют один адрес | Приложение видит peer proxy или неверно разбирает forwarded-цепочку | Нарисовать hop-ы и проверить доверенные источники до включения realip | Задать список доверенных proxy; не брать первый элемент заголовка вслепую |
| 502 появился после изменения маршрута | Неверный proxy_pass, URI, порт или недоступный upstream | Проверить итоговый location, адрес upstream и его status | Исправить маршрут; не увеличивать proxy_read_timeout |
| 504 появляется только на одном endpoint | Proxy не получил следующий байт ответа в пределах read timeout | Сопоставить request_time, upstream-времена и безопасную запись приложения | Найти паузу в upstream или изменить лимит только для этого типа endpoint |
| Потоковый ответ обрывается при долгой паузе | proxy_read_timeout ограничивает паузу между чтениями, а не всю передачу | Измерить интервалы между частями ответа | Выделить отдельную policy для streaming; не переносить общий JSON-лимит |
Таблица отделяет наблюдение от диагноза. Status 504 говорит о результате ожидания proxy, но не называет базу, GC, внешний API или код обработчика. Status 502 говорит о проблеме на пути к корректному upstream-ответу, но не выбирает между маршрутом, портом и самим процессом. Один сигнал даёт ветку проверки, а не готовое объяснение.
\nНиже приведён минимальный пример для изолированного стенда. Имена, адрес 127.0.0.1 и значения таймаутов учебные. Пример показывает места договора, а не готовую production-конфигурацию.
upstream app_backend { server 127.0.0.1:3000; } server { listen 8080; server_name _; location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Connection ""; proxy_connect_timeout 3s; proxy_send_timeout 10s; proxy_read_timeout 15s; proxy_pass http://app_backend; } }\nproxy_set_header Host $host задаёт значение явно. X-Forwarded-For расширяет цепочку, а не доказывает, что каждый её элемент заслуживает доверия. X-Forwarded-Proto $scheme описывает схему входного hop-а именно этого Nginx. Если перед ним есть другой TLS-терминатор, контракт должен учитывать его. Нельзя копировать строку https только потому, что внешняя страница открывается по HTTPS.
Таймауты отвечают на разные паузы. proxy_connect_timeout относится к установлению соединения с upstream. proxy_send_timeout относится к последовательным операциям записи запроса. proxy_read_timeout относится к паузе между последовательными операциями чтения ответа. Поэтому длительность ответа и read timeout — разные величины. Streaming может длиться дольше лимита, если upstream регулярно отправляет данные. Ответ без следующего байта может оборваться раньше, чем команда ожидает.
Для первого разбора достаточно access-log с URI, итоговым status, $request_time, адресом upstream, его status и временем подключения, заголовков и ответа. Такой формат отвечает на узкий вопрос: proxy не подключился к upstream или подключился, но не дождался заголовков. Не нужно записывать cookie, authorization, тело запроса или полный query string. Лишние данные не делают гипотезу точнее.
log_format proxy_boundary "$request_method $uri status=$status request=$request_time upstream=$upstream_addr upstream_status=$upstream_status connect=$upstream_connect_time header=$upstream_header_time response=$upstream_response_time"; access_log /path/to/proxy-boundary.log proxy_boundary; GET /health status=504 request=3.001 upstream=<backend> upstream_status=- connect=0.001 header=- response=3.001;\nСинтетическая строка поддерживает гипотезу: соединение установилось быстро, но proxy не получил заголовки ответа до своего лимита. Она не доказывает причину внутри приложения. Следующий шаг — проверить конкретный endpoint и безопасный токен в логе приложения. Если записи нет, это неизвестное, а не повод сочинять её содержание.
\nlocation, proxy_pass и все proxy_set_header. Смотреть нужно собранную конфигурацию, а не только фрагмент include-файла.Эта модель не делает forwarded-заголовок безопасным. Доверие задаёт топология и конфигурация. В Nginx realip-модуле адрес клиента меняется только для источников, которые явно разрешены через set_real_ip_from. Если прямой запрос может обойти доверенный proxy, приложение должно считать такие поля недоверенными или закрыть этот путь на сети.
Модель также не выбирает таймауты за команду. Значение зависит от типа endpoint, бюджета клиента, лимита приложения, поведения upstream и числа одновременных соединений. Длинный timeout может уменьшить число быстрых ошибок, но увеличить очередь и расход ресурсов. Retry добавляет ещё одну попытку и может повторить неидемпотентное действие. Его нельзя добавлять как универсальное средство против 504.
\nОтрицательный путь обязателен. Если после явного Host приложение всё ещё строит неверную ссылку, проверяют поле, которое читает код, и предыдущий TLS-терминатор. Если после изменения read timeout status не изменился, проверяют маршрут, доступность upstream и клиентский timeout. Если адрес остаётся адресом proxy, проверяют доверенную цепочку и прямой доступ. «Перезапустили — стало нормально» не подтверждает ни одну из этих гипотез.
Граница готова, если команда может повторить один безопасный запрос и получить доказательства по каждому hop-у: внешний status и схема, итоговый набор переданных headers, запись proxy с request/upstream-временами и согласованная запись приложения. Для адреса известен список доверенных proxy. Для каждого timeout названа конкретная пауза. Отрицательный тест с недоверенным прямым header не меняет схему, права или адрес клиента. Если хотя бы одно условие не проверено, конфигурация ещё не готова к переносу в рабочий контур.
\nВсе конфигурации, команды и значения времени в статье учебные. Nginx, curl, staging и production здесь не запускались. Перед применением нужно проверить версию Nginx, реальную топологию, владельца конфигурации, политику журналирования и возможность безопасного отката.
\nproxy_pass, proxy_set_header и таймаутам proxy.Проблема часто выглядит как ошибка приложения. Пользователь открыл HTTPS, а код строит ссылку с http. В журнале все посетители имеют один адрес. После добавления proxy часть запросов заканчивается 502 или 504. Цена ошибки — не только один неудачный ответ. Неверная схема ломает редиректы и cookie. Ошибка в адресе клиента ломает лимиты, аудит и расследование. Слишком большой таймаут дольше держит соединения и маскирует зависший upstream.
\nТезис статьи простой: reverse proxy не является прозрачным проводом. Он принимает внешний HTTP-запрос, завершает один hop и создаёт новый запрос к upstream. На новой границе меняются peer address, часть заголовков, момент подключения и правила ожидания. Поэтому приложение нужно настраивать не на «оригинальный запрос вообще», а на явный договор: какие поля proxy передаёт, откуда они пришли, чему приложение доверяет и какой сигнал подтверждает каждый вывод.
\nКлиент устанавливает соединение с proxy. Proxy выбирает server и location, проверяет маршрут, может завершить TLS, изменить URI, добавить заголовки и буферизовать тело. Затем proxy устанавливает отдельное соединение с upstream. Для приложения непосредственным соседом становится proxy, а не браузер. Это нормально. Ошибка возникает, когда код принимает внутренний peer или клиентский header за внешний факт без проверки границы.
Схема работает так же. Если TLS завершается на Nginx, внешний hop может быть HTTPS, а соединение Nginx с приложением — HTTP. Значение $scheme на этом Nginx описывает именно вход в него. Если TLS завершился раньше, оно может не совпасть со схемой, которую видел клиент. В таком случае нужен один доверенный источник исходной схемы. Нельзя позволять приложению выбирать между несколькими заголовками по ситуации.
То же относится к адресу. $remote_addr на Nginx обозначает непосредственную удалённую сторону входного соединения. Если перед Nginx стоит балансировщик, это может быть адрес балансировщика. X-Forwarded-For может содержать цепочку, которую сформировал предыдущий proxy или прислал клиент. Само имя заголовка не делает значение достоверным.
| Симптом | Причина или гипотеза | Проверка | Действие |
|---|---|---|---|
Приложение строит ссылку с http | TLS завершился на proxy, а приложение не получило согласованный признак схемы | Сверить место TLS termination, $scheme и поле, которое читает приложение | Оставить один доверенный forwarded-сигнал и закрыть прямой обход приложения |
| Все пользователи имеют один адрес | Приложение видит peer proxy или неверно разбирает forwarded-цепочку | Нарисовать hop-ы и проверить доверенные источники до включения realip | Задать список доверенных proxy; не брать первый элемент заголовка вслепую |
| 502 появился после изменения маршрута | Неверный proxy_pass, URI, порт или недоступный upstream | Проверить итоговый location, адрес upstream и его status | Исправить маршрут; не увеличивать proxy_read_timeout |
| 504 появляется только на одном endpoint | Proxy не получил следующий байт ответа в пределах read timeout | Сопоставить request_time, upstream-времена и безопасную запись приложения | Найти паузу в upstream или изменить лимит только для этого типа endpoint |
| Потоковый ответ обрывается при долгой паузе | proxy_read_timeout ограничивает паузу между чтениями, а не всю передачу | Измерить интервалы между частями ответа | Выделить отдельную policy для streaming; не переносить общий лимит на другие маршруты |
Таблица отделяет наблюдение от диагноза. Status 504 говорит о результате ожидания proxy, но не называет базу, GC, внешний API или код обработчика. Status 502 говорит о проблеме на пути к корректному upstream-ответу, но не выбирает между маршрутом, портом и самим процессом. Один сигнал даёт ветку проверки, а не готовое объяснение.
\nНиже приведён минимальный пример для изолированного стенда. Имена, адрес 127.0.0.1 и значения таймаутов учебные. Пример показывает места договора, а не готовую production-конфигурацию.
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}\nproxy_set_header Host $host задаёт значение явно. В этом примере X-Real-IP $remote_addr передаёт приложению адрес непосредственного peer текущего Nginx; он не становится адресом клиента сам по себе. X-Forwarded-For расширяет цепочку, а не доказывает, что каждый её элемент заслуживает доверия. X-Forwarded-Proto $scheme описывает схему входного hop-а именно этого Nginx. Если перед ним есть другой TLS-терминатор, контракт должен учитывать его. Нельзя копировать строку https только потому, что внешняя страница открывается по HTTPS.
Таймауты отвечают на разные паузы. proxy_connect_timeout относится к установлению соединения с upstream. proxy_send_timeout относится к последовательным операциям записи запроса. proxy_read_timeout относится к паузе между последовательными операциями чтения ответа. Поэтому длительность ответа и read timeout — разные величины. Streaming может длиться дольше лимита, если upstream регулярно отправляет данные. Ответ без следующего байта может оборваться раньше, чем команда ожидает.
Для первого разбора достаточно access-log с URI, итоговым status, $request_time, адресом upstream, его status и временами подключения, получения заголовков и ответа. Такой формат отвечает на узкий вопрос: proxy не подключился к upstream или подключился, но не дождался заголовков. Не нужно записывать cookie, authorization, тело запроса или полный query string. Лишние данные не делают гипотезу точнее.
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;\nСинтетическая строка ниже показывает, как читать поля. Она учебная и не получена от реального Nginx.
\nGET /health status=504 request=15.001 upstream=<backend> upstream_status=-\nconnect=0.001 header=- response=15.001\nСтрока поддерживает гипотезу: соединение установилось быстро, но этот Nginx не получил заголовки ответа до заданного proxy_read_timeout 15s. Она не доказывает причину внутри приложения. Следующий шаг — проверить конкретный endpoint и безопасный токен в логе приложения. Если записи нет, это неизвестное, а не повод сочинять её содержание.
location, proxy_pass и все proxy_set_header. Смотреть нужно собранную конфигурацию, а не только фрагмент include-файла.Эта модель не делает forwarded-заголовок безопасным. Доверие задаёт топология и конфигурация. В Nginx realip-модуле адрес клиента меняется только для источников, которые явно разрешены через set_real_ip_from. Если прямой запрос может обойти доверенный proxy, приложение должно считать такие поля недоверенными или закрыть этот путь на сети.
Модель также не выбирает таймауты за команду. Значение зависит от типа endpoint, бюджета клиента, лимита приложения, поведения upstream и числа одновременных соединений. Длинный timeout может уменьшить число быстрых ошибок, но увеличить очередь и расход ресурсов. Retry добавляет ещё одну попытку и может повторить неидемпотентное действие. Его нельзя добавлять как универсальное средство против 504.
\nОтрицательный путь обязателен. Если после явного Host приложение всё ещё строит неверную ссылку, проверяют поле, которое читает код, и предыдущий TLS-терминатор. Если после изменения read timeout status не изменился, проверяют маршрут, доступность upstream и клиентский timeout. Если адрес остаётся адресом proxy, проверяют доверенную цепочку и прямой доступ. «Перезапустили — стало нормально» не подтверждает ни одну из этих гипотез.
Граница готова, если команда может повторить один безопасный запрос и получить доказательства по каждому hop-у: внешний status и схема, итоговый набор переданных headers, запись proxy с request/upstream-временами и согласованная запись приложения. Для адреса известен список доверенных proxy. Для каждого timeout названа конкретная пауза. Отрицательный тест с недоверенным прямым header не меняет схему, права или адрес клиента. Если хотя бы одно условие не проверено, конфигурация ещё не готова к переносу в рабочий контур.
\nВсе конфигурации, команды и значения времени в статье учебные. Nginx, curl, staging и production здесь не запускались. Перед применением нужно проверить версию Nginx, реальную топологию, владельца конфигурации, политику журналирования и возможность безопасного отката.
\nproxy_pass, proxy_set_header и таймаутам proxy.log_format, access_log и журналированию запросов.