Enabling a web application firewall in front of Nginx adds request inspection before traffic reaches the application. That extra inspection helps catch obvious cross-site scripting, protocol abuse, and other hostile patterns at the reverse proxy instead of leaving every request decision to upstream code.
On current Ubuntu and Debian packaging, the libnginx-mod-http-modsecurity package ships the ModSecurity v3 dynamic module together with /etc/nginx/modsecurity.conf and /etc/nginx/modsecurity_includes.conf. The modsecurity-crs package provides the OWASP Core Rule Set under /etc/modsecurity/crs and /usr/share/modsecurity-crs/rules, so the supported package-based path does not require compiling Nginx or the rule set from source.
A safe rollout still starts in DetectionOnly mode so rule hits are written to the audit log before live traffic is blocked. One current packaging wrinkle matters on Nginx: /usr/share/modsecurity-crs/owasp-crs.load contains IncludeOptional directives for Apache loading, so /etc/nginx/modsecurity_includes.conf should use explicit Include lines for the CRS files instead of loading /owasp-crs.load directly through modsecurity_rules_file.
Related: How to improve Nginx security
Related: How to configure log rotation for Nginx
Steps to enable a web application firewall for Nginx:
- Open a terminal session with an account that can use sudo.
- Install the Nginx ModSecurity module and the packaged CRS rules.
$ sudo apt update 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... $ sudo apt install --assume-yes libnginx-mod-http-modsecurity modsecurity-crs 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 package installed the module include plus the default ModSecurity configuration files.
$ sudo ls -l /etc/nginx/modules-enabled/50-mod-http-modsecurity.conf /etc/nginx/modsecurity.conf /etc/nginx/modsecurity_includes.conf lrwxrwxrwx 1 root root 60 Apr 9 13:08 /etc/nginx/modules-enabled/50-mod-http-modsecurity.conf -> /usr/share/nginx/modules-available/mod-http-modsecurity.conf -rw-r--r-- 1 root root 9100 Mar 31 2024 /etc/nginx/modsecurity.conf -rw-r--r-- 1 root root 62 Mar 31 2024 /etc/nginx/modsecurity_includes.conf
- Verify the packaged rule engine still starts in DetectionOnly mode.
$ sudo grep -n '^SecRuleEngine' /etc/nginx/modsecurity.conf 7:SecRuleEngine DetectionOnly
The packaged /etc/nginx/modsecurity.conf already points the audit log to /var/log/nginx/modsec_audit.log, so the main setup work is loading the rule set and enabling the module in the Nginx configuration.
- Replace /etc/nginx/modsecurity_includes.conf with explicit CRS include lines that Nginx can parse.
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
Do not point modsecurity_rules_file straight at /usr/share/modsecurity-crs/owasp-crs.load on current Ubuntu or Debian packages. That file contains IncludeOptional directives for Apache loading and causes nginx -t to fail when ModSecurity-nginx parses it.
- Enable ModSecurity in the http context so every server block inherits the WAF.
# /etc/nginx/conf.d/modsecurity.conf modsecurity on; modsecurity_rules_file /etc/nginx/modsecurity_includes.conf;
For a narrower rollout, place the same two directives inside one specific server block instead of /etc/nginx/conf.d/modsecurity.conf.
- Create the audit log path with permissions Nginx can use.
$ 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
- Test the Nginx configuration before reloading it.
$ sudo nginx -t 2026/04/09 13:08:45 [notice] 3449#3449: 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
Related: How to test Nginx configuration
- Reload nginx to apply the WAF configuration.
$ sudo systemctl reload nginx
Related: How to manage the Nginx service
- Send a request that should trigger the CRS while the rule engine is still in DetectionOnly mode.
$ curl --silent --show-error -D - -o /dev/null 'http://127.0.0.1/?q=%3Cscript%3Ealert(4)%3C%2Fscript%3E' HTTP/1.1 200 OK Server: nginx/1.24.0 (Ubuntu) Date: Thu, 09 Apr 2026 13:08:57 GMT Content-Type: text/html Content-Length: 615 Last-Modified: Thu, 09 Apr 2026 13:08:18 GMT Connection: keep-alive ETag: "69d7a4c2-267" Accept-Ranges: bytes
Use the real site hostname, or add --resolve example.com:80:127.0.0.1, when 127.0.0.1 does not map to the site that should be protected. A 200 response here is expected because DetectionOnly logs the match without blocking it.
- Confirm the request hit the audit log.
$ sudo grep -n 'XSS Attack Detected via libinjection' /var/log/nginx/modsec_audit.log | tail --lines=1 26:ModSecurity: Warning. detected XSS using libinjection. [file "/usr/share/modsecurity-crs/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf"] [line "38"] [id "941100"] [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 "177574013781.138244"] [ref "v8,25t:utf8toUnicode,t:urlDecodeUni,t:htmlEntityDecode,t:jsDecode,t:cssDecode,t:removeNulls"]
That confirms the module is active, the CRS rules are loaded, and the request is reaching the audit log before enforcement is enabled.
- Switch the rule engine from DetectionOnly to enforcement mode.
$ sudo sed --in-place 's/^SecRuleEngine DetectionOnly$/SecRuleEngine On/' /etc/nginx/modsecurity.conf
Blocking mode can return 403 for legitimate traffic until exclusions are tuned. Keep a rollback path ready and watch the audit log during the first live rollout.
- Verify the rule engine now shows On.
$ sudo grep -n '^SecRuleEngine' /etc/nginx/modsecurity.conf 7:SecRuleEngine On
- Test the configuration and reload nginx after turning blocking on.
$ sudo nginx -t 2026/04/09 13:09:07 [notice] 3494#3494: 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 $ sudo systemctl reload nginx
Related: How to test Nginx configuration
Related: How to manage the Nginx service - Verify the same test payload is now blocked with HTTP 403.
$ curl --silent --show-error -D - -o /dev/null 'http://127.0.0.1/?q=%3Cscript%3Ealert(6)%3C%2Fscript%3E' HTTP/1.1 403 Forbidden Server: nginx/1.24.0 (Ubuntu) Date: Thu, 09 Apr 2026 13:10:10 GMT Content-Type: text/html Content-Length: 162 Connection: keep-alive
At this point the WAF is enforcing the packaged CRS. The next production step is tuning exclusions in /etc/modsecurity/crs/crs-setup.conf or separate local rule files so legitimate application traffic is not blocked.
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.
