Nginx часто воспринимают как «просто веб-сервер», который достаточно поставить и запустить с дефолтным конфигом. На этом этапе обычно и останавливаются: процессы работают как попало, заголовки отдаются по умолчанию, SSL настроен формально, keepalive либо не используется, либо вредит, а маршрутизация запросов со временем обрастает хаотичными location. В результате конфигурация вроде бы выполняет свою задачу, но остаётся плохо управляемой, неочевидной и далёкой от оптимальной.

Эта статья — о базовых, но часто недооценённых возможностях Nginx. Мы последовательно разберём настройку рабочих процессов, управление HTTP-заголовками, корректную конфигурацию SSL, работу keepalive-соединений и маршрутизацию запросов. Без магии и редких трюков — только то, что действительно используется в продак��ене и позволяет сделать конфигурацию понятной, предсказуемой и безопасной даже для начинающего администратора

Тонкая настройка worker-процессов и файловых лимитов

Итак начнём, первое с чего хотелось бы начать — это директива worker_processes, которая отвечает за то, сколько рабочих процессов Nginx создаст и, соответственно, сколько ядер процессора он сможет задействовать одновременно. Большинство из вас, скорее всего, ставит значение auto, полагаясь на автоматическую привязку к числу доступных CPU. Да, это работает «как-то», но если цель — выжать из сервера максимум производительности и добиться предсказуемой нагрузки, то имеет смысл выставлять точное значение вручную. Рассчитать его можно достаточно просто: если у вас нагрузка преимущественно CPU-bound, то количество worker-процессов должно соответствовать количеству физических или логических ядер — узнать их можно через nproc. В случае I/O-bound нагрузки и большого количества долгоживущих соединений допускается повышать число процессов до коэффициента 1–2 от количества ядер, но на практике это имеет смысл только если воркеры реально простаивают в ожидании внешних ресурсов. Дополнительно стоит помнить, что общая пропускная способность определяется не только числом процессов, но и лимитом соединений — worker_connections, поскольку итоговое количество одновременно обслуживаемых клиентов будет равно произведению worker_processes × worker_connections. Перед тем как выставлять финальное значение, необходимо также поднять ограничение на количество открытых файлов (через ulimit -n и worker_rlimit_nofile), иначе Nginx упрётся в системный лимит раньше, чем в пределы своих настроек. В итоге формула выбора выглядит так: CPU-bound — ставим worker_processes равным количеству ядер; I/O-bound — начинаем с того же значения, тестируем нагрузкой и при необходимости увеличиваем до двухкратного уровня, оценивая реальное использование CPU, задержки и количество активных соединений. Такой подход даёт точную управляемость и предсказуемый результат, в отличие от слепой надежды на «auto».

Что такое worker_connections и за что он отвечает? Этот параметр определяет максимальное количество одновременных соединений, которое может обслуживать один процесс из worker_processes. Если у вас указано, что процессов — 2, а worker_connections — 1024, то суммарно Nginx сможет открыть до 2048 файловых дескрипторов. Эти дескрипторы используются для обслуживания статических файлов, установления и поддержания TCP-соединений с клиентами и backend-серверами.

Что такое worker_rlimit_nofile и как его правильно настроить? Этот параметр задаёт ограничение RLIMIT_NOFILE — максимальное количество открытых файловых дескрипторов, это лимит на каждый процесс-воркер отдельно, а не суммарно.

Пример и правило расчёта: если worker_processes 2 и worker_connections 2048, то одному воркеру нужно минимум ~2048 дескриптора. Практическая рекомендация — дать запас, например 1.2–2× от worker_connections. В этом примере можно установить:

worker_rlimit_nofile 4096;
events {
  worker_connections 2048;
}
worker_processes 2;

На systemd-системах ставьте ��имит через LimitNOFILE в unit-файле сервиса (иначе systemd может накладывать собственный верхний предел). Посмотреть дефолтные параметры для всех юнитов можно через systemctl show | grep LimitNOFILE

# /etc/systemd/system/nginx.service.d/override.conf
[Service]
LimitNOFILE=65536

Проверить, что лимиты вступили в силу после перезапуска юнита можно вот так: cat /proc/$(pidof nginx | awk '{print $1}')/limits | grep "Max open files"

