Install ModSecurity with Nginx on Ubuntu 20.04
If you’ve ever watched a malicious request slip past a default Nginx config, you know why a WAF is worth the extra coffee‑break. This guide walks through adding ModSecurity to an existing Nginx install on Ubuntu 20.04, shows where things can go sideways, and leaves you with a lean rule set that actually blocks something.
Prepare the system
sudo apt update
sudo apt install -y nginx git build-essential libpcre3-dev libssl-dev zlib1g-dev uuid-dev
Why? Nginx is already on the box, but ModSecurity needs development headers for PCRE (regular expressions) and OpenSSL. Skipping these libraries will cause the compile step to explode.
Grab ModSecurity source
cd /usr/local/src
sudo git clone --depth 1 -b v3/master https://github.com/SpiderLabs/ModSecurity.git
I once tried to download a zip from an unofficial mirror and ended up with a broken Makefile. The official repo is the safest bet.
Build ModSecurity
cd ModSecurity
sudo git submodule init
sudo git submodule update
sudo ./build.sh
sudo ./configure
sudo make
sudo make install
The ./build.sh script pulls in the libmodsecurity‑common files; without it the later make will complain about missing symbols.
Compile Nginx with the ModSecurity module
First, fetch the exact source version that matches your installed Nginx:
nginx -v # note the version number, e.g. 1.18.0
cd /usr/local/src
sudo wget http://nginx.org/download/nginx-1.18.0.tar.gz
sudo tar xf nginx-1.18.0.tar.gz
Now pull the connector:
sudo git clone https://github.com/SpiderLabs/ModSecurity-nginx.git
Configure and compile:
cd nginx-1.18.0
sudo ./configure \
--with-compat \
--add-dynamic-module=../ModSecurity-nginx
sudo make modules
sudo cp objs/ngx_http_modsecurity_module.so /etc/nginx/modules/
Why a dynamic module? It lets you enable or disable ModSecurity without rebuilding Nginx later. If you try to embed it statically and then upgrade Nginx, you’ll be stuck with version mismatches.
Load the module in Nginx
Edit /etc/nginx/nginx.conf (or a file under conf.d/) and add near the top:
load_module modules/ngx_http_modsecurity_module.so;
If you forget this line, Nginx will start but ignore all ModSecurity directives—waste of time.
Create a basic ModSecurity config
Copy the example file and trim it down:
sudo mkdir -p /etc/nginx/modsec
sudo cp /usr/local/modsecurity/etc/modsecurity.conf-recommended /etc/nginx/modsec/modsecurity.conf
sudo sed -i 's/SecRuleEngine DetectionOnly/SecRuleEngine On/' /etc/nginx/modsec/modsecurity.conf
The default config ships in “DetectionOnly” mode, which logs but never blocks. Turning it on is the whole point of a WAF.
Hook ModSecurity into a server block
Inside your site’s server {} block add:
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/main.conf;
Create main.conf with a few sensible rules; you don’t need the massive OWASP CRS unless you love false positives:
sudo tee /etc/nginx/modsec/main.conf > /dev/null <<'EOF'
Include /usr/local/modsecurity/etc/base_rules/*.conf
# Simple SQLi check
SecRule ARGS "@detectSQLi" "id:1001,phase:2,deny,status:403,msg:'SQL Injection Detected'"
EOF
I’ve seen the CRS grind a busy site to a crawl because it throws away legitimate traffic. Start small, then add rules as you discover actual attacks.
Test and reload
sudo nginx -t # syntax check
sudo systemctl restart nginx
A quick curl test with a known attack string should return 403:
curl "http://yourdomain.com/?id=1' OR '1'='1"
If you get 200, double‑check that modsecurity on; is inside the correct server block and that the rules file path matches.
Keep it tidy
- Log rotation: ModSecurity writes to /var/log/modsec_audit.log. Add a logrotate entry so the file doesn’t fill your disk.
- Rule updates: When you finally decide to pull in more rules, do it incrementally and watch the error log for “[error] ModSecurity: Access denied with code 403”.
That’s it. You now have a functional WAF sitting in front of Nginx, and you avoided the common pitfall of loading a bloated rule set that chokes your site.