Веб-серверы и Reverse Proxy: Nginx до последней директивы

Nginx — это «выходные двери» 90% веб-приложений мира. Балансер, кэш, статика, TLS, WAF, rate limit — всё под одной крышей. Этот урок — детальное руководство: от установки до production-конфигов, без воды и без зубрёжки.

В этом уроке
  1. Что такое веб-сервер, app server и reverse proxy
  2. Почему именно Nginx
  3. Архитектура Nginx — master и workers
  4. Установка и запуск
  5. Структура конфигурации
  6. Server blocks — виртуальные хосты
  7. Location matching — самая частая ошибка
  8. Отдача статики: root, alias, try_files
  9. Reverse proxy — proxy_pass до байта
  10. Заголовки при проксировании
  11. Upstream и алгоритмы балансировки
  12. Буферизация и таймауты
  13. Кэширование
  14. SSL/TLS — production-grade
  15. HTTP/2 и HTTP/3
  16. Сжатие: gzip и brotli
  17. Rate limiting
  18. WebSocket и gRPC
  19. Логи
  20. Security headers
  21. Тюнинг производительности
  22. Hot reload и graceful restart
  23. Реальные конфиги
  24. Антипаттерны
  25. Дебаг
  26. Альтернативы Nginx
  27. Что с этим делает DevOps
  28. Nginx Ingress Controller в Kubernetes
  29. stream{} — TCP/UDP проксирование
  30. fastcgi_pass и uwsgi_pass
  31. OpenResty и Lua
  32. NJS — JavaScript внутри Nginx
  33. ModSecurity + OWASP CRS — встроенный WAF
  34. Mutual TLS — клиентские сертификаты
  35. GeoIP и split_clients
  36. Кейс troubleshooting: 502 Bad Gateway
  37. Open Source vs Nginx Plus
  38. Чек-лист

1. Что такое веб-сервер, app server и reverse proxy

Слова «веб-сервер», «приложение», «прокси» путаются. Чёткое разделение:

🌐 Веб-сервер

Слушает HTTP, отдаёт статический контент (HTML, CSS, JS, картинки) с диска. Когда-то это была главная функция. Примеры: Apache, Nginx, IIS, Caddy.

🛠️ Application server

Выполняет твой код приложения: Python (uWSGI/Gunicorn), Node.js, Java (Tomcat), Go (встроенный net/http), Ruby (Puma). Не оптимизирован для отдачи статики.

🔁 Reverse proxy

Принимает запросы от клиентов и проксирует их в app server. Между ними — кэш, TLS-терминация, балансировка, фильтрация.

🎯 Все три в одном

Nginx умеет быть всеми тремя одновременно: статику отдаёт сам, динамику проксирует, между ними кэш, на входе TLS+WAF.

Forward vs reverse proxy — частая путаница:

2. Почему именно Nginx

Игорь Сысоев написал Nginx в 2004 году как замену Apache, который захлёбывался под нагрузкой. Главные плюсы:

По данным W3Techs (2024): Nginx обслуживает 34% всех веб-сайтов. Среди топ-100к — почти 50%. Знать его — обязательно для DevOps.

3. Архитектура Nginx — master и workers

Nginx работает по модели master + N workers:

Master + Worker архитектура Nginx Master process читает конфиг, биндит порты управляет worker'ами Worker 1 event loop (epoll) CPU core 1 Worker 2 event loop CPU core 2 Worker 3 event loop CPU core 3 Worker 4 event loop CPU core 4 10К конн 10К конн 10К конн 10К конн Каждый worker — отдельный процесс. Слушают одни и те же порты через SO_REUSEPORT. Падение worker'а — master запустит новый, остальные продолжают работать.

Master process

Worker process

Зачем именно так

Master/worker даёт два больших плюса:

  1. Безопасность. Worker работает без root — даже если уязвимость, привилегий минимум.
  2. Hot reload. Master запускает новые workers с обновлённым конфигом, старые дорабатывают существующие соединения и завершаются. Никаких сброшенных запросов.

4. Установка и запуск

# Ubuntu / Debian
sudo apt update
sudo apt install nginx

# RHEL / Rocky / Alma
sudo dnf install nginx

# Свежая версия (mainline) с официального репозитория
# https://nginx.org/en/linux_packages.html

# Запуск
sudo systemctl enable --now nginx
sudo systemctl status nginx

# Базовые команды
sudo nginx -t              # проверить конфиг — ОБЯЗАТЕЛЬНО ПЕРЕД РЕЛОУДОМ
sudo nginx -s reload       # горячая перезагрузка конфига
sudo nginx -s reopen       # переоткрыть лог-файлы (для logrotate)
sudo nginx -s stop         # немедленная остановка
sudo nginx -s quit         # graceful shutdown
nginx -V                   # версия + флаги сборки

После установки Nginx уже работает. По умолчанию слушает на 80, отдаёт «Welcome to nginx!» из /var/www/html/.

5. Структура конфигурации

Главный файл — /etc/nginx/nginx.conf. Внутри — контексты (блоки) и директивы.

# MAIN context (глобальный, без обёртки)
user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
pid /run/nginx.pid;

# EVENTS context — настройки event loop
events {
    worker_connections 4096;       # макс конн на worker
    multi_accept on;
    use epoll;                     # на Linux — оптимально
}

# HTTP context — всё, что касается HTTP-уровня
http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    sendfile on;                   # нулевая копия для статики
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;

    gzip on;
    gzip_types text/plain text/css application/json application/javascript;

    # Включить все конфиги сайтов
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;

    # SERVER context — виртуальный хост
    server {
        listen 80;
        server_name example.com www.example.com;

        # LOCATION context — правило для конкретного path
        location / {
            root /var/www/html;
            index index.html;
        }

        location /api/ {
            proxy_pass http://127.0.0.1:8080;
        }
    }
}

Иерархия контекстов

main
├── events
└── http
    ├── upstream
    └── server   (один на каждый виртуальный хост)
        └── location  (одна или много)
            └── location  (вложенные тоже можно)

Директивы наследуются вниз. То, что определено в http, действует во всех server. Но более глубокий контекст может переопределить.

Структура файлов на Ubuntu/Debian

/etc/nginx/
├── nginx.conf              # главный
├── mime.types
├── conf.d/                 # дополнительные http-блоки
├── sites-available/        # все конфиги сайтов (хранение)
│   ├── default
│   └── example.com
├── sites-enabled/          # symlinks на включённые
│   └── example.com -> ../sites-available/example.com
└── snippets/               # переиспользуемые куски
    ├── snakeoil.conf
    └── fastcgi-php.conf

Включить сайт = ln -s из sites-available в sites-enabled. Выключить = удалить ссылку.

6. Server blocks — виртуальные хосты

Один Nginx обслуживает много сайтов на одном IP. Различает по заголовку Host.

