Динамические модули, njs и WAF

Рейтинг: 73% · 17 голосов
Самый подробный курс по Nginx - от первого конфига до production API-gateway. Архитектура и event loop, контексты и location, виртуальные хосты, статика, reverse proxy и upstream-балансировка, кэширование, HTTPS и TLS (post-quantum), HTTP/2 и HTTP/3 QUIC, безопасность и rate limiting, realip, JSON-логи и сквозная трассировка, метрики, тюнинг и траблшутинг. Плюс OpenResty (Lua, фазы, JWT, динамика) и Angie (нативные Prometheus, ACME, dynamic upstream). На основе эталонного gateway и лучших практик. Для новичков и SRE/DevOps. Актуально на 2026 (nginx 1.30/1.31, Angie, ingress-nginx EOL -> Gateway API).
Ответить
Аватара пользователя
Igor_NgINX
Сообщения: 44
Зарегистрирован: 11 май 2026, 05:31

Динамические модули, njs и WAF

Сообщение Igor_NgINX »

Оглавление курса (44)
  1. Что такое Nginx и почему он захватил веб
  2. Установка Nginx, Angie и OpenResty: пакеты, версии, Docker
  3. Первый конфиг с нуля: минимальный сервер за 5 минут
  4. Архитектура процессов: master, worker и event loop
  5. Анатомия конфигурации: контексты, наследование и модульность
  6. Виртуальные хосты: server и server_name
  7. Директива location: матчинг и приоритеты по шагам
  8. Раздача статики: root, alias, try_files, sendfile
  9. Переменные, map и блок if
  10. rewrite, return и канонизация URL
  11. Работа с заголовками: add_header и proxy_set_header
  12. Модель доверия прокси-заголовков: realip и защита от спуфинга
  13. Сжатие: gzip, brotli и zstd
  14. Reverse proxy: proxy_pass и проброс запроса
  15. upstream и балансировка нагрузки
  16. Таймауты, повторы и устойчивость прокси
  17. FastCGI и PHP: nginx + PHP-FPM (и uwsgi/scgi)
  18. WebSocket, gRPC и стриминг через Nginx
  19. Кэширование ответов: proxy_cache и микрокэш
  20. HTTPS и TLS: сертификаты, протоколы, шифры (2026)
  21. Let's Encrypt и ACME: certbot, acme.sh, нативный ACME Angie
  22. HTTP/2 и HTTP/3 (QUIC) в 2026
  23. Безопасность: заголовки, скрытие версии, ограничения
  24. Rate limiting и защита от перегрузки
  25. Контроль доступа: allow/deny, auth_basic, auth_request и mTLS
  26. Логирование: access_log, форматы и структурированный JSON
  27. Сквозная трассировка запросов и OpenTelemetry
  28. Метрики и мониторинг Nginx
  29. Производительность и тюнинг под нагрузку
  30. Траблшутинг: 502, 504, 403, 404 и debug-лог
  31. OpenResty: Nginx как платформа на LuaJIT
  32. Фазы обработки запроса и Lua-хуки
  33. Lua API: ngx.*, shared dict, cosocket и lua-resty-core
  34. Практика OpenResty: авторизация, JWT, кэш, Redis
  35. Angie: современный форк Nginx 2026
  36. Динамическая конфигурация и service discovery
  37. Nginx как API-gateway и Kubernetes Gateway API
  38. Динамические модули, njs и WAF (вы здесь)
  39. Stream-модуль: проксирование TCP и UDP
  40. Деплой, перезагрузка и конфиг как код
  41. Сценарий: статика, SPA и кэширование за CDN
  42. Сценарий: микросервисный gateway по образцу ngx-trace-gateway
  43. Капстоун: собираем production-gateway с нуля
  44. Чек-лист безопасности, CVE 2026 и аудит конфигурации
