Весь курс ты собирал шлюз по кусочкам - TLS тут, кэш там, rate limit в третьем уроке. Это нормально для учёбы, но в бою так нельзя. Реальный production-gateway - это не один файл на 800 строк, который страшно открыть. Это набор маленьких самодостаточных кусков, каждый из которых можно прочитать за минуту, протестировать через nginx -t и откатить независимо. Сегодня мы соберём такой шлюз руками с нуля - модульный, на nginx 1.30 (а где интересно - с оглядкой на Angie), с боевым TLS, проксированием, кэшем, лимитами, трассировкой и метриками.
Главная боль, которую решает этот урок: ты умеешь делать каждую вещь по отдельности, но не видел, как они уживаются в одном дереве конфигов так, чтобы порядок include не сломал rate limit, чтобы realip не подрался с логами, а кэш не закэшировал ответ с Set-Cookie. Это и есть разница между nginx best practices и набором скопированных сниппетов. Будем собирать боевой конфиг по принципам эталонного шлюза ngx-trace-gateway - модульная структура, JSON-логи, сквозная трассировка хопов.
Договоримся о фактах на 2026, чтобы ты не тащил старьё в свой nginx конфиг. Пример директив будет современным: HTTP/2 включается отдельной директивой http2 on, а не параметром listen. HTTP/3 - production-ready, не эксперимент. Соединение к бэкенду по умолчанию уже HTTP/1.1 с keepalive (с nginx 1.29.7, вошло в stable 1.30), но мы всё равно явно пропишем proxy_http_version 1.1 - чтобы конфиг работал и на старых сборках. И сразу мотаем на ус: ingress-nginx из kubernetes мёртв (read-only с марта 2026, CVE не патчатся) - в проде смотрим в сторону Gateway API, но это другой урок.Капстоун (capstone) - финальный проект, который связывает воедино все навыки курса. Цель не выучить новую директиву, а собрать рабочую систему и понять, как части влияют друг на друга.

