Public Nginx sites that accept untrusted requests need inspection before traffic reaches the application. A web application firewall at the reverse proxy can log or reject obvious cross-site scripting, protocol abuse, and other hostile payloads before upstream code handles them.
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 path does not require compiling Nginx or the rule set from source.
A safe rollout starts in DetectionOnly mode so rule hits are written to the audit log before live traffic is blocked. The packaged CRS loader is written for Apache, so Nginx should use explicit Include lines in its ModSecurity include file. The packaged CRS setup also keeps blocking decisions in pass mode until its default actions are changed for enforcement.
$ sudo apt update Hit:1 http://archive.ubuntu.com/ubuntu resolute InRelease Hit:2 http://archive.ubuntu.com/ubuntu resolute-updates InRelease Hit:3 http://archive.ubuntu.com/ubuntu resolute-backports InRelease Hit:4 http://security.ubuntu.com/ubuntu resolute-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... The following NEW packages will be installed: libmodsecurity3t64 libnginx-mod-http-modsecurity libnginx-mod-http-ndk modsecurity-crs nginx ##### snipped Setting up modsecurity-crs (3.3.8-1) ... Setting up nginx (1.28.3-2ubuntu1.2) ... Setting up libnginx-mod-http-modsecurity (1.0.3-2build6) ...
$ dpkg-query -W -f '${binary:Package}\t${Version}\n' \
nginx libnginx-mod-http-modsecurity modsecurity-crs libmodsecurity3t64
libmodsecurity3t64:arm64 3.0.14-1build2
libnginx-mod-http-modsecurity 1.0.3-2build6
modsecurity-crs 3.3.8-1
nginx 1.28.3-2ubuntu1.2
$ 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.
$ sudoedit /etc/nginx/modsecurity_includes.conf
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.
$ sudoedit /etc/nginx/conf.d/modsecurity.conf
# /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.
$ 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
$ sudo nginx -t 2026/06/06 11:28:12 [notice] 3078#3078: ModSecurity-nginx v1.0.3 (rules loaded inline/local/remote: 0/924/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
$ sudo systemctl reload nginx
Related: How to manage the Nginx service
$ 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.28.3 (Ubuntu) Date: Sat, 06 Jun 2026 11:28:12 GMT Content-Type: text/html Content-Length: 615 Last-Modified: Sat, 06 Jun 2026 11:28:10 GMT Connection: keep-alive ETag: "6a24044a-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.
$ sudo grep -n 'XSS Attack Detected via libinjection' /var/log/nginx/modsec_audit.log 26:ModSecurity: Warning. detected XSS using libinjection. ##### snipped [id "941100"] [msg "XSS Attack Detected via libinjection"] ##### snipped [uri "/"] [unique_id "178074529270.355962"]
That confirms the module is active, the CRS rules are loaded, and the request is reaching the audit log before enforcement is enabled.
$ sudoedit /etc/nginx/modsecurity.conf
Change the active rule engine line.
SecRuleEngine On
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.
$ sudoedit /etc/modsecurity/crs/crs-setup.conf
Replace the two active default-action lines near the top of the file.
SecDefaultAction "phase:1,log,auditlog,deny,status:403" SecDefaultAction "phase:2,log,auditlog,deny,status:403"
Changing only SecRuleEngine to On is not enough when the CRS setup file still leaves SecDefaultAction in pass mode.
$ sudo grep -n '^SecRuleEngine' /etc/nginx/modsecurity.conf 7:SecRuleEngine On
$ sudo grep -n '^SecDefaultAction' /etc/modsecurity/crs/crs-setup.conf 97:SecDefaultAction "phase:1,log,auditlog,deny,status:403" 98:SecDefaultAction "phase:2,log,auditlog,deny,status:403"
$ sudo nginx -t 2026/06/06 11:28:12 [notice] 3095#3095: ModSecurity-nginx v1.0.3 (rules loaded inline/local/remote: 0/924/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
$ sudo systemctl reload nginx
Related: How to manage the Nginx service
$ 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.28.3 (Ubuntu) Date: Sat, 06 Jun 2026 11:28:13 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.