Ванильный nginx умеет много, но не всё. Нужна геолокация по IP, чтобы резать ботов по странам? Brotli вместо gzip? Подмена строки прямо в теле ответа от легаси-бэкенда, который ты не можешь править? Защита от SQL-инъекций и XSS на уровне шлюза? Маленький кусочек логики на JavaScript для роутинга по SNI без подъёма целого OpenResty? Всё это - расширения. И тут начинается самое интересное, потому что расширять nginx можно тремя совершенно разными способами, и каждый со своими граблями. В этом уроке разберём механику динамических модулей (где ошибка версии превращает рабочий бинарь в кирпич), официальный встроенный njs - который в 2026 пережил смену движка, - и WAF-стратегию, когда тебе реально нужно ловить атаки, а не просто верить в лучшее.

Зачем вообще нужны nginx модули и чем динамический отличается от статического

Архитектурно nginx - это маленькое ядро плюс куча модулей. Даже proxy_pass, gzip, ssl - это модули, просто вкомпилированные в бинарь намертво (статически) на этапе сборки через ./configure --with-... Проблема статической линковки очевидна: чтобы добавить один модуль, надо пересобрать ВЕСЬ nginx и заменить бинарь. На проде это даунтайм и риск.

Динамический модуль - это отдельный .so-файл (shared object), который ядро подгружает на старте директивой load_module. Бинарь nginx остаётся прежним, ты просто кладёшь рядом .so и говоришь его грузить. Это и есть смысл nginx load_module: расширять без пересборки ядра.

Код: Выделить всё

# на самом верху nginx.conf, в main-контексте, ДО блока events
load_module modules/ngx_http_geoip2_module.so;
load_module modules/ngx_http_headers_more_filter_module.so;
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;

events {
    worker_connections 10240;
}
Важная деталь: порядок load_module имеет значение для фильтр-модулей. Фильтры выстраиваются в цепочку, и тот, что загружен позже, обычно отрабатывает над телом раньше (цепочка фильтров идёт в обратном порядке регистрации). Для большинства случаев это неважно, но если у тебя brotli и substitutions конфликтуют за тело ответа - порядок загрузки начинает играть.

Где брать .so? Два пути. Первый - готовые пакеты из официального репозитория nginx (пакеты вида nginx-module-geoip2, nginx-module-otel и десятки других) или из репозитория GetPageSpeed, где собрано 100+ модулей под конкретные версии. Это путь по умолчанию: меньше боли, авто-совместимость с пакетным nginx. Второй путь - собрать самому, и вот тут надо понимать механику до винтика.

Изображение

Сборка стороннего модуля под СВОЮ версию: совместимость как закон

Допустим, готового пакета нет (свежий форк модуля, экзотическая платформа, патч). Тогда модуль собирается так:

Код: Выделить всё

# 1. узнаём ТОЧНУЮ версию и флаги сборки своего nginx
nginx -V
# nginx version: nginx/1.30.0
# built by gcc ...
# configure arguments: --prefix=/etc/nginx --with-http_ssl_module ...

# 2. качаем ИСХОДНИКИ РОВНО ТОЙ ЖЕ версии
wget https://nginx.org/download/nginx-1.30.0.tar.gz
tar xzf nginx-1.30.0.tar.gz
cd nginx-1.30.0

# 3. configure с ТЕМИ ЖЕ аргументами + наш модуль динамически
./configure --with-compat --add-dynamic-module=../ngx_http_geoip2_module
make modules

# 4. готовый .so лежит в objs/
cp objs/ngx_http_geoip2_module.so /etc/nginx/modules/
Ключевых граблей здесь две, и обе ломают прод жёстко.

Грабля версии. Модуль собирается под конкретную версию nginx. Не похожую, а конкретную. Между минорными релизами меняются внутренние структуры (ngx_http_request_t и десятки других), смещения полей, ABI. Если собрать модуль под 1.29.x и подсунуть в 1.30.0, в лучшем случае nginx откажется грузить и упадёт на старте с ошибкой, в худшем - подгрузит и будет рандомно сегфолтиться под нагрузкой, потому что читает структуру по неправильным смещениям. Поэтому шаг nginx -V не опционален, а обязателен.