server {
    listen 80;
    server_name example.com;
    root /var/www/example.com;
}

server {
    listen 80;
    server_name blog.example.com;
    root /var/www/blog;
}

server {
    listen 80 default_server;        # дефолт, если Host не подошёл
    server_name _;                   # «catch all»
    return 444;                      # Nginx-specific: тихий разрыв
}

server_name — нюансы

Алгоритм выбора server

  1. Совпадение по IP:port из listen
  2. Точное совпадение server_name
  3. Wildcard, начинающийся с *
  4. Wildcard, заканчивающийся на *
  5. Regex (в порядке появления в конфиге)
  6. Если ничего не подошло — default_server для этого listen

Без default_server — первый server в порядке появления становится дефолтным. Поэтому если у тебя 50 server-блоков, и первый — «catch-all» с return 444, никакой другой работать не будет. Будь внимателен с порядком.

7. Location matching — самая частая ошибка

Внутри server — блоки location, каждый описывает поведение для какого-то path. Это самая запутанная часть Nginx.

Модификаторы и приоритет

СинтаксисТипПриоритет
location = /exactТочное совпадение1 (самый высокий)
location ^~ /prefixПрефиксное, остановить поиск regex2
location ~ /regexRegex case-sensitive3 (в порядке)
location ~* /regexRegex case-insensitive3 (в порядке)
location /prefixПрефиксное (без модификатора)4 (самый длинный выигрывает)

Алгоритм выбора

  1. Если есть location = /path и он точно совпал — взять, остановиться
  2. Найти самый длинный префикс из location / и location ^~ /
  3. Если найден ^~ — взять, остановиться
  4. Иначе — попробовать regex'ы в порядке появления. Первый match → взять
  5. Если regex не нашёлся — использовать самый длинный префикс из шага 2

Пример с разбором

server {
    listen 80;
    server_name example.com;

    location = / {                       # только GET /
        return 200 "homepage\n";
    }

    location / {                         # всё остальное
        proxy_pass http://app;
    }

    location ^~ /static/ {               # /static/* — без regex-проверки
        root /var/www;
        expires 1y;
    }

    location ~* \.(jpg|png|gif)$ {       # любые картинки
        root /var/www/images;
        expires 30d;
    }
}

Что произойдёт:

8. Отдача статики: root, alias, try_files

Два способа сказать «бери файлы отсюда»:

root

location /images/ {
    root /var/www;                       # файл: /var/www/images/cat.png
}

root приклеивается к полному URI. Запрос /images/cat.png → файл /var/www/images/cat.png.

alias

location /images/ {
    alias /var/www/pics/;                # файл: /var/www/pics/cat.png
}

alias заменяет часть URI, совпавшую с location. /images/cat.png/var/www/pics/cat.png (без /images/).

Грабли с alias

С alias path в location должен заканчиваться слэшем ровно как у alias. Иначе ловишь странные баги. С root такой проблемы нет.

Правило: «не уверен — используй root».

try_files — главная директива для SPA

location / {
    root /var/www/myapp;
    try_files $uri $uri/ /index.html;
}

Что делает: пробует $uri (например /dashboard) → если нет файла, пробует /dashboard/ → если и его нет, возвращает /index.html.

Это стандарт для SPA (React/Vue/Angular): любой путь приводит к index.html, дальше JS-роутер сам разберётся.

Заголовки для статики

location /static/ {
    root /var/www;
    expires 1y;                          # Cache-Control: max-age=31536000
    add_header Cache-Control "public, immutable";
    access_log off;                      # не логировать каждый запрос — экономия
}

9. Reverse proxy — proxy_pass до байта

Главный модуль Nginx для DevOps: proxy_pass. На вид просто, но имеет нюанс, на котором ломаются 80% инженеров.

Базовый proxy

location /api/ {
    proxy_pass http://127.0.0.1:8080;
}

Со слэшем и без — критическая разница

Без слэша на конце

location /api/ {
    proxy_pass http://app:8080;
}

Клиент: GET /api/users → upstream: GET /api/users

Со слэшем

location /api/ {
    proxy_pass http://app:8080/;
}

Клиент: GET /api/users → upstream: GET /users (без префикса!)

То есть слэш на конце proxy_pass «отрезает» префикс location при проксировании. Это спасает, когда твоё приложение слушает на корне /, а наружу ты выставляешь под /api/.

Правило: если у upstream'а есть префикс, такой же как у location, — пиши без слэша. Иначе — со слэшем.

proxy_pass с переменными

Если в proxy_pass есть переменная — нужно явно указать resolver и upstream:

resolver 8.8.8.8 valid=300s;

location /proxy/ {
    set $backend "https://api.example.com";
    proxy_pass $backend;                 # DNS будет ре-резолвится
}

Без переменной Nginx резолвит DNS один раз при старте. Если IP upstream'а меняется (например, K8s service) — старый IP останется. Поэтому в облачных средах часто используют переменные.

10. Заголовки при проксировании

По умолчанию Nginx меняет некоторые заголовки. Самое важное:

Host

