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.

Steps to enable a web application firewall for Nginx:

  1. 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.
  2. 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
  3. 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.

  4. 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
  5. 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.

  6. 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.

  7. 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
  8. Reload nginx to apply the WAF.
    $ sudo systemctl reload nginx
  9. 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
  10. 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--
  11. 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.

  12. 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
  13. 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>