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
sudoaccess, and a user such asdeploythat 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:
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:
sudo apt update
sudo apt install -y libmodsecurity3 libnginx-mod-http-modsecurity gitConfirm the dynamic module was registered:
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:
sudo nginx -t
sudo systemctl reload nginxIf 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)
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:
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:
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.confPoint the mapping file at its absolute path and start in detection mode:
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.confNow add the OWASP Core Rule Set:
cd /etc/nginx/modsec
sudo git clone https://github.com/coreruleset/coreruleset.git
cd coreruleset
sudo cp crs-setup.conf.example crs-setup.confCRS 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:
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
EOFKeep /etc/nginx/modsec readable by root only, since crs-setup.conf can hold exclusion logic:
sudo chown -R root:root /etc/nginx/modsec
sudo chmod -R 750 /etc/nginx/modsecEnable ModSecurity in Nginx
Turn the WAF on globally by editing the http block in /etc/nginx/nginx.conf:
http {
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/main.conf;
# existing includes stay here
}Test and reload:
sudo nginx -t
sudo systemctl reload nginxTip: 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:
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.logIn 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:
sudo sed -i 's|^SecRuleEngine.*|SecRuleEngine On|' /etc/nginx/modsec/modsecurity.conf
sudo nginx -t && sudo systemctl reload nginxRe-run the same test — you should now get 403:
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:
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"
EOFAdd 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:
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
SecRequestBodyInMemoryLimit 131072Warning: 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
- Confirm the module loads with no warnings:
sudo nginx -t. - Attack strings return
403: try<script>alert(1)</script>,' OR 1=1--, and../../etc/passwdin a query string. - Normal pages return their usual status code:
curl -sI https://example.com/. - The audit log records matches:
sudo tail -f /var/log/modsec_audit.log. - 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. Runsudo nginx -tand check the Nginx error log; the exact file and line are printed there. nginx -tfails withunknown directive "modsecurity"→ the connector module is not loaded. Confirm the.sofile exists and that aload_moduleline 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 →
SecRequestBodyLimitis 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
SecRequestBodyInMemoryLimitonly if memory is the constraint.
Next steps
- Pair the WAF with rate limiting in Nginx using limit_req to slow brute-force and scraping.
- Add Fail2ban jails for Nginx so repeat offenders are blocked at the firewall after the WAF flags them.
- Put the WAF in front of applications correctly by reading Set up Nginx as a reverse proxy.
- Size.