Без proxy_set_header Host, Nginx ставит Host: 127.0.0.1:8080 (адрес upstream'а). Это путает приложение, которое генерирует URL по Host. Всегда задавай явно:

proxy_set_header Host $host;

Реальный IP клиента

За прокси приложение видит как client IP — IP прокси. Чтобы передать настоящий:

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;            # http / https
proxy_set_header X-Forwarded-Host $host;

$proxy_add_x_forwarded_for — это специальная переменная, которая берёт существующий X-Forwarded-For и добавляет к нему IP клиента. Поддерживает цепочку прокси.

Дефолтные настройки в snippet'е

Создай /etc/nginx/snippets/proxy_params с общими настройками:

proxy_http_version 1.1;
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 X-Forwarded-Host $host;
proxy_set_header Connection "";                # для keepalive upstream

proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;

И в server-блоках:

location /api/ {
    include /etc/nginx/snippets/proxy_params;
    proxy_pass http://app:8080;
}

Это DRY — не дублируешь в каждом блоке.

11. Upstream и алгоритмы балансировки

До сих пор мы проксировали на один адрес. Но обычно за прокси — пул серверов. Для этого — upstream block:

upstream backend {
    server 10.0.1.10:8080 weight=5;
    server 10.0.1.11:8080 weight=5;
    server 10.0.1.12:8080 weight=1 backup;       # резервный
    server 10.0.1.13:8080 down;                  # временно отключён

    keepalive 32;                                # пул keep-alive
    keepalive_timeout 60s;
}

server {
    location / {
        proxy_pass http://backend;
        proxy_set_header Connection "";          # обязательно для keepalive
    }
}

Алгоритмы

ДирективаАлгоритмКогда применять
(дефолт)Round-robin с весамиУниверсально
least_conn;Меньше всего активных коннектовДолгие запросы / WebSocket
ip_hash;По хэшу клиентского IPSticky sessions
hash $request_uri;По хэшу любой переменнойРаспределение по URL — для кэшей
hash $request_uri consistent;Consistent hashing (ring)Дешёвая ре-балансировка
random two least_conn;Power of two choicesСовременный, эффективный

Health check — passive

OSS Nginx поддерживает только passive health check: если запрос к upstream упал — пометить как недоступный на N секунд.

upstream backend {
    server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
    server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
}

3 ошибки за 30 секунд → исключаем из ротации на 30 секунд → пробуем снова.

Active health check (фоновые запросы) — только в Nginx Plus. В OSS — используют nginx_upstream_check_module (форкали Tengine) или alternative reverse-proxy.

12. Буферизация и таймауты

Буферизация

По умолчанию Nginx буферизует ответ от upstream'а: принимает в память (или диск), а потом отдаёт клиенту. Это позволяет освободить upstream быстро — пока медленный клиент тащит ответ через 3G, upstream уже свободен.

proxy_buffering on;                              # дефолт
proxy_buffer_size 4k;                            # для заголовков ответа
proxy_buffers 8 16k;                             # 8 буферов по 16K
proxy_busy_buffers_size 32k;
proxy_max_temp_file_size 1024m;                  # макс на диске

Когда буферизацию отключить

Для streaming ответов (SSE, long polling, gRPC, LLM streaming) — буферизация всё ломает: клиент видит ответ только когда весь готов.

location /events {
    proxy_pass http://app;
    proxy_buffering off;                         # отключаем
    proxy_cache off;
    proxy_set_header Connection "";
    proxy_http_version 1.1;
}

Таймауты — самая частая ошибка

Дефолты Nginx — 60 секунд. Для production почти всегда нужно подкрутить:

proxy_connect_timeout  5s;                       # соединиться с upstream
proxy_send_timeout     30s;                      # отправить запрос
proxy_read_timeout     30s;                      # получить ответ

# Для long polling / SSE
proxy_read_timeout 3600s;                        # час

Каскадно: client → Nginx → upstream → БД. Каждый следующий таймаут короче предыдущего, иначе хвосты накапливаются.

Retry на следующий upstream

proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 10s;

«Если ошибка timeout/502/503/504 — попробуй другой upstream, но не более 2 раз и не более 10 секунд суммарно».

⚠️ Retry на POST

По умолчанию Nginx делает retry только для идемпотентных методов (GET, HEAD). Можно включить для POST через proxy_next_upstream_tries + non_idempotent, но это рискованно — двойное списание денег и т.д.

13. Кэширование

Nginx умеет кэшировать ответы upstream'а. Это превращает его в полноценный CDN local-уровня.

http {
    proxy_cache_path /var/cache/nginx/cache
        levels=1:2
        keys_zone=app_cache:100m
        max_size=10g
        inactive=60m
        use_temp_path=off;

    server {
        location / {
            proxy_pass http://app;

            proxy_cache app_cache;
            proxy_cache_key "$scheme$request_method$host$request_uri";
            proxy_cache_valid 200 302 10m;
            proxy_cache_valid 404 1m;
            proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
            proxy_cache_lock on;                  # не толкать сразу 100 запросов в upstream
            proxy_cache_background_update on;

            add_header X-Cache-Status $upstream_cache_status;
        }
    }
}

Что это даёт

Cache key

По умолчанию ключ — это URL. Но часто нужно отдельно кэшировать для разных пользователей или Accept-Encoding:

proxy_cache_key "$scheme$host$uri$is_args$args$http_authorization";

Будь осторожен — чем подробнее ключ, тем хуже cache hit rate.

Не кэшировать для авторизованных

proxy_cache_bypass $http_authorization $http_cookie;
proxy_no_cache $http_authorization $http_cookie;

Если есть Authorization или Cookie — пропустить кэш. Иначе можно отдать ответ одного пользователя другому. Это catastrophic.

14. SSL/TLS — production-grade

Современный production-конфиг TLS:

server {
    listen 443 ssl;
    http2 on;

    server_name example.com;

    # Сертификат + приватный ключ
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Только современные версии
    ssl_protocols TLSv1.2 TLSv1.3;

    # Шифры (Mozilla intermediate, актуально)
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # Session resumption
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 1.1.1.1 valid=60s;
    resolver_timeout 2s;

    # DH parameters для PFS (если RSA-сертификат)
    ssl_dhparam /etc/ssl/dhparam.pem;             # openssl dhparam -out ... 2048

    # HSTS
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    location / {
        proxy_pass http://app;
    }
}

# Редирект HTTP → HTTPS
server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

Готовый production-конфиг можно сгенерировать в ssl-config.mozilla.org и не писать руками — Mozilla обновляет рекомендации.

Получение сертификата

# Let's Encrypt через certbot
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

# Авто-обновление
sudo certbot renew --dry-run
# Cron / systemd timer — автоматически каждый день

15. HTTP/2 и HTTP/3

HTTP/2 в Nginx — одна директива:

server {
    listen 443 ssl;
    http2 on;                            # Nginx 1.25.1+
    # Старый синтаксис: listen 443 ssl http2;
    ...
}

HTTP/3

HTTP/3 требует QUIC — это UDP, не TCP. Поддержка в Nginx — с версии 1.25 mainline:

server {
    listen 443 ssl;
    listen 443 quic reuseport;           # QUIC по UDP/443
    http2 on;
    http3 on;

    ssl_protocols TLSv1.3;               # HTTP/3 требует TLS 1.3

    # Сказать клиенту: «у меня есть HTTP/3»
    add_header Alt-Svc 'h3=":443"; ma=86400';

    ...
}

Не забудь открыть UDP/443 в firewall. И на хосте — net.core.rmem_max хорошо бы увеличить для QUIC.

16. Сжатие: gzip и brotli

Gzip

gzip on;
gzip_vary on;                            # корректный Vary: Accept-Encoding
gzip_proxied any;                        # сжимать ответы от upstream
gzip_comp_level 6;                       # 1-9, 6 — баланс
gzip_min_length 256;                     # не сжимать совсем мелкое
gzip_types
    text/plain
    text/css
    text/xml
    application/json
    application/javascript
    application/xml+rss
    application/atom+xml
    image/svg+xml;

Brotli — лучше gzip

Brotli даёт на 20-30% лучшее сжатие при сравнимой скорости. Не входит в стандартный Nginx — нужен модуль:

sudo apt install libnginx-mod-http-brotli-filter libnginx-mod-http-brotli-static
brotli on;
brotli_comp_level 6;
brotli_static on;                        # отдавать предсжатые .br файлы
brotli_types
    text/plain text/css
    application/json application/javascript
    image/svg+xml;

Pre-compressed static — лайфхак: положи рядом с app.js файл app.js.br (сжатый максимальным уровнем). Nginx будет отдавать его без runtime-сжатия — бесплатное ускорение.

17. Rate limiting

Защита от ботов и DoS на L7:

http {
    # Зоны — память для счётчиков
    limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;       # строже для логина
    limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;

    server {
        # Общий лимит на IP — 10 req/s + burst 20
        location / {
            limit_req zone=general burst=20 nodelay;
            limit_req_status 429;
            proxy_pass http://app;
        }

        # Жёсткий лимит на логин — 5 попыток в минуту
        location /login {
            limit_req zone=login burst=2;
            proxy_pass http://app;
        }

        # Не больше 10 параллельных соединений с одного IP
        location /download/ {
            limit_conn conn_per_ip 10;
            proxy_pass http://app;
        }
    }
}

burst и nodelay — что это

Запоминать: burst без nodelay = latency. С nodelay = всплеск разрешён, но «израсходовал бюджет».

18. WebSocket и gRPC

WebSocket

WebSocket начинается как HTTP/1.1 запрос с Upgrade-заголовком. Nginx надо явно сказать, что нужно пропускать эти заголовки:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    location /ws/ {
        proxy_pass http://app;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_read_timeout 3600s;            # долгие сессии
    }
}

