Базовый ультимативный гайд по Nginx

maxcore

Max Core

Posted on December 30, 2023

Базовый ультимативный гайд по Nginx

Ниже — две части.

  1. База — её можно быстро пролистать.
  2. Боевая имплементация — там то, что мы хотим на практике.

Важно учесть, что настройка Nginx — не только для "наших" задач.
Все наши фреймворки, их middleware и т.д. — тоже рассчитывают на достоверные заголовки. Для корректной работы их autoban и т.д.

База. Зачем нужен Nginx. Логика X-Forwarded-For.

Тестировать будем на starlette.

Создадим простой main.py, который просто рисует http-мету:

from starlette.applications import Starlette
from starlette.responses import HTMLResponse
from starlette.routing import Route

async def home(request):
    return HTMLResponse(f"""<pre>
        url: {str(request.url)}
        client:
            host: {request.client.host}
            port: {request.client.port}
        headers:
            {('<br>' + '&nbsp' * 12).join(k + ': ' + v for k, v in request.headers.items())}
    </pre>""")

app = Starlette(debug=True, routes=[Route('/', home),])
Enter fullscreen mode Exit fullscreen mode

Его можно запустить на сервере:

uvicorn main:app --host=0.0.0.0 --port=2100
Enter fullscreen mode Exit fullscreen mode

И, если DNS настроены:

  1. Его уже можно открыть в браузере по http://myproject.org:2100
  2. Он уже будет знать:
    • IP-пользователя в request.client.host вида 'xxx.xxx.xxx.xxx'.
    • Host-Cервера в request.headers.host вида 'myproject.org:2100'.
  3. Даже ssl/https можно прикрутить.

И если обращаться к серверу прям так — http://myproject.org:2100 — без прокси, без VPN-ов — как минимум с заголовками — всё будет нормально.

Но:

  1. Возможно — нам бы хотелось иметь возможность как-то знать о прокси/VPN-ах.
  2. Возможно — нам также хотелось бы — мапить/перемапливать урлы (чтобы по 'myproject.org/api/' открывалось 'api.org/take/some/api/dude/'); эффективней обслуживать статику (не приложением); контролировать — таймауты, размеры файловых аплоудов, размеры текстовых аплоудов; иметь возможность показать пользователю хоть что-то, пока сервер лежит/перезапускается; клёвое логирование и т.д. и т.п.

А главное что нам хотелось бы — делать всё это через одну привычную технологию, на чём бы мы не писали наше приложение.

Nginx

Сэмулируем проксирование самих себя (может это мы сами для наших нужд):

server {
    listen 2200;
    location / {
        proxy_pass http://myproject.org:2100;
    }
}
Enter fullscreen mode Exit fullscreen mode

Открываем браузер по http://myproject.org:2200.
В request.client.host лежит уже не IP-Пользователя, а IP-Cервера, который к нам обратился.
Скажем сразу — если доступа к этому конфигу у нас нет — многого мы не сделаем.

Но, можем отловить "чуть более верхнего инициатора запроса" добавив "подконтрольный" нам конфиг:

server {
    listen 2100;
    location / {
        proxy_pass http://myproject.org:2000;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
Enter fullscreen mode Exit fullscreen mode

Запускаемся (но уже с портом 2000):

uvicorn main:app --host=0.0.0.0 --port=2000
Enter fullscreen mode Exit fullscreen mode

Открываем браузер также по http://myproject.org:2200.
Теперь наш сервис дополнительно отображает нам некий request.headers['x-real-ip'] с IP отличающимся от request.client.host.
И если в request.client.host у нас IP-Сервера, то в request.headers['x-real-ip'] похоже IP-Роутера-Сервера. (Но это не важно).

А вот если бы тот первый конфиг был нам подконтролен (или просто настроен хорошо), то мы могли бы в обоих случаях добавить:

server {
    listen 2200;
    location / {
        proxy_pass http://myproject.org:2100;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;  # +
        proxy_set_header Host $http_host;  # +
    }
}

server {
    listen 2100;
    location / {
        proxy_pass http://myproject.org:2000;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;  # +
        proxy_set_header Host $http_host;  # +
    }
}
Enter fullscreen mode Exit fullscreen mode

И теперь, в http://myproject.org:2200 мы получим:

  1. В request.headers.host лежит всё как надо — 'myproject.com:2200'.
  2. В request.client.host всё также лежит IP-Сервера.
  3. В request.headers['x-real-ip'] лежит всё тот же IP-Роутера-Сервера.
  4. Но, появился ещё request.headers['x-forwarded-for'], и это — цепочка IP-шников! Потерянный IP-Пользователя и IP-Роутера-Сервера.

Т.е., самый достоверный IP-пользователя — это всегда — request.headers['x-forwarded-for'][0].

Боевая имплементация

Нам конечно не нужны такие двойные конфиги.
Просто только так можно было понять 'X-Forwarded-For'.
Что и в какой степени достоверно.

И вот перед нами просто "голый" конфиг:

server {
    listen 2200;
    location / {
        proxy_pass http://headhall.com:2000;
    }
}
Enter fullscreen mode Exit fullscreen mode

По http://myproject.org:2200 мы получим:
Который показывает:

  1. В request.headers.host лежит 'myproject.org:2000', хотя браузер открыт с ':2200'.
  2. В request.client.host — IP-Сервера, а не IP-Пользователя.
  3. request.headers['x-real-ip'] вообще нет.
  4. request.headers['x-forwarded-for'] также нет.

Приводим конфиг в боевое состояние:

server {
    listen 2200;
    location / {
        # Чтобы показывал :2200
        proxy_set_header Host $http_host;

        # Пытаемся получить IP-Пользователя (и получим, если без прокси)
        proxy_set_header X-Real-IP $remote_addr;

        # Ещё пытаемся получить IP-Пользователя (и получим, если прокси наше или "нормальное")
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_pass http://headhall.com:2000;
    }
}
Enter fullscreen mode Exit fullscreen mode

(Вместо $http_host конечно можно — и $host (без порта), и $server_name (если хочется) и т.д. Просто $http_host самый полный и универсальный.).
(К слову, все эти proxy_* можно указать и на уровне server).

Закрепим, что мы получим в http://myproject.org:2200:

  1. В request.headers.host лежит гарантированный 'myproject.org:2200'.
  2. В request.client.host — IP-Сервера, а не IP-Пользователя, как можно было ожидать — всегда НЕ надёжный ориентир.
  3. request.headers['x-real-ip'] — IP-Пользователя, но, всё же — тоже НЕ надёжный ориентир.
  4. request.headers['x-forwarded-for'][0] — IP-Пользователя, как — наиболее надёжный ориентир.

А вот теперь можно уже к — таймаутам, лимиту аплоудов и т.д.

В помощь по всему что выше:
Alphabetical index of variables: https://nginx.org/en/docs/varindex.html
Злой, но золотой комментарий на SO: https://stackoverflow.com/a/72586833/4117781

Удачи, авантюрист!

💖 💪 🙅 🚩
maxcore
Max Core

Posted on December 30, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related