Модульная структура: nginx настройка с нуля без монолита
Начнём со скелета. Идея простая: главный nginx.conf почти пустой, он только подключает события и http-слой. Весь http-слой - это include набора globals/* в строго заданном порядке плюс vhost-ы из sites-enabled. Порядок критичен: map-переменные и limit_req_zone должны быть объявлены ДО того, как их используют server-блоки.
Код: Выделить всё
# /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
pcre_jit on;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 10240;
multi_accept on;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
charset utf-8;
# порядок имеет значение: сначала map/zone, потом server
include /etc/nginx/globals/00-tcp.conf;
include /etc/nginx/globals/10-trace.conf;
include /etc/nginx/globals/20-proxy.conf;
include /etc/nginx/globals/30-cache.conf;
include /etc/nginx/globals/40-rate-limit.conf;
include /etc/nginx/globals/50-gzip.conf;
include /etc/nginx/globals/60-ssl.conf;
include /etc/nginx/globals/70-security.conf;
include /etc/nginx/globals/80-logging.conf;
include /etc/nginx/globals/90-error-pages.conf;
include /etc/nginx/globals/95-metrics.conf;
include /etc/nginx/sites-enabled/*.conf;
}
Базовый транспорт - в один файл, чтобы не размазывать по vhost-ам:
Код: Выделить всё
# globals/00-tcp.conf
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65s;
keepalive_requests 1000;
types_hash_max_size 4096;
server_tokens off;
max_headers 100;
TLS на 2026: post-quantum, HSTS и автопродление
TLS - то место, где чаще всего тащат конфиг пятилетней давности. Соберём современный globals/60-ssl.conf. Ключевые моменты: ssl_ciphers НЕ влияет на TLS 1.3 (для него отдельный механизм ssl_conf_command), а post-quantum обмен ключами включается через ssl_ecdh_curve с гибридной группой X25519MLKEM768.
Код: Выделить всё
# globals/60-ssl.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
# ciphers применяются ТОЛЬКО к TLS 1.2, на 1.3 не влияют
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
# гибридный пост-квантовый обмен ключами (OpenSSL 3.5+)
ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# OCSP stapling - legacy: Let's Encrypt убрал OCSP-URL в мае 2025
# ssl_stapling on;
# ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s ipv6=off;
resolver_timeout 5s;
Сам сертификат и HSTS вешаем на server-блок vhost-а, не в globals (ключи у каждого домена свои):
Код: Выделить всё
# sites-enabled/api.conf - HTTP -> HTTPS редирект
server {
listen 80;
listen [::]:80;
server_name api.cyberlake.ru;
# для ACME-челленджа certbot/acme.sh
location /.well-known/acme-challenge/ { root /var/www/acme; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl;
listen [::]:443 ssl;
listen 443 quic reuseport; # HTTP/3
listen [::]:443 quic reuseport;
http2 on; # отдельная директива, НЕ listen ... http2
http3 on;
server_name api.cyberlake.ru;
ssl_certificate /etc/letsencrypt/live/api.cyberlake.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.cyberlake.ru/privkey.pem;
ssl_early_data on; # 0-RTT для QUIC
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Alt-Svc 'h3=":443"; ma=86400' always;
include /etc/nginx/snippets/api-locations.conf;
}
Reverse proxy, realip и кэш: ядро боевого шлюза
Теперь сердце. Сначала общий слой проксирования - таймауты, буферы, проброс заголовков и трассировка. Это nginx конфиг пример того, как НЕ дублировать proxy_set_header в каждом location.
Код: Выделить всё
# globals/20-proxy.conf
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
proxy_http_version 1.1; # явно, для совместимости со старыми сборками
proxy_set_header Connection ""; # пустой Connection включает keepalive к апстриму
proxy_buffering on;
proxy_buffer_size 16k;
proxy_buffers 16 16k;
proxy_busy_buffers_size 32k;
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 3;
# проброс на бэкенд
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;
# WebSocket: апгрейд протокола
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# сквозная трассировка хопов
proxy_set_header X-Request-ID $trace_request_id;
Код: Выделить всё
# globals/10-trace.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# если клиент/балансировщик прислал X-Request-ID - используем его,
# иначе генерим свой из встроенной $request_id
map $http_x_request_id $trace_request_id {
default $http_x_request_id;
'' $request_id;
}
Код: Выделить всё
# globals/15-realip.conf (подключить ПОСЛЕ trace, ДО rate-limit)
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# доверяем ТОЛЬКО своим сетям балансировщиков/CDN
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
# set_real_ip_from <диапазоны CDN>;
Кэш ответов. Объявляем зону глобально, политику - там же:
Код: Выделить всё
# globals/30-cache.conf
proxy_cache_path /var/cache/nginx/gate levels=1:2 keys_zone=appcache:128m
inactive=60m max_size=10g use_temp_path=off;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 301 1h;
proxy_cache_valid 404 1m;
# не кэшируем приватное
proxy_no_cache $http_authorization;
proxy_cache_bypass $http_authorization;
proxy_ignore_headers Set-Cookie;
proxy_hide_header Set-Cookie;
# защита от dog-pile и тёплый кэш
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
proxy_cache_background_update on;
proxy_cache_revalidate on;
proxy_cache_use_stale error timeout updating http_502 http_503 http_504;
add_header X-Cache-Status $upstream_cache_status always;
Апстрим с балансировкой и failover:
Код: Выделить всё
# snippets/upstream-api.conf
upstream api_backend {
least_conn;
server 10.0.1.11:9000 max_fails=3 fail_timeout=30s;
server 10.0.1.12:9000 max_fails=3 fail_timeout=30s;
server 10.0.1.13:9000 backup;
keepalive 64;
}
Сжатие, security-заголовки, лимиты: nginx best practices
Сжатие. gzip встроен, brotli - сторонний динамический модуль ngx_brotli (в ванильный nginx не входит), нативный Zstd есть только в Angie.
Код: Выделить всё
# globals/50-gzip.conf
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_vary on;
gzip_proxied any;
gzip_types text/plain text/css application/json application/javascript
application/xml text/xml image/svg+xml;
# Если собран ngx_brotli:
# brotli on; brotli_comp_level 5; brotli_types text/css application/json ...;
# Если Angie: zstd on; zstd_comp_level 9; zstd_types ...;
Код: Выделить всё
# globals/70-security.conf
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "no-referrer" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# X-XSS-Protection НЕ добавляем - устарел, полагаемся на CSP
# add_header Content-Security-Policy "default-src 'self'" always;
# запрет точечных файлов и служебных каталогов
location ~ /\.(?!well-known) { deny all; }
location ~* \.(bak|sql|zip|git|svn|ht)$ { deny all; }
Rate limiting. Зоны - глобально, применение - в location. Разные лимиты для логина, публичного API и внутренних клиентов:
Код: Выделить всё
# globals/40-rate-limit.conf
limit_req_zone $binary_remote_addr zone=app_login:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=public_api:10m rate=60r/m;
limit_req_status 429;
Код: Выделить всё
# в location логина - жёстко, без всплесков
location = /api/login {
limit_req zone=app_login burst=5 nodelay;
proxy_pass http://api_backend;
}
# публичное API - даём сглаженный всплеск
location /api/ {
limit_req zone=public_api burst=120 delay=60;
proxy_cache appcache;
proxy_pass http://api_backend;
}
JSON-логи, трассировка и метрики: видимость боевого конфига
Шлюз, который ты не видишь - это шлюз, который ты не починишь в 3 часа ночи. Структурные JSON-логи с request_id и timing-полями:
Код: Выделить всё
# globals/80-logging.conf
map $status $log_level {
~^[23] info;
~^4 warn;
default error;
}
log_format structured_log escape=json
'{'
'"time":"$time_iso8601","level":"$log_level",'
'"request_id":"$trace_request_id",'
'"remote_addr":"$remote_addr","xff":"$http_x_forwarded_for",'
'"method":"$request_method","uri":"$uri","args":"$args",'
'"status":$status,"bytes":$body_bytes_sent,'
'"request_time":$request_time,'
'"upstream_addr":"$upstream_addr",'
'"upstream_status":"$upstream_status",'
'"upstream_time":"$upstream_response_time",'
'"cache":"$upstream_cache_status",'
'"tls":"$ssl_protocol","cipher":"$ssl_cipher","sni":"$ssl_server_name"'
'}';
access_log /var/log/nginx/access.log structured_log;
Трассировку через цепочку хопов мы уже завели в 10-trace.conf и пробрасываем заголовком в 20-proxy.conf. Дальше - распределённая трассировка через официальный ngx_otel_module. Важно: он open-source (НЕ Plus-only) с nginx 1.25.3, ставится пакетом nginx-module-otel, с 1.27.5 идёт по умолчанию в -otel образах. Накладные ~10-15%, против ~50% у сторонних. Только OTLP/gRPC.
Код: Выделить всё
# подключить как динамический модуль и настроить экспортёр
otel_exporter {
endpoint otel-collector:4317; # OTLP/gRPC
}
otel_trace on;
otel_trace_context propagate; # W3C trace context
otel_span_name $uri;
Код: Выделить всё
# globals/95-metrics.conf
server {
listen 127.0.0.1:8080;
location = /nginx_status {
stub_status;
allow 127.0.0.1;
allow 10.0.0.0/8;
deny all;
}
}
Красивые страницы ошибок - чтобы пользователь не видел голый 502:
Код: Выделить всё
# globals/90-error-pages.conf
error_page 404 /errors/404.html;
error_page 500 502 503 504 /errors/50x.html;
location ^~ /errors/ { internal; root /usr/share/nginx; }
- Порядок include. limit_req_zone или map после server-блока, который их использует - nginx -t упадёт с "unknown variable" или зона не найдётся. Префиксы 00-95 решают это раз и навсегда.
- add_header без always. HSTS и security-заголовки не уйдут на 4xx/5xx. Атакующий получит страницу ошибки без защит.
- add_header в location перекрывает наследование. Если в location появился хоть один add_header, ВСЕ add_header из http/server в этом location перестают наследоваться. Дублируй или собирай в include.
- realip с широким доверием. set_real_ip_from 0.0.0.0/0 - дыра: клиент подделает свой IP. Доверяй только сетям своих прокси.
- Кэш с Set-Cookie. Без proxy_ignore_headers Set-Cookie и proxy_no_cache по Authorization можно закэшировать персональный ответ и отдать его другому пользователю - утечка сессии.
- listen ... http2. Устаревший синтаксис. Только http2 on; отдельной директивой.
- rewrite-правила и CVE-2026-42945. "NGINX Rift" - heap overflow в ngx_http_rewrite_module (версии до 1.30.0, эксплуатируется с мая 2026). Минимизируй rewrite, держи nginx обновлённым.
Повтори по шагам на тестовой машине или в Docker (образ openresty/openresty или nginx:1.30).
- Шаг 1. Создай дерево: /etc/nginx/nginx.conf, каталоги globals/, sites-enabled/, snippets/. Разложи файлы с префиксами 00-95 как выше.
- Шаг 2. Подними фейковый бэкенд: docker run -d -p 9000:80 --name be1 kennethreitz/httpbin (или любой echo-сервер). Пропиши его в upstream api_backend как 127.0.0.1:9000.
- Шаг 3. Проверь синтаксис и порядок: nginx -t. Любая ошибка "unknown variable" значит, что map подключён после использования - правь префиксы.
- Шаг 4. Прогони статический анализатор gixy: pip install gixy && gixy /etc/nginx/nginx.conf. Он ловит ssrf, переопределение add_header, проблемы $uri в redirect. Чини findings.
- Шаг 5. Запусти nginx, дёрни curl -k -I https://localhost/api/. Проверь в ответе HSTS, X-Cache-Status, отсутствие Server-версии и X-XSS-Protection.
- Шаг 6. Проверь кэш: два раза curl на один URL, первый - X-Cache-Status: MISS, второй - HIT.
- Шаг 7. Нагрузочный тест: wrk -t4 -c100 -d30s https://localhost/api/. Смотри latency p99 и requests/sec. Затем погаси бэкенд и убедись, что use_stale отдаёт старый кэш (X-Cache-Status: STALE), а не 502.
- Шаг 8. Проверь rate limit: for i in $(seq 1 20); do curl -s -o /dev/null -w "%{http_code}\n" https://localhost/api/login; done - после порога должны пойти 429.
- Почему limit_req_zone и map нельзя объявлять после server-блока, который их использует, и как этого избежать структурно?
- Что произойдёт с HSTS-заголовком на ответе 502, если у add_header нет параметра always?
- Чем отличается поведение клиента при proxy_cache_use_stale ... updating против отсутствия этой опции, когда запись в кэше протухла?
- Почему set_real_ip_from 0.0.0.0/0 - это уязвимость, и как она ломает rate limit?
Ты собрал production-gateway с нуля: модульная структура с осмысленным порядком include, современный TLS с post-quantum обменом и HSTS, reverse proxy с балансировкой, keepalive и failover, корректный realip, кэш с lock, background_update и use_stale, сжатие, security-заголовки без устаревшего X-XSS-Protection, rate limit для логина и API, JSON-логи с request_id, трассировка через ngx_otel и метрики. И, главное, проверил его через nginx -t, gixy и wrk. Это и есть nginx боевой конфиг - не магия, а дисциплина: маленькие файлы, явные зависимости, проверка перед reload, видимость в логах и метриках. Дальше - только эксплуатация: мониторь 5xx-rate, latency p99, upstream_response_time, cache hit ratio и алерть на их рост.