gRPC

gRPC поверх HTTP/2 — отдельный модуль grpc_pass:

server {
    listen 443 ssl;
    http2 on;

    location / {
        grpc_pass grpc://grpc_backend;
        grpc_set_header Host $host;
        grpc_read_timeout 60s;
    }
}

upstream grpc_backend {
    server grpc1:50051;
    server grpc2:50051;
}

Поддержка TLS до upstream'а — grpcs://.

19. Логи

Стандартные форматы

log_format main '$remote_addr - $remote_user [$time_local] '
                '"$request" $status $body_bytes_sent '
                '"$http_referer" "$http_user_agent"';

log_format detailed '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent" '
                    'rt=$request_time uct="$upstream_connect_time" '
                    'urt="$upstream_response_time" '
                    'cache=$upstream_cache_status';

access_log /var/log/nginx/access.log detailed;
error_log  /var/log/nginx/error.log warn;

JSON для structured logging (рекомендую)

log_format json_combined escape=json '{'
    '"time":"$time_iso8601",'
    '"remote_addr":"$remote_addr",'
    '"x_forwarded_for":"$http_x_forwarded_for",'
    '"method":"$request_method",'
    '"uri":"$request_uri",'
    '"status":$status,'
    '"body_bytes_sent":$body_bytes_sent,'
    '"referer":"$http_referer",'
    '"user_agent":"$http_user_agent",'
    '"request_time":$request_time,'
    '"upstream_connect_time":"$upstream_connect_time",'
    '"upstream_response_time":"$upstream_response_time",'
    '"upstream_status":"$upstream_status",'
    '"cache_status":"$upstream_cache_status"'
'}';

access_log /var/log/nginx/access.json json_combined;

JSON-логи легко парсятся Promtail/Vector/Fluent Bit в Loki/Elasticsearch — становятся structured field'ы.

Уровни error_log

debug, info, notice, warn, error, crit, alert, emerg. В production — warn или error. debug требует пересборки Nginx с --with-debug и забивает диск.

Logrotate

Дистрибутив обычно ставит /etc/logrotate.d/nginx сам. После ротации шлёт USR1 сигнал — Nginx переоткрывает лог-файлы. Аналог: nginx -s reopen.

20. Security headers

Минимальный набор для production:

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;

# CSP — главный, но требует настройки под сайт
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-...'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com" always;

Слово always в конце — отправлять заголовок даже для ошибочных ответов (4xx/5xx). Без него — часть пропадёт.

⚠️ add_header перезаписывается

В Nginx add_header в вложенном location полностью перезаписывает родительские. То есть в location /api/ { add_header X-Foo bar; } не наследуется HSTS из server-блока.

Лечится: дубликат всех заголовков в каждом location или модулем headers-more-nginx-module.

21. Тюнинг производительности

Параметры, которые реально двигают перформанс:

# Кол-во worker'ов = кол-во CPU cores
worker_processes auto;
worker_rlimit_nofile 65535;                   # макс файловых дескрипторов на worker

events {
    worker_connections 65535;                 # макс соединений на worker
    multi_accept on;                          # принимать все готовые соединения сразу
    use epoll;                                # Linux native
}

http {
    # Zero-copy для статики
    sendfile on;
    tcp_nopush on;                            # отправлять полные пакеты
    tcp_nodelay on;                           # но не задерживать маленькие

    # Keepalive с клиентами
    keepalive_timeout 65;
    keepalive_requests 1000;

    # Кэш файловых дескрипторов
    open_file_cache max=10000 inactive=20s;
    open_file_cache_valid 30s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;

    # Размеры буферов
    client_body_buffer_size 16k;
    client_max_body_size 10m;                 # макс upload
    client_header_buffer_size 1k;
    large_client_header_buffers 4 8k;
    output_buffers 2 32k;

    # Таймауты
    client_body_timeout 12;
    client_header_timeout 12;
    send_timeout 10;

    # Скрыть версию
    server_tokens off;
}

Ядро Linux под Nginx

# /etc/sysctl.conf

# Backlog очередей
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535

# TIME_WAIT
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30

# Buffer sizes
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

# Эфемерные порты
net.ipv4.ip_local_port_range = 1024 65535

# BBR congestion control
net.ipv4.tcp_congestion_control = bbr
net.core.default_qdisc = fq

# Применить
sudo sysctl -p

22. Hot reload и graceful restart

Nginx умеет менять конфиг без остановки и без потери соединений. Магия — в master-worker модели.

nginx -s reload

  1. Master читает новый конфиг, проверяет
  2. Если ок — запускает новые workers с новым конфигом
  3. Старые workers перестают принимать новые соединения
  4. Старые workers дорабатывают существующие соединения, потом завершаются
  5. На время — у тебя 2× worker'ов в памяти

Это zero-downtime. Никаких 502 во время reload.

nginx -s reopen

Переоткрыть лог-файлы (после logrotate). Workers не пересоздаются.

Upgrade бинарника без рестарта

Можно обновить сам бинарник Nginx без потери соединений (USR2 → master запускает новый master).

# Заменил бинарник /usr/sbin/nginx
sudo kill -USR2 $(cat /run/nginx.pid)
# Новый master запустился, старый ещё рядом

# Если всё ок — завершить старый
sudo kill -QUIT $(cat /run/nginx.pid.oldbin)

Это редко нужно (обычно systemd рестартует), но знать стоит — это пример того, как сделана продакшен-готовая инфраструктура.