Грабля флагов. Если ты собирал модуль без --with-compat, а ядро nginx собрано с другим набором define-ов (например, с debug или без него), nginx при загрузке выдаст знаменитое "module is not binary compatible". Флаг --with-compat включает режим бинарной совместимости и сглаживает часть различий в флагах - его ставят почти всегда. Но он не спасает от разницы в версиях, только от разницы в некоторых опциях конфигурации.
Практическое правило: если можешь поставить модуль пакетом под свою версию - ставь пакетом. Самосбор оправдан, только когда пакета нет. И после КАЖДОГО апгрейда nginx все самосборные .so надо пересобирать заново.
Модульный арсенал 2026: что ставить и зачем

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

headers-more (ngx_http_headers_more) - кумулятивная правка заголовков. Штатный add_header умеет только добавлять и, что коварно, перестаёт работать в location, если там появился свой add_header (наследование заголовков в nginx неинтуитивное). headers-more даёт more_set_headers (перезаписать), more_clear_headers (удалить), причём работает и на заголовках ответа от апстрима. Классика - убрать палевный Server от бэкенда:

Код: Выделить всё

more_clear_headers 'Server';
more_set_headers 'X-Frame-Options: SAMEORIGIN';
ngx_brotli - сжатие Brotli, нативно nginx его НЕ умеет, это сторонний динамический модуль (два .so: filter и static). Brotli жмёт текст плотнее gzip при сравнимой нагрузке. Помни: нативный Zstd есть только в Angie, в ванильном nginx ни Brotli, ни Zstd штатно нет.

Код: Выделить всё

brotli on;
brotli_comp_level 5;
brotli_types text/css application/javascript application/json image/svg+xml;
brotli_static on;   # отдавать предсжатые .br с диска
ngx_http_geoip2 - геолокация по базам MaxMind (формат .mmdb), это и есть актуальный nginx geoip2. Старый модуль geoip с базами в legacy-формате мёртв, MaxMind его не обновляет. geoip2 читает GeoLite2/GeoIP2 и раскладывает данные в переменные:

Код: Выделить всё

geoip2 /etc/nginx/geoip/GeoLite2-Country.mmdb {
    $geoip2_country_code source=$remote_addr country iso_code;
}
map $geoip2_country_code $blocked_country {
    default 0;
    RU 0;
    CN 1;   # пример: режем по стране
}
server {
    if ($blocked_country) { return 403; }
}
Тонкость: source должен быть РЕАЛЬНЫМ адресом клиента. Если перед nginx стоит CDN или балансировщик, $remote_addr - это адрес прокси, а не клиента. Сначала восстанови настоящий IP модулем realip (set_real_ip_from на сети CDN), и только потом скармливай его в geoip2 - иначе будешь геолоцировать свой же дата-центр.

ngx_http_substitutions (substitutions_filter) - правка ТЕЛА ответа на лету по регэкспам, замена строк. Спасательный круг, когда бэкенд возвращает захардкоженный http:// вместо https://, или старый домен, а исходники трогать нельзя. Дорого по CPU (буферизует и парсит тело), на горячих путях аккуратно.

ngx_otel_module - официальная OpenTelemetry-трассировка. Важный факт 2026: это OPEN-SOURCE модуль (не только для Plus), доступен с nginx 1.25.3, в -otel docker-образах включён по умолчанию с 1.27.5. Шлёт спаны по OTLP/gRPC, пробрасывает W3C trace context. Накладные ~10-15%, против ~50% у старых сторонних трейсеров. Подробно его разбираем в уроке про наблюдаемость, тут отмечаем как штатный путь.

nginx njs: официальный JavaScript внутри nginx и революция QuickJS

Теперь к njs. Это официальный модуль от команды nginx, дающий писать логику на JavaScript прямо в конфиге. Не Lua, не отдельный язык - подмножество JS. Ставится тоже динамическим модулем (nginx-module-njs), грузится load_module и даёт директивы js_import, js_set, js_content, и аналоги в stream-контексте.

