Enabling a web application firewall (WAF) in front of Nginx reduces exposure to common web attacks by inspecting HTTP traffic before it reaches the application. A WAF is most valuable at reverse-proxy edges where many applications share the same ingress, or where legacy code cannot be hardened quickly.
A common open-source WAF stack for Nginx uses ModSecurity v3 plus the OWASP Core Rule Set (CRS). On Ubuntu and Debian, the packaged ModSecurity module (libnginx-mod-http-modsecurity) and CRS (modsecurity-crs) install both the dynamic module and the rule set without a custom build.
Rule-based inspection can introduce false positives plus measurable overhead, so a safe rollout starts in DetectionOnly mode with audit logging enabled. After tuning and exclusions are applied, switch to blocking mode and continue monitoring the audit log.
Related: How to secure Nginx web server
Related: How to configure log rotation for Nginx
Steps to enable a web application firewall for Nginx:
- Install the ModSecurity module and OWASP CRS packages.
$ sudo apt update WARNING: apt does not have a stable CLI interface. Use with caution in scripts. Hit:1 http://ports.ubuntu.com/ubuntu-ports noble InRelease Hit:2 http://ports.ubuntu.com/ubuntu-ports noble-updates InRelease Hit:3 http://ports.ubuntu.com/ubuntu-ports noble-backports InRelease Hit:4 http://ports.ubuntu.com/ubuntu-ports noble-security InRelease Reading package lists... Building dependency tree... Reading state information... All packages are up to date. $ sudo apt install --assume-yes libnginx-mod-http-modsecurity modsecurity-crs WARNING: apt does not have a stable CLI interface. Use with caution in scripts. Reading package lists... Building dependency tree... Reading state information... libnginx-mod-http-modsecurity is already the newest version (1.0.3-1build3). modsecurity-crs is already the newest version (3.3.5-2). 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
- Confirm the ModSecurity module file and module include are present.
$ ls -l /usr/lib/nginx/modules/ngx_http_modsecurity_module.so -rw-r--r-- 1 root root 68448 Mar 31 2024 /usr/lib/nginx/modules/ngx_http_modsecurity_module.so $ ls -l /etc/nginx/modules-enabled/50-mod-http-modsecurity.conf lrwxrwxrwx 1 root root 60 Dec 30 00:36 /etc/nginx/modules-enabled/50-mod-http-modsecurity.conf -> /usr/share/nginx/modules-available/mod-http-modsecurity.conf
- Create a ModSecurity include file that loads the base configuration and CRS rules.
Include /etc/nginx/modsecurity.conf Include /etc/modsecurity/crs/crs-setup.conf Include /etc/modsecurity/crs/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf Include /usr/share/modsecurity-crs/rules/*.conf Include /etc/modsecurity/crs/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf
Package paths vary by distro; adjust the include paths if CRS is installed elsewhere.
- Create the ModSecurity audit log file with restrictive permissions.
$ sudo touch /var/log/nginx/modsec_audit.log $ sudo chown www-data:adm /var/log/nginx/modsec_audit.log $ sudo chmod 640 /var/log/nginx/modsec_audit.log
- Enable ModSecurity in the Nginx HTTP context.
modsecurity on; modsecurity_rules_file /etc/nginx/modsecurity_includes.conf;
Place the directives in a dedicated file such as /etc/nginx/conf.d/modsecurity.conf for global enablement, or inside a single server block for a narrower rollout.
- Set ModSecurity to DetectionOnly for the initial rollout.
# /etc/nginx/modsecurity.conf SecRuleEngine DetectionOnly
DetectionOnly logs rule matches without blocking responses, which keeps the first rollout safer while false positives are tuned.
- Test the Nginx configuration for syntax errors.
$ sudo nginx -t 2025/12/30 00:51:52 [notice] 8918#8918: ModSecurity-nginx v1.0.3 (rules loaded inline/local/remote: 0/921/0) nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
- Reload nginx to apply the WAF.
$ sudo systemctl reload nginx
- Send a CRS-triggering request to generate an audit log entry while in detection mode.
$ curl -i "http://127.0.0.1/?q=<script>alert(4)</script>" HTTP/1.1 200 OK Server: nginx Date: Tue, 30 Dec 2025 00:51:53 GMT Content-Type: text/html Content-Length: 20 Connection: keep-alive Keep-Alive: timeout=15 Vary: Accept-Encoding Last-Modified: Mon, 29 Dec 2025 22:23:50 GMT X-Cache-Status: STALE Upstream cache demo
- Confirm the request appears in /var/log/nginx/modsec_audit.log.
$ sudo tail -n 40 /var/log/nginx/modsec_audit.log ---ewQIGtph---H-- ModSecurity: Warning. Matched "Operator `Rx' with parameter `^[\d.:]+$' against variable `REQUEST_HEADERS:Host' (Value: `127.0.0.1' ) [file "/usr/share/modsecurity-crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "719"] [id "920350"] [rev ""] [msg "Host header is a numeric IP address"] [data "127.0.0.1"] [severity "4"] [ver "OWASP_CRS/3.3.5"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "capec/1000/210/272"] [tag "PCI/6.5.10"] [hostname "127.0.0.1"] [uri "/"] [unique_id "176705591334.070085"] [ref "o0,9v59,9"] ModSecurity: Warning. detected XSS using libinjection. [file "/usr/share/modsecurity-crs/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf"] [line "38"] [id "941100"] [rev ""] [msg "XSS Attack Detected via libinjection"] [data "Matched Data: XSS data found within ARGS:q: <script>alert(4)</script>"] [severity "2"] [ver "OWASP_CRS/3.3.5"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-xss"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "capec/1000/152/242"] [hostname "127.0.0.1"] [uri "/"] [unique_id "176705591334.070085"] [ref "v8,25t:utf8toUnicode,t:urlDecodeUni,t:htmlEntityDecode,t:jsDecode,t:cssDecode,t:removeNulls"] ---ewQIGtph---I-- ---ewQIGtph---J-- ---ewQIGtph---Z--
- Enable blocking mode by setting SecRuleEngine to On.
$ sudo sed -i 's/^SecRuleEngine DetectionOnly$/SecRuleEngine On/' /etc/nginx/modsecurity.conf
SecRuleEngine On can return 403 for legitimate traffic until rule exclusions are tuned; deploy gradually plus monitor the audit log for false positives.
- Re-test the configuration and reload nginx after switching to blocking mode.
$ sudo nginx -t 2025/12/30 00:52:07 [notice] 8945#8945: ModSecurity-nginx v1.0.3 (rules loaded inline/local/remote: 0/921/0) nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
- Verify the test payload returns 403 in blocking mode.
$ curl -i "http://127.0.0.1/?q=<script>alert(6)</script>" HTTP/1.1 403 Forbidden Server: nginx Date: Tue, 30 Dec 2025 00:52:08 GMT Content-Type: text/html Content-Length: 146 Connection: keep-alive Keep-Alive: timeout=15 Vary: Accept-Encoding <html> <head><title>403 Forbidden</title></head> <body> <center><h1>403 Forbidden</h1></center> <hr><center>nginx</center> </body> </html>
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.