23. Реальные конфиги

Single Page App (React/Vue)

server {
    listen 443 ssl;
    http2 on;
    server_name app.example.com;

    ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

    root /var/www/app/dist;
    index index.html;

    # Статические ассеты — кэшируем долго
    location ~* \.(js|css|woff2|webp|svg|png|jpg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        try_files $uri =404;
    }

    # API — проксируем на backend
    location /api/ {
        proxy_pass http://backend;
        include /etc/nginx/snippets/proxy_params;
    }

    # SPA-fallback — любой неизвестный путь → index.html
    location / {
        try_files $uri $uri/ /index.html;
        add_header Cache-Control "no-cache, no-store";
    }
}

Микросервисный API gateway

upstream users-service   { server users:8080;   keepalive 32; }
upstream orders-service  { server orders:8080;  keepalive 32; }
upstream payments-service { server payments:8080; keepalive 32; }

server {
    listen 443 ssl;
    http2 on;
    server_name api.example.com;

    include /etc/nginx/snippets/ssl_params;
    include /etc/nginx/snippets/security_headers;

    # Rate limit на api в целом
    limit_req zone=api burst=50 nodelay;

    location /users/ {
        proxy_pass http://users-service;
        include /etc/nginx/snippets/proxy_params;
    }

    location /orders/ {
        proxy_pass http://orders-service;
        include /etc/nginx/snippets/proxy_params;
    }

    location /payments/ {
        proxy_pass http://payments-service;
        include /etc/nginx/snippets/proxy_params;
        # Особо жёсткий лимит — нельзя ретраить
        limit_req zone=payments burst=5;
        proxy_next_upstream off;
    }

    location /healthz {
        access_log off;
        return 200 "ok\n";
    }
}

WebSocket-чат

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

upstream chat_backend {
    ip_hash;                                 # sticky — клиент держит один сервер
    server chat-1:3000;
    server chat-2:3000;
    server chat-3:3000;
}

server {
    listen 443 ssl;
    http2 on;
    server_name chat.example.com;
    include /etc/nginx/snippets/ssl_params;

    location /ws/ {
        proxy_pass http://chat_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        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_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }

    location / {
        root /var/www/chat;
        try_files $uri /index.html;
    }
}

Caching reverse proxy перед slow backend

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=catalog:200m max_size=20g inactive=24h use_temp_path=off;

upstream backend {
    server backend:8080;
    keepalive 32;
}

server {
    listen 443 ssl;
    http2 on;
    server_name shop.example.com;
    include /etc/nginx/snippets/ssl_params;

    location / {
        proxy_pass http://backend;
        include /etc/nginx/snippets/proxy_params;

        proxy_cache catalog;
        proxy_cache_key "$scheme$host$request_uri";
        proxy_cache_valid 200 1h;
        proxy_cache_valid 404 1m;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
        proxy_cache_lock on;
        proxy_cache_background_update on;
        proxy_cache_bypass $cookie_session;

        add_header X-Cache-Status $upstream_cache_status;
    }

    # Очистка по тегу (через ngx_cache_purge)
    location ~ /purge(/.*) {
        allow 127.0.0.1;
        deny all;
        proxy_cache_purge catalog "$scheme$host$1";
    }
}

24. Антипаттерны

25. Дебаг

# Проверка конфига
sudo nginx -t                            # синтаксис + проверка путей
sudo nginx -T                            # весь итоговый конфиг с include'ами

# Статус процессов
ps auxf | grep nginx
systemctl status nginx

# Сокеты слушаются?
ss -tlnp | grep nginx

# Связи к upstream'ам
ss -tan state established 'sport = :8080'

# Логи
tail -F /var/log/nginx/access.log
tail -F /var/log/nginx/error.log

# Только ошибки за последние X минут
journalctl -u nginx --since "10 minutes ago"

# Тест с разными host-headers
curl -v -H "Host: example.com" http://127.0.0.1/

# Какой server-блок отработает
nginx -T | grep -A5 "server_name example.com"

# Module stub_status — встроенная метрика
location /nginx_status {
    stub_status;
    allow 127.0.0.1;
    deny all;
}
# curl http://localhost/nginx_status
# Active connections: 291
# server accepts handled requests
#  16630948 16630948 31070465
# Reading: 6 Writing: 179 Waiting: 106

Debug log

Если нужно очень детально — debug-log. Требует Nginx с --with-debug:

nginx -V 2>&1 | grep -o with-debug

error_log /var/log/nginx/debug.log debug;

# Только для конкретного клиента
events {
    debug_connection 1.2.3.4;
}

26. Альтернативы Nginx

ИнструментОсобенностиКогда применять
CaddyАвтоматические TLS-сертификаты, простой конфиг, HTTP/3 из коробкиМаленькие проекты, домашние серверы
HAProxyЖёсткая фокусировка на балансировке, лучший на L4, продвинутый health checkФинтех, высокая нагрузка на чистый load balancing
EnvoyL7 для микросервисов, gRPC native, основа IstioService mesh, сложная маршрутизация
TraefikCloud-native, авто-discovery в Docker/K8sДинамические окружения
ApacheДревний, гибкий через .htaccess, но медленнееShared hosting, legacy
Nginx UnitApp + proxy в одном, поддержка runtimes (PHP, Python, Go)Замена php-fpm + nginx

Nginx остаётся стандартом для общего случая. В K8s часто берут Nginx Ingress Controller, в service mesh — Envoy.

27. Что с этим делает DevOps

28. Nginx Ingress Controller в Kubernetes

В K8s «голый» Nginx не разворачивают. Его берут в виде Ingress Controller — оператор, который превращает Kubernetes-объекты Ingress в реальную Nginx-конфигурацию.

Важное различие

Есть два разных проекта с похожими именами:

Они разные. Конфиги, аннотации, фичи — отличаются. Стандарт по умолчанию — ingress-nginx.

Установка через Helm

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx --create-namespace \
  --set controller.replicaCount=3 \
  --set controller.service.type=LoadBalancer

Базовый Ingress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api
                port:
                  number: 8080
  tls:
    - hosts: [api.example.com]
      secretName: api-tls

Annotations — главное оружие

Через annotations можно настраивать почти любую директиву Nginx, не редактируя ConfigMap. Самые частые:

AnnotationЧто делает
proxy-body-size: 100mclient_max_body_size
proxy-read-timeout: 60timeout до ответа upstream
rewrite-target: /перезапись URI
ssl-redirect: "true"HTTP → HTTPS
force-ssl-redirect: "true"даже за внешним прокси
configuration-snippetпроизвольный кусок nginx-конфига в location
server-snippetв server-блок
limit-rps: "10"встроенный rate limit
auth-url / auth-signinвнешняя аутентификация (oauth2-proxy)
canary: "true" + canary-weight: "10"canary deployment 10%
whitelist-source-rangeIP allowlist
backend-protocol: "HTTPS"проксировать в backend по HTTPS
backend-protocol: "GRPC"gRPC backend

ConfigMap — глобальные настройки

Аннотации действуют на один Ingress. Глобальные параметры (worker_processes, gzip, log format) — через ConfigMap контроллера:

apiVersion: v1
kind: ConfigMap
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
data:
  worker-processes: "auto"
  use-gzip: "true"
  gzip-level: "6"
  proxy-buffering: "on"
  log-format-escape-json: "true"
  log-format-upstream: '{"time":"$time_iso8601","host":"$host","method":"$request_method","uri":"$request_uri","status":$status,"req_time":$request_time,"upstream_time":"$upstream_response_time"}'
  enable-real-ip: "true"
  use-forwarded-headers: "true"
  ssl-protocols: "TLSv1.2 TLSv1.3"
  hsts: "true"
  hsts-include-subdomains: "true"

Canary deployment одним манифестом

Без service mesh, без сложных tools — двумя Ingress'ами на тот же host:

# Основной (stable, 100% по умолчанию)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api
spec:
  ingressClassName: nginx
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service: {name: api-v1, port: {number: 8080}}

---
# Canary, 10% трафика на v2
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  ingressClassName: nginx
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service: {name: api-v2, port: {number: 8080}}

Альтернативы canary-аннотации: canary-by-header: X-Canary (header-based) и canary-by-cookie — сначала видят канарейку отдельные пользователи.

Когда стоит уходить с ingress-nginx

29. stream{} — TCP/UDP проксирование

До сих пор мы говорили про HTTP. Но Nginx умеет балансировать и обычный TCP / UDP — для этого есть stream модуль (включён в дистрибутив, но отдельно от http).

Зачем это нужно:

Структура

# Совершенно отдельная секция от http {}
stream {
    upstream postgres {
        server pg-primary:5432 max_fails=3 fail_timeout=10s;
        server pg-replica1:5432 backup;
        server pg-replica2:5432 backup;
    }

    server {
        listen 5432;
        proxy_pass postgres;
        proxy_connect_timeout 5s;
        proxy_timeout 3600s;        # долгие соединения к БД
    }

    # TLS termination для PostgreSQL?
    server {
        listen 5433 ssl;
        ssl_certificate /etc/ssl/db.crt;
        ssl_certificate_key /etc/ssl/db.key;
        proxy_pass pg-primary:5432;
    }

    # UDP пример: DNS
    server {
        listen 53 udp;
        proxy_pass dns_upstream;
        proxy_responses 1;
        proxy_timeout 5s;
    }

    upstream dns_upstream {
        server 1.1.1.1:53;
        server 8.8.8.8:53;
    }

    # Лог отдельно для stream
    log_format basic '$remote_addr [$time_local] $protocol $status '
                     '$bytes_sent $bytes_received $session_time';
    access_log /var/log/nginx/stream.log basic;
}

Ограничения stream{}

SNI-routing без расшифровки

Можно проксировать TLS по имени сервера, не терминируя:

stream {
    map $ssl_preread_server_name $upstream_pool {
        api.example.com   api_backend;
        web.example.com   web_backend;
        default           default_backend;
    }

    upstream api_backend { server api:443; }
    upstream web_backend { server web:443; }

    server {
        listen 443;
        ssl_preread on;            # читаем SNI из TLS ClientHello
        proxy_pass $upstream_pool;
    }
}

Полезно когда TLS-терминирует само приложение или mTLS должен быть сквозным.

30. fastcgi_pass и uwsgi_pass

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

FastCGI

Бинарный протокол для CGI-приложений. Используется PHP (php-fpm).

uWSGI

Свой бинарный протокол uWSGI server'а. Используется в Python (Django/Flask с uWSGI).

SCGI

Старый, редкий, упоминается ради полноты.

memcached / Redis

Nginx умеет ходить прямо в кэш через memcached_pass, минуя приложение.

PHP-FPM конфиг — классика

server {
    listen 443 ssl;
    http2 on;
    server_name shop.example.com;

    root /var/www/shop;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include /etc/nginx/snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;   # Unix-socket — быстрее TCP
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;

        fastcgi_read_timeout 120;
        fastcgi_buffer_size 16k;
        fastcgi_buffers 16 16k;
    }

    # Защита от выполнения PHP в upload-папке
    location ~* /uploads/.*\.php$ {
        return 403;
    }
}