И вот тут - главная новость 2026, которую надо усвоить чётко, иначе будешь писать на мёртвом движке. У njs ДВА движка:
  • Классический njs-движок - родной, написан под nginx. Поддерживает ES5 плюс отдельные кусочки ES6. С выходом njs 1.0.0 он официально DEPRECATED. Не выпилен пока, но новые конфиги на нём писать не надо.
  • QuickJS - встраиваемый движок Фабриса Беллара, подключён к njs (доступен с njs 0.9.x). Даёт полноценный ES2023: модули, async/await, Proxy, BigInt, асинхронные генераторы. Это рекомендованный путь.
Переключение - одной директивой:

Код: Выделить всё

load_module modules/ngx_http_js_module.so;

js_engine qjs;          # выбираем QuickJS вместо классического движка

js_import main from /etc/nginx/njs/main.js;
Релиз njs 0.9.9 (январь 2026) довёл QuickJS до http И stream одновременно - то есть теперь и L7, и L4 на современном JS. Раньше qjs был частично. Если у тебя njs старее - обновляйся, потому что (внимание, патч-гигиена) была CVE-2026-8711: heap overflow в реализации js_fetch. Урок простой: njs - это код, исполняющий запросы; держи модуль свежим так же строго, как само ядро.

njs vs Lua/OpenResty. Честное сравнение без фанатизма:
  • njs - официальный, поддерживается командой nginx, ставится одним пакетом, никаких форков LuaJIT. Язык знаком всем (JS). Есть Fetch API и WebCrypto прямо из коробки - можно делать HTTP-запросы и криптографию без сторонних библиотек.
  • njs - легче по экосистеме, но и менее мощный, чем OpenResty. Нет богатого набора lua-resty-* библиотек, нет таких зрелых паттернов кэша (mlcache) и балансировки (balancer_by_lua).
  • Критичное ограничение модели: асинхронность (await, Fetch) работает только в js_content и js_access. В js_set функция обязана быть СИНХРОННОЙ и быстрой - она вызывается при вычислении переменной, заблокировать там воркер нельзя. Это частая ошибка новичков: пытаются сделать async-fetch в js_set и ловят непонятное поведение.
Где njs реально к месту: роутинг по SNI в stream (выбрать апстрим по имени из ClientHello без расшифровки), генерация значений и подписей, мелкие фильтры заголовков/тела со своей логикой, лёгкая авторизация и проверка JWT через WebCrypto - когда тащить целый OpenResty ради десятка строк JS избыточно.

nginx waf: ModSecurity, Coraza и стратегия защиты

Модули - это удобство. WAF (Web Application Firewall) - это безопасность, и тут расстановка сил в 2026 поменялась. WAF на шлюзе ловит инъекции, XSS, path traversal, сканеры - по набору правил, до того как мусор дойдёт до приложения.

nginx modsecurity (ModSecurity-nginx) - исторически главный коннектор. Это динамический модуль (ngx_http_modsecurity_module.so), который подключает движок ModSecurity v3 (libmodsecurity) и грузит правила OWASP Core Rule Set (CRS) - индустриальный набор, закрывающий весь топ OWASP.

Код: Выделить всё

load_module modules/ngx_http_modsecurity_module.so;

server {
    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsec/main.conf;
    # main.conf подключает modsecurity.conf + crs-setup.conf + rules/*.conf
}
Большая оговорка 2026: вокруг ModSecurity много движения по поддержке. F5 свернула собственный коммерческий ModSecurity WAF для NGINX (End-of-Life), а команда OWASP CRS сместила фокус на новый движок Coraza из-за вопросов к долгосрочной судьбе самого ModSecurity. Сам OSS-движок ModSecurity и коннектор пока живы и патчатся, CRS на нём работает - но смотреть надо вперёд.

