Setting Up A WAF

2020-06-01

I regularly check my server logs for traffic and seeing what attacks are happening. My site isn't high value so they're nearly all low effort scans like checking for a Wordpress login page. As I see possible issues, I'll add a little more security. A few days ago I saw someone had found my admin login page and tried to login 30 times. I'm not really concerned someone will succeed, but it seemed like a good time to make it impossible. A reasonable limit on attempts will make any brute force attack too slow to be useful.

My requirements were to do this as far up the stack as reasonable and use FOSS. Some of the choices I looked at were django-ratelimit, mod_qos and mod_security. Django-ratelimit would be easy to setup for this, but implementing this in python so far down the stack seems like a good way to create a bottleneck. Mod_qos is an apache module that had the features I wanted and a simple rule syntax. It doesn't guarantee support for event mpm which is a deal breaker, any other worker type will have far worse performance. I ended up using mod_security which has far more features than I'll need. It's a FOSS web application firewall(WAF) implemented as an apache module with a language similar to nftables.

Inital set up of mod_security on Debian is easy, just apt install and change the recommended conf to .conf in /etc/modsecurity. This will start logging requests that get caught by the OWASP core rule set. The core rule set has a large number of rules for many common apps and situations. The rule set has restriction levels called paranoia levels from 1 to 4. The default is 1 and thats what I've left it at for now. Congratulations, your server has another layer of protection from low effort attacks.

Now on to the high value work, setting up rules for my django admin login. Setting up my own rules was not easy. I couldn't find anything resembling a quick start guide for Debian. It was unclear to me where my .conf rule file should go and be named. I started out with it in /etc/modsecurity/crs with the other rules files. It didn't work until I put it in /etc/modsecurity. I finally found out where the rules were located by investigating the apache mod directly

vi /etc/apache2/mods-enabled/security2.conf

Rules load from /etc/modsecurity/ and /usr/share/modsecurity-crs/.

The other hurdle is figuring out the syntax as there is not a guide. I ended up piecing it together using the 2.X Reference Guide and reading rules that were similar to what I wanted to do. It became clear each rule should start with SecRule and looking that up in the reference guide will give you the syntax for that command. I also found this guide for securing a wordpress login which is nearly what I wanted. I didn't want to do it by user since I could get locked out, use Locationmatch since that means having to put it on every vhost(rate limiting login pages is good general policy) and resetting the counter for successful logins seemed unnecessary. Here's what I came up with:

# Setup collection
SecAction phase:1,id:1,nolog,pass,initcol:ip=%{REMOTE_ADDR}
# Block and track # of requests and log
SecRule ip:login_brute_force_block "@eq 1" "id:2, phase:1, deny, status:401, msg:'DOS attack from %{tx.real_ip}'"
# Add to count each time login page accessed
SecRule REQUEST_FILENAME "@beginsWith /admin/login/" "id:4, phase:1, t:none,setvar:ip.login_brute_force_counter=+1"
# Set block after 10 attempts, reset counter after 60 seconds
SecRule ip:login_brute_force_counter "@ge 10" "id:4, phase:5, t:none, setvar:'ip.login_brute_force_block',\
expirevar:'ip.login_brute_force_block=60'"

This means only 10 requests per minute will be allowed from an IP. Since I'm the only one using the interface there is no chance of being locked out and brute force attacks will take too long to be effective.