Здесь видно, что мастер-процесс — это 12087, а остальные — воркеры, которыми он управляет
Здесь видно, что мастер-процесс — это 12087, а остальные — воркеры, которыми он управляет
При worker_rlimit_nofile 4096 каждый процесс получил по 4096 файловых дескриптора которые может открыть
При worker_rlimit_nofile 4096 каждый процесс получил по 4096 файловых дескриптора которые может открыть

Если процесс запущен в контейнере Docker, то лимиты там обычно очень высокие. Как мы видим ulimit -n равен 1048675 в базом состоянии

Мастер процесс nginx, как раз и получает этот лимит nofiles
Мастер процесс nginx, как раз и получает этот лимит nofiles

На первый взгляд кажется, что всего несколько параметров могут быть заданы абы как, все равно непонятно, что там написано. Но именно они критичны для стабильной работы сервера. При высокой неожиданной нагрузке неверные значения способны привести к OOM Killer’у, который может завершить процесс Nginx или, в худшем случае, вызвать неконтролируемое потребление ресурсов всей системы при DDoS атаке.

Модель наследования заголовков

Механизм наследования заголовков часто вызывает путаницу, потому что директивы add_header и proxy_set_header подчиняются разным правилам. Чтобы корректно управлять заголовками в ответах.

add_header добавляет заголовки в ответ клиенту. Директивы add_header наследуются от родительского блока только если в текущем (дочернем) блоке нет ни одной директивы add_header. Если в дочернем блоке объявлена хотя бы одна add_header, то все add_header родителя НЕ наследуются.

Наследование происходит, когда дочерний блок не содержит add_header:

server {
    listen 80;
    add_header X-Server "global";
    add_header X-Security "strict";

    location / {
        # нет add_header — наследуются X-Server и X-Security
        try_files $uri /index.html;
    }
}

Ожидаемый ответ для GET / (код 200):

HTTP/1.1 200 OK
X-Server: global
X-Security: strict

Если в дочернем блоке есть хотя бы одна add_header, родительские add_header перестают действовать для этого location:

server {
    add_header X-Server "global";

    location /static/ {
        add_header X-Static "assets";
        # X-Server НЕ будет присутствовать в ответе этого location
    }
}

Ожидаемый ответ для GET /static/:

HTTP/1.1 200 OK
X-Static: assets
X-Server: global  <-- отсутствует

Современный контроль наследования (add_header_inherit, nginx ≥1.29.3):

http {
    add_header X-Global "g";
    add_header_inherit merge;  # options: on | off | merge

    server {
      location / {
        add_header X-Server "s";
        # при merge: дочерние add_header дополняют родительские, а не заменяют
      }   
    }
}

Ответ:

HTTP/1.1 200 OK
X-Global: g
X-Server: s

Почему так происходит? Ngix хранит add_header как "массив" на каждом уровне; если на текущем уровне присутствует хотя бы один add_header, массив берётся только с этого уровня — родительский массив не склеивается автоматически. Это часто неожиданно, поэтому многие практики: объявлять все общие заголовки в одном уровне (например, http или server) и не дублировать add_header в location.

Для proxy_set_header немного иные условия:

http {
    proxy_set_header X-Global "g";
    server {
        location / {
            # наследует X-Global, если не переопределено
        }
        location /app {
            proxy_set_header X-Global "overridden";
            # перезаписывает (override). Другие proxy_set_header родителя остаются
        }
    }
}

В отличие от add_header, proxy_set_header наследуется «обычно»: дочерние блоки получают родительские значения, но могут их переопределить.

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

location /api/ {
    proxy_pass http://backend;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Host — доменное имя, с которым клиент обратился к серверу. X-Forwarded-For — реальный IP-адрес клиента, а не прокси. X-Forwarded-Proto — протокол, по которому пришёл клиент: HTTP или HTTPS.

Location

В Nginx директива location определяет, как сервер должен обрабатывать запросы, сопоставляя путь URI с конкретным блоком конфигурации. Механизм работает по строгому набору правил: сначала Nginx ищет наиболее точные префиксные совпадения, затем регулярные выражения, и в итоге выбирает единственный финальный location, внутри которого выполняет проксирование, отдача статических данных или другие действия. Ключевая особенность — сравнение пути происходит посимвольно, а не логически с учётом «папок».

location = / {
    Exact [ configuration A ]
}

location / {
    Preffix [ configuration B ]
}

location /documents/ {
    Prefix  [ configuration C ]
}

location ^~ /images/ {
    Prefix  [ configuration D ]
}

location ~* \.(gif|jpg|jpeg)$ {
    RegExp  [ configuration E ]
}

Сначала Nginx ищет точное совпадение — в примере это location = /. Оно срабатывает только для запросов строго к корню сайта. Если найдено, другие правила не рассматриваются. В ingress controllers это сопоставление называется Exact

Если точного совпадения нет, Nginx переходит к префиксным правилам (В ingress называется Prefix сопоставление). Среди них побеждает то, чей путь длиннее. Поэтому location /documents/ приоритетнее, чем общий location /. Все запросы внутри /documents/ обрабатываются конфигурацией C, а всё, что просто начинается с /, но не подходит под другие правила, — конфигурацией B.

Отдельную категорию представляет location ^~ /images/. Это тоже префиксное правило, но с повышенным приоритетом. Если путь начинается с /images/, Nginx применит конфигурацию D и даже не будет проверять регулярные выражения.

Только если точных и префиксных совпадений нет, сервер переходит к регулярным выражениям. В примере это location ~* \.(gif|jpg|jpeg)$, где ~* указывает на нечувствительность к регистру. Такой блок обрабатывает любые запросы к файлам с указанными расширениями, за исключением тех, что уже перехвачены более приоритетными правилами, например ^~ /images/.

В итоге порядок выбора таков: точные совпадения → префиксные ^~ → обычные префиксные → регулярные выражения.

Также есть еще некоторые особенности работы с location. Ниже на первый взгляд 2 очевидные записи, но не все так просто, как может показаться.

location /user/ {
    proxy_pass http://user.example.com;
}

location /login/ {
    proxy_pass http://login.example.com/;
}

У proxy_pass нет завершающего слэша, поэтому Nginx сохраняет весь URI целиком и просто подставляет его после домена. Если клиент запрашивает /user/profile?id=1, то upstream получит:

http://user.example.com/user/profile?id=1

То есть префикс /user/ не вырезается. Такой вариант чаще используют, когда путь на backend уже включает этот префикс, и вы хотите прокидывать URI «как есть», без манипуляций.

Во втором примере:

location /login/ {
    proxy_pass http://login.example.com/;
}

Здесь у proxy_pass есть завершающий слэш, а значит Nginx отбрасывает совпавшую часть location’а (/login/) и подставляет всё, что идёт после неё. Если клиент запрашивает /login/check?status=id, то upstream получит:

http://login.example.com/check?status=id

SSL

При оптимизации TLS в nginx один из наиболее эффективных, но часто недооценённых инструментов — ssl_session_cache. Этот механизм позволяет серверу хранить параметры завершённых сессий и повторно использовать их при новых соединениях, что значительно снижает нагрузку от полного TLS-рукопожатия. Nginx сохраняет эти параметры на своей стороне, благодаря чему сокращает использование процессора при повторных подключениях в течение заданного времени.

ssl_session_cache shared:SSL:10m; # примерно 40000 сессий
ssl_session_timeout 1h;

Также желательно отключить устаревшие TLS-протоколы и включить приоритет клиентским шифрам вместо серверных:

ssl_protocols TLSv1.2 TLSv1.3; # желательно вообще оставить TLSv1.3, но надо смотреть статистику по клиентам с какими версиями они приходят
ssl_prefer_server_ciphers off;

Server name

Иногда при настройке требуется, чтобы nginx не отвечал на запросы без заголовка Host и не использовал «дефолтный» маршрут при обращении напрямую к IP-адресам, на которых он слушает. В таких случаях хорошей практикой считается явное разделение трафика с помощью двух server-блоков.

server {
  listen 80 default_server;
  server_name "";
  return 444; # или 503
} 

server {
  listen 80;
  server_name *.example.com;
  return 308 https://$host$request_uri;
}

Первый блок обрабатывает все запросы, которые не совпадают ни с одним корректным server_name. Это обращения с пустым или некорректным Host, прямые запросы на IP и трафик сканеров. Использование return 444 немедленно разрывает соединение без ответа и снижает лишнюю нагрузку. При желании этот код можно заменить на 503, если требуется вернуть формальный HTTP-статус.

Второй блок принимает только запросы к доменам формата *.example.com и перенаправляет их на HTTPS с помощью кода 308, сохраняя путь и параметры. Переменные $host и $request_uri позволяют корректно передать домен и исходный URL при редиректе. Такое разделение даёт предсказуемое поведение: обращения к вашим доменам корректно переводятся на HTTPS, а весь остальной трафик жёстко отсекается

Keepalive и HTTP

Использование keepalive в upstream и переход на HTTP/1.1 позволяют nginx не создавать TCP-соединение к бэкенду при каждом запросе, а переиспользовать уже установленное. Это критично в системах с большим числом коротких запросов: накладные расходы на установку TCP-сессии, а тем более TLS-рукопожатия, легко превосходят само время обработки на бэкенде.

С версии 1.29.4 поддерживает проксирование в HTTP/2 через директиву:

proxy_http_version 1.0 | 1.1 | 2;

По умолчанию используется HTTP/1.0 (что очень странно на 25 год), но для keepalive рекомендуется HTTP/1.1 или HTTP/2, так как они корректно поддерживают многоразовые соединения и дают дополнительную экономию на уровне протокола. При работе на протоколе 1.0 каждый запрос на backend закрывается через директиву Connection: Closeи не переиспользуется снова.

Переиспользование соединений важно потому, что TCP — это протокол с дорогостоящей установкой и разрушением состояния. Установка соединения требует трёхстороннего handshake, его завершение — четырёхстороннего, а при активном трафике это приводит к накоплению сокетов в переходных состояниях. Повторное использование освобождает систему от постоянного создания новых структур ядра, экономит CPU на обработке handshake, уменьшает рост очередей backlog и исключает паразитные задержки, возникающие при пиках RPS.

TCP-сокет в Linux проходит несколько состояний:

  • SYN_SENT / SYN_RECV при установке соединения;

  • ESTABLISHED при активном обмене данными;

  • FIN_WAIT1/2, CLOSE_WAIT, LAST_ACK при закрытии;

  • TIME_WAIT после закрытия, где сокет остаётся до истечения таймера, занимая дескриптор и порт.

Пул keepalive-соединений позволяет nginx держать сокеты в состояниях ESTABLISHED и переиспользовать их многократно, избегая большого числа соединений, попадающих в TIME_WAIT и засоряющих таблицу сокетов. Это напрямую повышает производительность: снижается нагрузка на ядро, уменьшается латентность, повышается пропускная способность при высоком RPS.

Важно, чтобы бэкенд корректно поддерживал keep-alive и reuse соединений. Сама инфраструктура должна выдерживать рост числа долгоживущих TCP-сокетов: требуется достаточный лимит файловых дескрипторов, обновлённые параметры net.ipv4.tcp_keepalive_*, корректные somaxconn и tcp_max_syn_backlog.

upstream backend {
  server 10.0.0.10:8080;
  server 10.0.0.11:8080;
  keepalive 4; # количество keepalive соединений в пуле, указывается х2 от кол-во серверов в upstream
  keepalive_timeout 10s;
  keepalive_requests 512;
  keepalive_time 15m;
}

server {
  location /api/ {
    proxy_http_version 2;
    proxy_set_header Connection "";
    proxy_pass http://backend;
  }
}

Параметр keepalive 4 задаёт количество поддерживаемых постоянных (persistent) соединений в пуле. keepalive_timeout 10s означает, что соединение может оставаться открытым для повторного использования до 10 секунд без активности. keepalive_requests 512 ограничивает число запросов, которые могут быть обработаны через одно keepalive-соединение, а keepalive_time 15m задаёт общий срок жизни соединения в пуле (через 15 минут соединение обновится). Запросы проксируются на upstream с использованием HTTP/2, при этом заголовок Connection очищается, чтобы избежать закрытия соединения при проксировании.

Bash скрипт который покажет сколько на данный момент tcp соединений и в каких состояниях они находятся
#!/usr/bin/env bash
set -euo pipefail

if command -v ss >/dev/null 2>&1; then
  TOOL="ss"
  FETCH_CMD=("ss" "-tan4")
elif command -v netstat >/dev/null 2>&1; then
  TOOL="netstat"
  FETCH_CMD=("netstat" "-ant" "-4")
else
  echo "Ошибка: ни ss, ни netstat не найдены в PATH." >&2
  exit 1
fi

mapfile -t raw_lines < <("${FETCH_CMD[@]}" 2>/dev/null)

if [ "${#raw_lines[@]}" -eq 0 ]; then
  echo "Ни одного вывода от ${TOOL} (возможно, нет сетевых соединений или права доступа)." >&2
  exit 1
fi

declare -A counts
total=0
examples_dir="/tmp/tcp_states_examples.$$"
mkdir -p "$examples_dir"

for ((i=0;i<${#raw_lines[@]};i++)); do
  line="${raw_lines[i]}"
  if [[ "$line" =~ ^[[:space:]]*$ ]]; then
    continue
  fi
  if [[ "$line" =~ ^State[[:space:]]+Recv-Q ]]; then
    continue
  fi
  if [[ "$line" =~ ^Proto[[:space:]]+Recv-Q ]]; then
    continue
  fi

  if [ "$TOOL" = "ss" ]; then
    state=$(awk '{print $1}' <<<"$line")
  else
    state=$(awk '{print $NF}' <<<"$line")
  fi

  if [ -z "$state" ]; then
    continue
  fi

  counts["$state"]=$(( ${counts["$state"]:-0} + 1 ))
  total=$(( total + 1 ))

  example_file="$examples_dir/${state}.txt"
  if [ ! -f "$example_file" ] || [ "$(wc -l < "$example_file")" -lt 10 ]; then
    printf '%s\n' "$line" >> "$example_file"
  fi
done

printf "Источник данных: %s\n" "$TOOL"
printf "Общее количество IPv4 TCP-соединений: %d\n\n" "$total"

if [ "$total" -eq 0 ]; then
  rm -rf "$examples_dir"
  exit 0
fi

printf "Состояния (количество):\n"
for st in "${!counts[@]}"; do
  printf "%s %s\n" "${counts[$st]}" "$st"
done | sort -rn -k1,1

echo
printf "Примеры соединений (по состояниям, максимум 10 строк на состояние):\n\n"
for st in $(for k in "${!counts[@]}"; do echo "$k"; done | sort); do
  echo "=== $st (${counts[$st]}) ==="
  if [ -f "$examples_dir/${st}.txt" ]; then
    sed -n '1,10p' "$examples_dir/${st}.txt"
  else
    echo "(примеров нет)"
  fi
  echo
done

rm -rf "$examples_dir"
exit 0

Буферы

proxy_buffering on;

В Nginx буферы — это область памяти, используемая для временного хранения данных при приёме, обработке и передаче HTTP-запросов и ответов. Они критически важны для производительности, так как позволяют Nginx работать с асинхронным и неблокирующим I/O, не дожидаясь, пока клиент или upstream сервер полностью отправят или примут данные.

Когда Nginx получает HTTP-запрос, тело запроса сначала помещается в client_body_buffer. Если запрос маленький, он полностью помещается в буфер, что позволяет быстро передать его upstream или обработчику. Если запрос превышает размер буфера, Nginx записывает остаток на диск во временный файл. Размер и количество буферов настраиваются через директивы типа client_body_buffer_size.

client_body_buffer_size 16k;

Это значит, что Nginx будет выделять 16 килобайт для хранения тела запроса. Если тело больше, оно будет сброшено на диск, что замедляет обработку. Важный момент: слишком маленькие буферы приводят к частым обращениям к диску и увеличению latency, слишком большие — к лишнему потреблению памяти и снижению общей производительности при большом количестве одновременных подключений. Обычно рекомендуется балансировать размер буферов под средний размер ответов вашего сервиса.

Потестировать ответы можно через скрипт ниже:

curl -s -w "%{size_download} %{size_header}" -o /dev/null https://example.com | awk '{print "Тело: "$1" + Заголовки: "$2" = Всего: "$1+$2" байт"}'
Тело: 513 + Заголовки: 264 = Всего: 777 байт

Существуют также прокси буферы для чтения запросов от upstream серверов:

proxy_buffer_size 8k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;

Смысл у них такой же, что и у обычных буферов, но больше тонких настроек под стабильную работу. Отключение же буфера приводит к синхронной передаче ответа от сервера к клиенту, а не дожидаясь пока он полностью запишется и только после этого будет передан. При больших и длинных запросах могут возникать "фантомные" проблемы при передаче ответа клиенту.

Карты

Директива map позволяет один раз вычислять значение на этапе загрузки конфигурации и затем применять его к каждому запросу без лишней логики в обработчике. По сути, это ассоциативная таблица, которая преобразует входное значение (обычно переменную) в другое значение по заданным правилам. Используется она для разветвления логики без выполнения if, что критично для стабильности и предсказуемости. if в контексте запросов опасен тем, что нарушает фазность обработки, может пересоздавать контекст, ломать цепочку директив и приводить к трудноотлавливаемым багам. map же работает полностью на этапе конфигурации, не вмешивается в жизненный цикл запроса и обеспечивает быстрый поиск по заранее скомпилированной таблице. Применяют map, когда нужно подставлять разные значения в зависимости от хоста, URI, заголовков или других переменных, а также когда нужен чистый, детерминированный и быстрый способ разруливания условий без побочных эффектов, которые несёт if.

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

    server {
        location /ws/ {
            proxy_pass http://example.com:8080;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
        }
    }

Входным параметром выступает $http_upgrade, отражающий содержимое заголовка Upgrade. Если заголовок присутствует и содержит любое непустое значение, в $connection_upgrade записывается upgrade. Если заголовок отсутствует или пустой, переменная получает значение close. Такая логика применяется для корректной работы WebSocket, требующих переключения протокола: при наличии запроса на апгрейд сервер обязан вернуть Connection: upgrade, а при его отсутствии — корректно закрыть соединение.

Лучше использовать map для установки переменной, а не логики внутри if. Можно также немного усложнить пример и добавить if вместе c map (но так делать не нужно):

map $http_x_variant $use_cache {
  default    0;
  "~^mobile" 1;
}

server {
  location / {
    if ($use_cache = 1) {
      proxy_cache microcache;
    }
    proxy_pass http://backend;
  }
}

Microcache для тяжёлых динамических страниц

Сокращает пиковую нагрузку на бэкенды при высоком трафике для страниц, которые могут быть свежими несколько секунд. Очень эффективен при burst'ах.

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=microcache:10m max_size=50;
server {
  location / {
    proxy_cache microcache;
    proxy_cache_valid 200 10s;
    proxy_cache_valid 404 1m;
    proxy_cache_methods GET HEAD;
    proxy_cache_lock on; # блокирует одновременные запросы к бэкенду
    proxy_cache_use_stale error timeout updating;
    proxy_pass http://backend;
  }
}

Очень осторожно с кэшированием. Некоторые конечные точки или методы не должны кэшироваться в ответах вообще. Часто кэшируют GET, HEAD запросы с кодом 200, то что запрашивается часто и скорее всего не меняется. Здесь размер кэша ограничен 1 ГБ и при переполнении nginx удаляет старые записи, но всегда надо учитывать особенности ответов, их вес и надо ли их кэшировать. Например: можно временно кэшировать 404 для исключения спама от ботнета

Общие рекомендации

Для уменьшения риска использования найденных уязвимостей nginx, необходимо выключить версию сервера в ответе:

server_tokens off;

Если включить, что и сделано по дефолту:

HTTP/2 200
server: nginx/1.29.4
date: Wed, 10 Dec 2025 10:30:49 GMT

Выключить:

HTTP/2 200
server: nginx
date: Wed, 10 Dec 2025 10:30:49 GMT

Также необходимо отключить игнорирование невалидных заголовков, которые нарушают RFC 2076:

ignore_invalid_headers off;

Заголовок X_TEST: test будет считаться невалидным, когда X-TEST: test пройдет валидацию.

Добавление заголовка HSTS, который принудительно заставляет использовать HTTPS для последующих соединений и защищая от MITM и down grade атак:

add_header Strict-Transport-Security "max-age=63072000" always;

В ответе должно придти:

HTTP/2 200
server: nginx
date: Wed, 10 Dec 2025 11:40:12 GMT
strict-transport-security: max-age=63072000

Эти 3 базовых правила которые следует использовать всегда и везде.