Python uWSGI

upstream django {
    server unix:/run/uwsgi/django.sock;
}

server {
    listen 443 ssl;
    server_name app.example.com;

    location / {
        include uwsgi_params;
        uwsgi_pass django;
        uwsgi_read_timeout 60s;
    }

    location /static/ {
        alias /var/www/django/static/;
        expires 1y;
    }

    location /media/ {
        alias /var/www/django/media/;
    }
}

В современных проектах Python/Node редко используют FastCGI/uWSGI напрямую — обычно Gunicorn/uvicorn/Node слушают HTTP, и Nginx делает обычный proxy_pass. FastCGI остаётся в основном с PHP.

31. OpenResty и Lua

OpenResty — это «суперзаряженный Nginx». Сборка Nginx + LuaJIT + десятки модулей + базовая стандартная библиотека. Запускается так же, как Nginx, но внутри можно писать Lua-код прямо в директивах.

Зачем это нужно:

На OpenResty построены Kong, APISIX, 3scale — все эти API-gateway по сути Nginx + Lua-логика.

Простой пример: JWT-валидация в Nginx

location /api/ {
    access_by_lua_block {
        local jwt = require "resty.jwt"
        local auth = ngx.var.http_authorization or ""
        local token = string.match(auth, "Bearer%s+(.+)")

        if not token then
            ngx.status = 401
            ngx.say('{"error":"missing token"}')
            return ngx.exit(401)
        end

        local jwt_obj = jwt:verify("my-secret-key", token)
        if not jwt_obj.verified then
            ngx.status = 401
            ngx.say('{"error":"invalid token"}')
            return ngx.exit(401)
        end

        -- Передать user_id в upstream через заголовок
        ngx.req.set_header("X-User-Id", jwt_obj.payload.sub)
    }

    proxy_pass http://app;
}

Фазы выполнения Lua

В Nginx обработка запроса делится на фазы. Lua-код можно вставлять в нужную:

set_by_lua_block          # при назначении переменной
rewrite_by_lua_block      # до выбора location
access_by_lua_block       # проверка доступа — здесь auth
content_by_lua_block      # вместо proxy_pass — генерация ответа
header_filter_by_lua_block # перед отправкой заголовков
body_filter_by_lua_block  # изменение тела ответа
log_by_lua_block          # при логировании

Когда брать OpenResty