Coraza - современная альтернатива, тот самый вектор. WAF на Go, бинарно-совместимый с языком правил SecLang и на 100% совместимый с OWASP CRS v4 - то есть твои правила переезжают почти без изменений. Coraza уже встраивают в шлюзы (ngrok, APISIX, Caddy), а коннектор под nginx активно допиливают силами сообщества. Если строишь WAF с нуля в 2026 - закладывайся на CRS v4 и держи Coraza как стратегический выбор.

Третий путь - внешний WAF: вынести фильтрацию в отдельный слой (Coraza/CRS контейнером перед шлюзом, либо облачный WAF/CDN с защитой). Развязывает жизненный цикл WAF и nginx, проще обновлять правила, но добавляет сетевой хоп.

Что бы ты ни выбрал - три железных правила эксплуатации WAF: (1) сначала режим только-логирование (DetectionOnly), собери ложные срабатывания на реальном трафике, и лишь потом блокировку; (2) настрой paranoia level CRS под себя - максимальная паранойя ловит больше, но и ложит легитимные запросы; (3) WAF не отменяет патчей. Это эшелон, а не замена обновлению nginx и приложения.

Практика: динамический модуль geoip2 плюс njs на QuickJS

Соберём боевой фрагмент: грузим динамический geoip2, восстанавливаем реальный IP, и на njs/QuickJS делаем простой контроллер - отдаёт JSON со страной клиента и режет запрос, если страна в чёрном списке. Это маленький, но настоящий API-эндпоинт без бэкенда.

Код: Выделить всё

# /etc/nginx/njs/geo.js
function info(r) {
    var country = r.variables.geoip2_country_code || 'XX';
    var blocked = ['CN', 'KP'];
    if (blocked.indexOf(country) >= 0) {
        r.return(403, JSON.stringify({error: 'blocked', country: country}));
        return;
    }
    r.headersOut['Content-Type'] = 'application/json';
    r.return(200, JSON.stringify({ip: r.remoteAddress, country: country}));
}
export default { info };

Код: Выделить всё

# nginx.conf (main)
load_module modules/ngx_http_geoip2_module.so;
load_module modules/ngx_http_js_module.so;

http {
    js_engine qjs;                          # современный движок ES2023
    js_import geo from /etc/nginx/njs/geo.js;

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

    # доверяем реальному IP только от своего CDN (пример сети)
    set_real_ip_from 10.0.0.0/8;
    real_ip_header X-Forwarded-For;
    real_ip_recursive on;

    server {
        listen 80;
        location = /whoami {
            js_content geo.info;            # async тут разрешён, это js_content
        }
    }
}
Проверяем:

Код: Выделить всё

nginx -t && nginx -s reload
curl -s http://127.0.0.1/whoami
# {"ip":"203.0.113.7","country":"DE"}
Разбор: load_module грузит оба .so из modules/. js_engine qjs включает QuickJS - без неё njs молча возьмёт устаревший классический движок. geoip2 кладёт код страны в $geoip2_country_code, который njs читает через r.variables. real_ip обязателен - без него за CDN ты геолоцируешь не клиента. js_content отдаёт ответ полностью из JS, бэкенд не нужен.

Типичные грабли
  • "module is not binary compatible" - .so собран под другую версию nginx или без --with-compat. Лечение: nginx -V, скачать исходники РОВНО той версии, пересобрать. После апгрейда nginx - пересобрать все самосборные модули.
  • load_module не в том месте - директива работает только в main-контексте (самый верх, до events), не внутри http или server. Иначе "load_module directive is not allowed here".
  • Порядок load_module для конфликтующих фильтров (brotli и substitutions над одним телом) - помни про обратный порядок цепочки фильтров.
  • njs на мёртвом движке - забыл js_engine qjs, пишешь async/Proxy и получаешь синтаксические ошибки, потому что работает классический ES5-движок. И не забывай обновлять njs - была CVE-2026-8711 в js_fetch.
  • async в js_set - не работает по дизайну. Fetch/await только в js_content и js_access. В js_set - синхронно и быстро.
  • geoip2 без realip за прокси - геолоцируешь адрес балансировщика, а не клиента. Сначала восстанови IP, потом геолоцируй.
  • WAF сразу в блокировку - словишь шквал ложных срабатываний на проде. Сначала DetectionOnly, потом блок.
