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 (libmodsecurity) plus the ModSecurity-nginx connector module. Requests are evaluated against a rule set, most commonly the OWASP Core Rule Set (CRS), and matching rules can be logged, blocked, or both.

Rule-based inspection can introduce false positives plus measurable overhead, so a safe rollout starts in DetectionOnly mode with audit logging enabled. Commands below target Ubuntu or Debian with systemd plus the distro nginx package; dynamic modules must match the installed Nginx version, so module rebuilds are expected after nginx upgrades.

Steps to enable a web application firewall for Nginx:

  1. Install build packages required for libmodsecurity plus Nginx module compilation.
    $ sudo apt update && sudo apt install --assume-yes \
      ca-certificates curl git build-essential pkgconf \
      autoconf automake libtool flex bison \
      libpcre2-dev zlib1g-dev libssl-dev \
      libxml2 libxml2-dev libyajl-dev \
      libcurl4-openssl-dev libgeoip-dev liblmdb-dev libmaxminddb-dev
    Hit:1 http://archive.ubuntu.com/ubuntu jammy InRelease
    ##### snipped #####
    Setting up libyajl-dev:amd64 (2.1.0-3build2) ...
    Processing triggers for libc-bin (2.35-0ubuntu3.6) ...

    Older Debian or Ubuntu releases may use libpcre3-dev instead of libpcre2-dev.

  2. Create a workspace directory for the build.
    $ mkdir -p ~/src/modsecurity-nginx
  3. Switch into the workspace directory.
    $ cd ~/src/modsecurity-nginx
  4. Populate the workspace with the required source repositories.
    $ git clone --depth 1 https://github.com/SpiderLabs/ModSecurity.git
    Cloning into 'ModSecurity'...
    remote: Enumerating objects: 100, done.
    remote: Counting objects: 100% (100/100), done.
    ##### snipped #####
    $ git clone --depth 1 https://github.com/SpiderLabs/ModSecurity-nginx.git
    Cloning into 'ModSecurity-nginx'...
    remote: Enumerating objects: 100, done.
    ##### snipped #####
  5. Install libmodsecurity from source.
    $ cd ~/src/modsecurity-nginx/ModSecurity
    $ git submodule update --init --recursive
    Submodule 'others/libinjection' registered for path 'others/libinjection'
    ##### snipped #####
    $ ./build.sh
    ##### snipped #####
    $ ./configure
    checking for gcc... gcc
    checking whether the C compiler works... yes
    ##### snipped #####
    $ make -j"$(nproc)"
    ##### snipped #####
    $ sudo make install
    ##### snipped #####
    $ sudo ldconfig
  6. Create the ModSecurity configuration directory for Nginx.
    $ sudo mkdir -p /etc/nginx/modsec
  7. Copy unicode.mapping into the ModSecurity configuration directory.
    $ sudo cp ~/src/modsecurity-nginx/ModSecurity/unicode.mapping /etc/nginx/modsec/unicode.mapping
  8. Create /etc/nginx/modsec/modsecurity.conf in DetectionOnly mode.
    SecRuleEngine DetectionOnly
    
    SecRequestBodyAccess On
    SecRequestBodyLimit 13107200
    SecRequestBodyNoFilesLimit 131072
    SecRequestBodyInMemoryLimit 131072
    
    SecResponseBodyAccess Off
    
    SecAuditEngine RelevantOnly
    SecAuditLogRelevantStatus "^(?:5|4(?!04))"
    SecAuditLogParts ABIJDEFHZ
    SecAuditLogType Serial
    SecAuditLog /var/log/nginx/modsec_audit.log
    
    SecTmpDir /tmp
    SecDataDir /tmp
    
    SecUnicodeMapFile /etc/nginx/modsec/unicode.mapping 20127

    DetectionOnly logs rule matches without blocking responses, which keeps the first rollout safer while false positives are tuned.

  9. Install the OWASP Core Rule Set under /etc/nginx/modsec/.
    $ sudo git clone --depth 1 https://github.com/coreruleset/coreruleset.git /etc/nginx/modsec/coreruleset
    Cloning into '/etc/nginx/modsec/coreruleset'...
    ##### snipped #####
    $ sudo cp /etc/nginx/modsec/coreruleset/crs-setup.conf.example /etc/nginx/modsec/coreruleset/crs-setup.conf
  10. Create /etc/nginx/modsec/main.conf to load ModSecurity plus CRS rules.
    Include /etc/nginx/modsec/modsecurity.conf
    Include /etc/nginx/modsec/coreruleset/crs-setup.conf
    Include /etc/nginx/modsec/coreruleset/rules/*.conf
  11. Create the ModSecurity audit log file for Nginx.
    $ 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
  12. Build the ModSecurity dynamic module for the installed Nginx version.
    $ cd ~/src/modsecurity-nginx
    $ NGINX_VERSION="$(nginx -v 2>&1 | sed 's/^nginx version: nginx\\///')"
    $ echo "$NGINX_VERSION"
    1.22.1
    $ curl -fsSLO "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz"
    $ tar -xzf "nginx-${NGINX_VERSION}.tar.gz"
    $ cd "nginx-${NGINX_VERSION}"
    $ ./configure --with-compat --add-dynamic-module=../ModSecurity-nginx
    ##### snipped #####
    $ make modules
    ##### snipped #####
    $ ls -1 objs/ngx_http_modsecurity_module.so
    objs/ngx_http_modsecurity_module.so

    The module must match the installed Nginx version; repeat the module build after nginx package upgrades.

  13. Install the compiled module into the Nginx modules directory.
    $ sudo cp ~/src/modsecurity-nginx/nginx-${NGINX_VERSION}/objs/ngx_http_modsecurity_module.so /usr/lib/nginx/modules/
    $ sudo ls -l /usr/lib/nginx/modules/ngx_http_modsecurity_module.so
    -rw-r--r-- 1 root root 123456 Dec 14 12:34 /usr/lib/nginx/modules/ngx_http_modsecurity_module.so

    On some builds the modules directory is different; confirm with nginx -V output showing the prefix, then adjust the destination accordingly.

  14. Load the ModSecurity module via /etc/nginx/modules-enabled/50-modsecurity.conf.
    $ sudo tee /etc/nginx/modules-enabled/50-modsecurity.conf >/dev/null <<'EOF'
    load_module modules/ngx_http_modsecurity_module.so;
    EOF

    If /etc/nginx/nginx.conf does not include /etc/nginx/modules-enabled/*.conf, add the load_module line near the top of /etc/nginx/nginx.conf instead.

  15. Enable ModSecurity in the Nginx HTTP context.
    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsec/main.conf;

    Place the two directives into a dedicated file such as /etc/nginx/conf.d/modsecurity.conf for global enablement, or move them into a single server block for a narrower rollout.

  16. Test the Nginx configuration for syntax errors.
    $ sudo nginx -t
    nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
    nginx: configuration file /etc/nginx/nginx.conf test is successful
  17. Reload nginx to apply the WAF.
    $ sudo systemctl reload nginx
  18. Send a CRS-triggering request to generate an audit log entry.
    $ curl -i "http://127.0.0.1/?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E"
    HTTP/1.1 200 OK
    Server: nginx
    Date: Sun, 14 Dec 2025 12:35:12 GMT
    Content-Type: text/html
    Content-Length: 615
    Connection: keep-alive
    ##### snipped #####
  19. Confirm the request appears in /var/log/nginx/modsec_audit.log.
    $ sudo tail -n 60 /var/log/nginx/modsec_audit.log
    ##### snipped #####
    Message: Warning. Pattern match "<script" at ARGS:q. [file "/etc/nginx/modsec/coreruleset/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf"] [line "#####"] [id "941100"] [msg "XSS Attack Detected"] [severity "CRITICAL"]
    ##### snipped #####
  20. Enable blocking mode by setting SecRuleEngine to On.
    $ sudo sed -i 's/^SecRuleEngine DetectionOnly$/SecRuleEngine On/' /etc/nginx/modsec/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.

  21. Reload nginx to apply the ModSecurity engine mode change.
    $ sudo systemctl reload nginx
  22. Verify the test payload returns 403 in blocking mode.
    $ curl -i "http://127.0.0.1/?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E"
    HTTP/1.1 403 Forbidden
    Server: nginx
    Date: Sun, 14 Dec 2025 12:36:03 GMT
    Content-Type: text/html
    Content-Length: 153
    Connection: keep-alive
    ##### snipped #####
Discuss the article:

Comment anonymously. Login not required.