Минус — Lua это отдельный язык, добавляет операционную сложность. Если масштаб подходит — лучше готовый Kong / APISIX.

32. NJS — JavaScript внутри Nginx

В 2017 Nginx Inc выпустила NJS — подмножество JavaScript (ES6+) для встраивания в Nginx. Похоже на OpenResty/Lua, но на JS. Преимущество: фронтендеры уже знают JS.

Это НЕ Node.js

NJS — subset JS, без npm, без event loop в стиле Node, без файловой системы. Только то, что нужно для модификации запроса/ответа.

Установка

sudo apt install nginx-module-njs

# В nginx.conf:
load_module modules/ngx_http_js_module.so;

Пример: добавить request_id

# /etc/nginx/njs/request_id.js
function generate_request_id(r) {
    const ts = Date.now().toString(36);
    const rnd = Math.random().toString(36).substring(2, 10);
    return `${ts}-${rnd}`;
}

export default { generate_request_id };
# nginx.conf
js_path "/etc/nginx/njs/";
js_import main from request_id.js;

server {
    location / {
        js_set $request_id main.generate_request_id;
        proxy_set_header X-Request-Id $request_id;
        add_header X-Request-Id $request_id always;
        proxy_pass http://app;
    }
}

NJS vs Lua

NJSOpenResty/Lua
ЯзыкJS subsetLua + LuaJIT
ПроизводительностьХорошаяЛучшая (JIT)
ЭкосистемаМало модулейОгромная (lua-resty-*)
Кривая входаJS-фронтендеры освоятLua — отдельный язык
ЗрелостьМолодой10+ лет в продакшене

Для простых задач (модификация заголовков, request_id, A/B по логике) — NJS отличный выбор. Для серьёзной программируемости — Lua.

33. ModSecurity + OWASP CRS — встроенный WAF

Хочешь WAF, но не платить Cloudflare? ModSecurity — open source WAF, который встраивается в Nginx как модуль. С набором правил OWASP CRS покрывает большинство атак из OWASP Top 10.

Установка

# ModSecurity 3 (libmodsecurity)
sudo apt install libmodsecurity3 libnginx-mod-http-modsecurity

# OWASP CRS
sudo mkdir -p /etc/nginx/modsec
cd /etc/nginx/modsec
sudo git clone https://github.com/coreruleset/coreruleset.git
sudo cp coreruleset/crs-setup.conf.example crs-setup.conf

Базовый конфиг