Мини-лаба: руками по шагам
  • Шаг 1. Узнай свою версию: nginx -V. Запиши версию и флаги.
  • Шаг 2. Поставь njs пакетом (nginx-module-njs) или собери под свою версию через --add-dynamic-module + make modules. Добавь load_module ngx_http_js_module.so в main.
  • Шаг 3. Включи js_engine qjs и проверь, что движок современный: напиши в JS const x = [1,2].at(-1); (ES2022) - на классическом движке упадёт, на QuickJS отработает.
  • Шаг 4. Скачай GeoLite2-Country.mmdb (нужен бесплатный аккаунт MaxMind), поставь geoip2-модуль, пропиши блок geoip2 и переменную страны.
  • Шаг 5. Собери эндпоинт /whoami из практики. Проверь curl - сначала с локалхоста (страна XX или твоя), потом подставь X-Forwarded-For с публичным IP и убедись, что country меняется.
  • Шаг 6. Добавь страну своего тестового IP в blocked внутри geo.js, перезагрузи, убедись что отдаётся 403.
  • Шаг 7 (со звёздочкой). Подними рядом контейнер с Coraza+CRS v4 в режиме DetectionOnly, прогони через него /whoami?id=1' OR '1'='1 и посмотри в логе сработавшее правило SQLi.
Контрольные вопросы
  • Почему .so-модуль, собранный под nginx 1.29.x, нельзя грузить в 1.30.0, и что для этого надо сделать?
  • Какой движок njs в 2026 рекомендован, какой deprecated, и какой директивой переключаются?
  • Почему async/Fetch нельзя использовать в js_set, и где его использовать можно?
  • Чем geoip2 отличается от старого geoip и почему перед ним обязателен модуль realip за CDN?
Итог

Расширение nginx - это три инструмента под три задачи. Динамические модули (load_module) добавляют функциональность без пересборки ядра, но требуют железной совместимости версий - собирай под свою или ставь пакетом, пересобирай после апгрейда. njs даёт официальный JavaScript внутри nginx; в 2026 пиши на QuickJS (js_engine qjs, ES2023), классический движок deprecated, и держи модуль свежим из-за CVE в js_fetch. WAF (ModSecurity или стратегически Coraza, оба с OWASP CRS v4) - это эшелон защиты на шлюзе, который запускают сначала в режиме логирования. Дальше - OpenResty и Lua, где те же идеи раскрываются на полную мощность.
👍4 ❤️3 🔥3 😄 🤔1
Аватара пользователя
raynman
Сообщения: 1
Зарегистрирован: 24 май 2026, 12:22

Re: Динамические модули, njs и WAF

Сообщение raynman »

Спасибо, наконец дошло почему мой модуль кидал not binary compatible - я качал исходники свежее чем мой пакетный nginx. nginx -V реально спас.
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
Kippersnack
Сообщения: 1
Зарегистрирован: 12 май 2026, 19:00

Re: Динамические модули, njs и WAF

Сообщение Kippersnack »

А есть смысл вообще учить классический njs движок если он deprecated? Или сразу qjs и не оглядываться? У меня на серве njs 0.9.4, обновлюсь до 0.9.9 раз там и stream завезли и дырку в fetch закрыли.
👍 ❤️ 🔥1 😄 🤔
Ответить
← Предыдущая глава
Nginx как API-gateway и Kubernetes Gateway API
Следующая глава →
Stream-модуль: проксирование TCP и UDP

Все главы курса «Nginx профессионально: от первого конфига до API-gateway на OpenResty и Angie»

Поделиться темой: ✈ Telegram VK
Похожие запросы: njs и динамические модули nginx

Вернуться в «Nginx профессионально: от первого конфига до API-gateway на OpenResty и Angie»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость