Control panels

Implementing a Web Application Firewall on Nginx with ModSecurity

Install the ModSecurity module on Nginx, load the OWASP Core Rule Set, run in detection mode first, then block SQL injection and XSS with tuned rules.

AdvancedLinux server6 min read

A ModSecurity WAF on Nginx inspects every HTTP request before it reaches your application and blocks common exploits such as SQL injection, cross-site scripting and local file inclusion. This guide installs the ModSecurity v3 connector, loads the OWASP Core Rule Set (CRS), runs it in detection-only mode to collect false positives, then switches it to blocking.

Warning: A misconfigured WAF returns HTTP 500 for every request. Keep a browser console open in your VPS provider's control panel on your Linux VPS while you reload Nginx, so you can undo the change if the site stops responding.

Before you start

You need:

  • A Linux VPS running Ubuntu 22.04/24.04, Debian 12, or AlmaLinux/Rocky 8–9, with Nginx already serving at least one site.
  • Root or sudo access, and a user such as deploy that you normally work as.
  • A working site you can test against, for example https://example.com.
  • About 15 minutes, plus a quiet window for the first reload.

Check your Nginx build and version first, because the connector module must match it:

Bash
nginx -v
nginx -V 2>&1 | tr ' ' '\n' | grep -E '^--(with|without)'

If you have not installed Nginx yet, follow Install Nginx and host your first website before continuing. Serve the site over TLS as well — see Secure Nginx with a free Let's Encrypt SSL certificate — so credentials are never sent in clear text.

Install ModSecurity and the Nginx connector

On Debian and Ubuntu, both the engine and the connector are packaged. Install them:

Bash
sudo apt update
sudo apt install -y libmodsecurity3 libnginx-mod-http-modsecurity git

Confirm the dynamic module was registered:

Bash
ls -l /usr/lib/nginx/modules/ | grep modsecurity
ls /etc/nginx/modules-enabled/

You should see ngx_http_modsecurity_module.so and a file such as 50-mod-http-modsecurity.conf that loads it. Reload Nginx and check for errors:

Bash
sudo nginx -t
sudo systemctl reload nginx

If nginx -t reports unknown directive "modsecurity", the module is not loaded — the package name differs on your release or your Nginx came from a third-party repository. Check your distribution's package list, or build the connector from source against the exact Nginx version shown by nginx -v.

Build the connector from source (only if needed)

Bash
sudo apt install -y build-essential libpcre2-dev libssl-dev zlib1g-dev libmodsecurity-dev
cd /usr/local/src
sudo git clone --depth 1 https://github.com/owasp-modsecurity/ModSecurity-nginx.git
nginx -V 2>&1 | grep -o 'configure arguments: .*'

Download the matching Nginx source tarball, run ./configure with the arguments printed above plus --with-compat, then:

Bash
sudo make modules
sudo cp objs/ngx_http_modsecurity_module.so /usr/lib/nginx/modules/

Load it with a one-line file in /etc/nginx/modules-enabled/ containing load_module modules/ngx_http_modsecurity_module.so;.

Create the rule configuration

Keep everything in one directory so it is easy to audit and back up. Fetch the recommended base configuration and the Unicode mapping file that ships with the engine:

Bash
sudo mkdir -p /etc/nginx/modsec
cd /etc/nginx/modsec
sudo curl -fsSLO https://raw.githubusercontent.com/owasp-modsecurity/ModSecurity/v3/master/modsecurity.conf-recommended
sudo curl -fsSLO https://raw.githubusercontent.com/owasp-modsecurity/ModSecurity/v3/master/unicode.mapping
sudo mv modsecurity.conf-recommended modsecurity.conf

Point the mapping file at its absolute path and start in detection mode:

Bash
sudo sed -i 's|^SecUnicodeMapFile.*|SecUnicodeMapFile /etc/nginx/modsec/unicode.mapping 20127|' /etc/nginx/modsec/modsecurity.conf
sudo sed -i 's|^SecRuleEngine.*|SecRuleEngine DetectionOnly|' /etc/nginx/modsec/modsecurity.conf
grep -E '^SecRuleEngine|^SecUnicodeMapFile|^SecRequestBodyAccess' /etc/nginx/modsec/modsecurity.conf

Now add the OWASP Core Rule Set:

Bash
cd /etc/nginx/modsec
sudo git clone https://github.com/coreruleset/coreruleset.git
cd coreruleset
sudo cp crs-setup.conf.example crs-setup.conf

CRS releases occasionally change their minimum engine version and the rule directory layout. Check the project's release notes when you update, and pin a tag rather than tracking the default branch on a production server.

Create the master include file:

Bash
sudo tee /etc/nginx/modsec/main.conf > /dev/null <<'EOF'
Include /etc/nginx/modsec/modsecurity.conf
Include /etc/nginx/modsec/coreruleset/crs-setup.conf
Include /etc/nginx/modsec/coreruleset/rules/*.conf
EOF

Keep /etc/nginx/modsec readable by root only, since crs-setup.conf can hold exclusion logic:

Bash
sudo chown -R root:root /etc/nginx/modsec
sudo chmod -R 750 /etc/nginx/modsec

Enable ModSecurity in Nginx

Turn the WAF on globally by editing the http block in /etc/nginx/nginx.conf:

Nginx
http {
    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsec/main.conf;

    # existing includes stay here
}

Test and reload:

Bash
sudo nginx -t
sudo systemctl reload nginx

Tip: You can scope the WAF to a single server block instead of http, which is useful when you want to protect one application before rolling out to everything.

Now send a harmless attack string and read the audit log:

Bash
curl -s -o /dev/null -w '%{http_code}\n' 'https://example.com/?q=<script>alert(1)</script>'
sudo tail -n 40 /var/log/modsec_audit.log

In DetectionOnly mode the request is still served, but the log contains a ModSecurity: Warning entry naming the matched rule ID.

Switch to blocking and tune false positives

Watch the audit log for a day or two of real traffic. Then enable enforcement:

Bash
sudo sed -i 's|^SecRuleEngine.*|SecRuleEngine On|' /etc/nginx/modsec/modsecurity.conf
sudo nginx -t && sudo systemctl reload nginx

Re-run the same test — you should now get 403:

Bash
curl -s -o /dev/null -w '%{http_code}\n' 'https://example.com/?q=<script>alert(1)</script>'

Expect some legitimate requests to be blocked, especially in admin panels and rich-text editors that post HTML. Disable specific rules rather than the whole engine. Create a per-site exclusion file:

Bash
sudo tee /etc/nginx/modsec/overrides.conf > /dev/null <<'EOF'
# Disable a noisy rule by ID globally
SecRuleRemoveById 942100

# Disable rules for one virtual host only
SecRule REQUEST_HEADERS:Host "@rx ^example\.com$" "id:9001,phase:1,pass,nolog,ctl:ruleRemoveById=942100"
EOF

Add Include /etc/nginx/modsec/overrides.conf; to the end of /etc/nginx/modsec/main.conf, then reload. Never set SecRuleEngine Off on a site you meant to protect — it hides the problem instead of solving it.

Tune the body limits if uploads fail with 403:

Nginx
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
SecRequestBodyInMemoryLimit 131072

Warning: Raising the body limit increases memory use per concurrent upload. Set it to the largest file your application genuinely accepts, not to an arbitrary large value.

Check it works

  1. Confirm the module loads with no warnings: sudo nginx -t.
  2. Attack strings return 403: try <script>alert(1)</script>, ' OR 1=1--, and ../../etc/passwd in a query string.
  3. Normal pages return their usual status code: curl -sI https://example.com/.
  4. The audit log records matches: sudo tail -f /var/log/modsec_audit.log.
  5. Your application's forms, logins and file uploads still work end to end.

Troubleshooting

  • Every request returns 500 → a missing include or unreadable unicode.mapping. Run sudo nginx -t and check the Nginx error log; the exact file and line are printed there.
  • nginx -t fails with unknown directive "modsecurity" → the connector module is not loaded. Confirm the .so file exists and that a load_module line for it is present, then reload.
  • Legitimate admin pages get 403 → CRS rules flag posted HTML. Identify the rule ID in the audit log and remove it with SecRuleRemoveById, scoped to that virtual host where possible.
  • File uploads fail but small posts succeed → SecRequestBodyLimit is too low for your maximum upload size. Raise it and reload.
  • Nginx CPU usage rises sharply → too many rules or expensive body inspection. Keep the CRS updated, disable unused rule files, and reduce SecRequestBodyInMemoryLimit only if memory is the constraint.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.