# /etc/nginx/modsec/main.conf
Include /etc/nginx/modsec/modsecurity.conf
Include /etc/nginx/modsec/crs-setup.conf
Include /etc/nginx/modsec/coreruleset/rules/*.conf
# /etc/nginx/modsec/modsecurity.conf — основной
SecRuleEngine On                           # или DetectionOnly для логирования без блока
SecRequestBodyAccess On
SecResponseBodyAccess On
SecResponseBodyMimeType text/plain text/html text/xml application/json

SecAuditLog /var/log/nginx/modsec_audit.log
SecAuditLogParts ABIJDEFHZ
SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:5|4(?!04))"

SecPcreMatchLimit 100000
SecPcreMatchLimitRecursion 100000
# nginx.conf — подключение к server-блоку
server {
    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsec/main.conf;

    location / {
        proxy_pass http://app;
    }
}

Paranoia levels

OWASP CRS работает в режиме anomaly scoring. Каждое правило добавляет очки, превышение порога — блок. Управляется через paranoia level в crs-setup.conf:

SecAction \
 "id:900000,\
  phase:1,\
  nolog,\
  pass,\
  t:none,\
  setvar:tx.paranoia_level=1"      # 1=мягко, 4=параноидально

SecAction \
 "id:900110,\
  phase:1,\
  nolog,\
  pass,\
  setvar:tx.inbound_anomaly_score_threshold=5,\
  setvar:tx.outbound_anomaly_score_threshold=4"

Стратегия внедрения

  1. SecRuleEngine DetectionOnly. Включи на пару дней — логируешь, но не блокируешь.
  2. Смотришь audit-лог. Какие правила срабатывают на легитимный трафик.
  3. Делаешь exclusions. Для каждого ложного срабатывания — точечно отключаешь правило для конкретного path/параметра.
  4. Включаешь enforcing. SecRuleEngine On.
  5. Поднимаешь paranoia постепенно. PL2, PL3 — после стабилизации.

Performance impact

ModSecurity — не бесплатный. Дополнительные 5-20% CPU на каждый запрос. На маленьких VM может стать узким местом. Альтернатива — WAF на уровне CDN (Cloudflare, Cloudfront WAF), там стоимость на edge размазана.

34. Mutual TLS — клиентские сертификаты

Обычный TLS аутентифицирует сервер. mTLS — обе стороны. Клиент тоже предъявляет сертификат, Nginx проверяет, что он подписан доверенным CA.

Где применяется:

Конфигурация

server {
    listen 443 ssl;
    http2 on;
    server_name partners.example.com;

    # Серверный сертификат (как обычно)
    ssl_certificate     /etc/ssl/server.crt;
    ssl_certificate_key /etc/ssl/server.key;

    # CA, которому доверяем для проверки клиентских сертификатов
    ssl_client_certificate /etc/ssl/clients-ca.crt;
    ssl_verify_client on;                  # on = обязательно, optional = опционально
    ssl_verify_depth 2;                    # глубина цепочки

    # CRL — список отозванных (опционально)
    ssl_crl /etc/ssl/clients-revoked.crl;

    location / {
        proxy_pass http://app;

        # Передать инфу о клиенте в upstream
        proxy_set_header X-Client-Cert-Subject $ssl_client_s_dn;
        proxy_set_header X-Client-Cert-Issuer  $ssl_client_i_dn;
        proxy_set_header X-Client-Verify       $ssl_client_verify;
        proxy_set_header X-Client-Serial       $ssl_client_serial;
        proxy_set_header X-Client-Fingerprint  $ssl_client_fingerprint;
    }
}

Авторизация по subject

Можно ограничить, какие именно сертификаты принимать — например, по CN или OU:

map $ssl_client_s_dn $allowed_partner {
    default                                            0;
    "~CN=partner1\.example\.com,O=ACME Corp"           1;
    "~CN=partner2\.example\.com,O=Globex"              1;
}

server {
    ...
    ssl_verify_client on;

    if ($allowed_partner != 1) {
        return 403;
    }

    location / { proxy_pass http://app; }
}

Optional mTLS

Иногда хочется — клиент с cert получает API-доступ, без cert — только публичная часть:

ssl_verify_client optional;

location /api/admin/ {
    if ($ssl_client_verify != "SUCCESS") { return 403; }
    proxy_pass http://app;
}

location /api/public/ {
    proxy_pass http://app;
}

35. GeoIP и split_clients

GeoIP2 — геолокация по IP

Nginx умеет определять страну/город/ASN клиента по его IP, используя базу MaxMind. Полезно для geoblocking, региональных правил, аналитики.

# Установить модуль
sudo apt install libnginx-mod-http-geoip2

# Скачать базу (нужен MaxMind account — free)
wget https://download.maxmind.com/.../GeoLite2-Country.mmdb
sudo mv GeoLite2-Country.mmdb /etc/nginx/geoip/
load_module modules/ngx_http_geoip2_module.so;

http {
    geoip2 /etc/nginx/geoip/GeoLite2-Country.mmdb {
        $geoip2_country_code source=$remote_addr country iso_code;
        $geoip2_country_name source=$remote_addr country names en;
    }

    map $geoip2_country_code $blocked_country {
        default 0;
        KP 1;        # North Korea
        IR 1;        # Iran
        SY 1;        # Syria
    }

    server {
        if ($blocked_country) {
            return 451;          # 451 Unavailable For Legal Reasons
        }

        # Передать страну в upstream
        proxy_set_header X-Country $geoip2_country_code;

        location / {
            proxy_pass http://app;
        }
    }
}

split_clients — A/B testing без mesh

Хочешь раздавать 10% трафика на новую версию? Без service mesh, без Argo Rollouts — две строчки в Nginx:

split_clients "${remote_addr}${http_user_agent}" $variant {
    10%   "v2";
    *     "v1";
}

upstream app_v1 { server v1:8080; }
upstream app_v2 { server v2:8080; }

map $variant $upstream_pool {
    "v1"  app_v1;
    "v2"  app_v2;
}

server {
    location / {
        proxy_pass http://$upstream_pool;
        add_header X-Served-By $variant always;
    }
}

split_clients — детерминистический хэш. Один и тот же клиент всегда попадает в одну группу — это критично для UX.

36. Кейс troubleshooting: «502 Bad Gateway»

Пользователи жалуются: «у вас 502 Bad Gateway». Покажу системный алгоритм поиска причины — как реально дебажат.

  1. Шаг 1: error.log Nginx
    sudo tail -50 /var/log/nginx/error.log
    Что искать:
    • connect() failed (111: Connection refused) → upstream не слушает
    • upstream timed out → upstream живой, но медленный
    • no live upstreams → все upstream'ы в fail_timeout
    • upstream sent invalid header → app возвращает мусор
    • upstream prematurely closed connection → app падает посреди ответа
  2. Шаг 2: жив ли upstream
    # Из машины с Nginx
    curl -v http://app:8080/healthz
    ss -tlnp | grep 8080                    # слушает ли локально
    
    # Если в K8s
    kubectl get pods -l app=backend
    kubectl logs deploy/backend --tail=100
  3. Шаг 3: connectivity
    ping app
    nc -zv app 8080
    traceroute app
    Если в облаке — Security Groups / NetworkPolicy могут блокировать.
  4. Шаг 4: timeout

    Если запрос медленный — app не успевает, Nginx режет. Смотри upstream_response_time в логах:

    tail -100 /var/log/nginx/access.log | awk '{print $NF}' | sort -n
    # или для JSON-лога:
    tail -100 /var/log/nginx/access.json | jq -r .upstream_response_time | sort -n

    Если 90% запросов 30+ секунд → крути таймауты Nginx или фикси приложение.

  5. Шаг 5: SSL/протокол

    Если upstream — HTTPS, и серт самоподписан:

    proxy_ssl_verify off;                   # только для дебага, не в prod
    proxy_ssl_server_name on;
    proxy_ssl_name $host;

    Если upstream HTTP/2-only и ты шлёшь HTTP/1.1 — нужен proxy_http_version 1.1;, а для gRPC — grpc_pass вместо proxy_pass.

  6. Шаг 6: размер заголовков

    Если в логах upstream sent too big header:

    proxy_buffer_size 16k;
    proxy_buffers 8 32k;
    proxy_busy_buffers_size 32k;

    Распространено когда JWT в Cookie/Authorization вырос больше 8K (default).

  7. Шаг 7: keep-alive проблемы

    Если 502 редкие и не воспроизводятся — может быть race: upstream закрыл keepalive-соединение, Nginx ещё не знает, шлёт запрос — получает FIN. Лечится:

    upstream backend {
        server app:8080;
        keepalive 32;
        keepalive_timeout 30s;          # должен быть КОРОЧЕ, чем у upstream
    }
    
    proxy_next_upstream error timeout http_502;
    proxy_next_upstream_tries 2;
  8. Шаг 8: tcpdump последний резерв
    sudo tcpdump -i any -nn 'host app and port 8080' -w /tmp/dump.pcap
    # потом анализируй в Wireshark

В 80% случаев ответ находится на шагах 1-2. Если нет — переходи дальше системно, не пропуская.

37. Open Source vs Nginx Plus

Nginx — коммерческий продукт у F5. Есть OSS и Plus. Полезно знать, чего в open source нет:

ФичаOSSPlus
Базовое HTTP / proxy / TLS
upstream / load balancing
HTTP/2 / HTTP/3
Active health checks✗ (только passive)
Dynamic config через API
DNS resolution for upstream без перезагрузкиограниченно
Session persistence (cookie, learn)только ip_hash✓ полный
JWT validationчерез OpenResty/NJSвстроено
OIDC из коробкиnginx-openidc или OpenResty
Dashboard / live monitoringтолько stub_status✓ красивый dashboard
Поддержка F5communitySLA

Чем заменить Plus, оставаясь в OSS

Active health checks

Tengine (форк Nginx с активными health checks), nginx_upstream_check_module, либо вынести HC на HAProxy / Envoy.

Dynamic config

OpenResty + Lua для динамического upstream'a из Redis/etcd. Либо использовать готовый APISIX.

JWT / OIDC

OpenResty модуль lua-resty-jwt или lua-resty-openidc. Либо oauth2-proxy перед Nginx.

Dashboard

Nginx Prometheus Exporter + Grafana. Бесплатно, гибко, лучше чем родной dashboard.

Альтернативы — когда Plus нет смысла

Если тебе реально нужны фичи Plus — посмотри также:

В реальной практике Plus покупают, когда нужна поддержка F5 и активные health checks без хаков. Большинству хватает OSS + грамотной архитектуры.

38. Чек-лист «понимаю Nginx»

Nginx — универсальный швейцарский нож между интернетом и твоим приложением: веб-сервер для статики, reverse proxy для динамики, load balancer для пула, cache для скорости, TLS-терминатор для безопасности, rate limiter для защиты. Master/worker модель даёт zero-downtime hot reload. Главные грабли: слэш в proxy_pass, location-приоритеты, дублирование add_header, забытый Host-заголовок. Изучить — must для любого DevOps.

← К списку уроков