1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6

# теория · шаг 1 из 6

Фазы: кто и когда решает

Коротко

Запрос идёт по nginx не сверху вниз по конфигу, а по фазам, и порядок фаз задан жёстко: server rewrite → выбор location → rewrite → лимиты → доступ → try_files → контент → фильтры.

  • return срабатывает раньше deny, а return в server {} - раньше выбора location.
  • Смена URI отправляет запрос назад к выбору location; кругов не больше десяти.
  • По порядку записи выполняются только return, rewrite, set, break и if; остальное - настройки уровня.

Если это и так знакомо - листай до «Теперь сам» в конце и проверь себя на конфиге.

Что получит этот запрос

Прежде чем читать дальше, ответь себе. Конфиг ниже выглядит совершенно однозначно. Что вернётся на запрос к /admin/ с чужого адреса?

/etc/nginx/conf.d/shop.local.confhttp server
location /admin/ {
    deny all;
    return 200 "тут ничего нет\n";
}

Не 403. Клиент получит 200 и текст.

Если ответ показался очевидным и неправильным - хорошо, это самое полезное место урока. Дальше разбираемся, почему так, и почему это не баг nginx.

На что это похоже

Конфиг выглядит как список команд, и читается как список команд. На самом деле это анкета, которую по очереди читают несколько разных служб.

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

На одном листе можно написать и «выдать справку», и «этого не пускать». Но лист идёт по окнам, и первое окно закончит дело раньше, чем второе успеет на него посмотреть. Резолюция «не пускать» останется неисполненной - не потому, что её кто-то отменил, а потому что до второго окна лист не доехал.

return живёт в первом окне. deny - во втором.

Механизм: конвейер фаз

nginx обрабатывает запрос восемью фазами. Каждая - отдельный этап со своим набором модулей, и попасть на этап раньше своей очереди директива не может.

server rewrite выбор location rewrite preaccess access try_files content filters return / rewrite / set / if поиск по URI то же, но внутри location limit_req, limit_conn allow / deny, auth_basic есть ли файл на диске root, proxy_pass add_header, expires, gzip last: до 10 кругов
Директива не может сработать раньше своей фазы, как бы высоко она ни стояла в файле. Смена URI отправляет запрос назад, к выбору location.

Правило

Три формулировки, которые стоит унести дословно - из них выводится почти любой спорный случай.

return в location бьёт всё, что ниже по конвейеру. Он в фазе rewrite - раньше лимитов, раньше проверки доступа, раньше обращения к диску.

return в server {} бьёт вообще всё, включая выбор location. Он в первой фазе, а location выбирается во второй.

По порядку записи выполняются только директивы модуля rewrite: return, rewrite, set, break и блоки if. Прерывают этот порядок return (запрос закончен), break (хватит переписывать, работаем в этом же location) и флаг last (URI сменился, ищем location заново).

Разбор: почему 200, а не 403

Пройдём тот самый конфиг по фазам.

/etc/nginx/conf.d/shop.local.confhttp server
location /admin/ {
    deny all;
    return 200 "тут ничего нет\n";
}
  1. server rewrite - в server {} директив модуля rewrite нет, фаза пуста.
  2. выбор location - URI /admin/ попадает в location /admin/.
  3. rewrite - здесь есть return 200. Он выполняется, ответ сформирован, обработка запроса закончена.
  4. Фазы preaccess, access, try_files, content не выполняются вовсе. deny all не «отменён» и не «проигнорирован» - до него не дошла очередь.

Клиент получает 200. Заметь, что порядок строк в файле роли не сыграл: если поменять deny и return местами, результат тот же.

Как надо, если цель - закрыть админку:

/etc/nginx/conf.d/shop.local.confhttp server
location /admin/ {
    deny all;                    # фаза access, и в этом location больше ничего
}

location = /admin/zaglushka {
    return 200 "тут ничего нет\n";
}

Заглушка уехала в отдельный location, и теперь deny доживает до своей фазы.

Разбор: где ломается наивное правило

Из первого разбора легко унести упрощённое «что написано выше в файле, то и сработает». Вот конфиг, на котором это правило даёт неверный ответ.

/etc/nginx/conf.d/shop.local.confhttp server
limit_req_zone $binary_remote_addr zone=api:10m rate=1r/s;   # ограничитель частоты, раздел про лимиты

location /old-api/ {
    rewrite ^/old-api/(.*)$ /api/$1 last;   # (.*) захватывает хвост в $1
}

location /api/ {
    limit_req zone=api burst=5;
    proxy_pass http://app:8000;
}

Вопрос: к какому лимиту относится запрос на /old-api/cart?

Наивный ответ - «ни к какому, в /old-api/ лимита нет». Правильный - к лимиту /api/. Фаза rewrite идёт раньше preaccess, флаг last отправляет запрос назад к выбору location, и на фазу лимитов запрос приходит уже с URI /api/cart, то есть из другого location.

То же самое работает и в обратную сторону, и это чаще кусается: запрос, переписанный ИЗ защищённого location в незащищённый, лимит потеряет.

Что ломается без этого

Три строки, которые приводят в лог и в чужой конфиг. За каждой стоит именно порядок фаз.

403 не наступает, хотя deny написан. Рядом в том же location стоит return - разобрано выше. Симптом коварен тем, что curl отдаёт содержимое, и проверка «страница открывается» проходит как успешная.

location /assets/ никогда не срабатывает. Смотри выше по файлу: в том же server {} стоит return 301. Он в первой фазе, location выбирается во второй, и до второй дело не доходит:

/etc/nginx/conf.d/shop.local.confhttp
server {
    listen 80;
    server_name shop.local;
    return 301 https://shop.local$request_uri;

    location /assets/ {
        root /var/www/shop;      # никогда не сработает
    }
}

500 и rewrite or internal redirection cycle в error_log. Круг «сменили URI - ищем location заново» повторился одиннадцать раз. Тот же счётчик тратят try_files с последним аргументом-URI, error_page и именованные location - поэтому конфиг, где try_files ведёт в location, который снова делает try_files, упирается в предел незаметно, до первого запроса по неудачному пути. Строка в логе выглядит как проблема бэкенда, хотя бэкенд ни при чём.

Зачем это в работе

Типовая задача с ревью: «закрыть админку по адресу». Человек дописывает deny all и рядом вежливую заглушку return 200, проверяет curl - страница отвечает, значит конфиг жив, - и отправляет в прод. Дыра уезжает вместе с коммитом, и найдут её не тестом, а по чужому запросу в логах.

Второй случай - разбор инцидента ночью. В логе 500 и rewrite or internal redirection cycle, дежурный идёт смотреть бэкенд, потому что 500 «обычно оттуда». Знание про десять кругов экономит здесь не аккуратность, а час ночного времени.

Как посмотреть, что происходит на самом деле

nginx -T печатает собранный конфиг целиком - со всеми include, в том виде, в каком его видит сам nginx. А rewrite_log on; вместе с error_log на уровне notice пишет каждую сработавшую перезапись:

$  команда
rewrite_log on;   # пишет в error_log на уровне notice (ниже warn) - при error_log warn не видно

Строки вида "^/shop/(.*)$" matches "/shop/socks", client: ... избавляют от гадания, какая именно регулярка сработала и в каком порядке.

Теперь сам

Что получит запрос GET /cart с адреса 203.0.113.10?

/etc/nginx/conf.d/shop.local.confhttp server
location /cart {
    allow 192.168.1.0/24;   # /24 - подсеть из 256 адресов 192.168.1.*
    deny all;
    rewrite ^/cart$ /korzina last;
}

location /korzina {
    return 200 "корзина\n";
}

Ответ: 200 и текст «корзина». Фаза rewrite идёт раньше access, поэтому rewrite ... last выполняется первым, URI меняется на /korzina, и поиск location начинается заново. Запрос приходит в location /korzina, где никаких allow/deny нет, - проверка доступа из первого location до него так и не добралась. Чтобы ограничение работало, deny должен стоять в том location, куда запрос в итоге приезжает, либо перезапись должна идти без last, с break.

Главное

Конфиг - не скрипт, а анкета для восьми служб, читающих её по очереди. Место директивы в файле не значит ничего; значит только фаза, в которой она живёт. Поэтому вопрос «почему не сработало» почти всегда сводится к одному: до этой директивы дошла очередь или дело закрыли раньше?

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.