DEV Community

Max Core
Max Core

Posted on • Edited on

13 1 1

Базовый ультимативный гайд по 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

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

Speedy emails, satisfied customers

Postmark Image

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (2)

Collapse
 
Sloan, the sloth mascot
Comment deleted

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs