WordPress Site Under Attack? Fail2Ban to the Rescue!

If you’ve ever run a WordPress site, you probably know how annoying it is when someone decides to spam your server with traffic. Services like Cloudflare help a lot, and the free plan enables you to add rate limiting rules but still, not every project can sit behind Cloudflare. So for websites that are not on Cloudflare, you need something on your own server, one simple tool that works well for this is Fail2Ban.

This is how it will work: first, Nginx will monitor for spikes in requests per second. We’ll exclude assets like CSS, JS, and media gallery. Then, any request that exceeds the limit will be logged as a 429 in the nginx access log. Fail2ban will then scan these logs for specific patterns and ban any IPs that abuse the server.

Configure Nginx

First, we need to create a new rate limiting rule, this can be done by adding the below code into the server block:

geo $limit_bypass {
    default 0;

    # any IP that you wish to exclude
    # i normally put "good" crawlers here like google or ahrefs
}

map $request_uri $limited {
    default         $binary_remote_addr;
    ~^/wp-content/  "";
    ~^/wp-includes/ "";
    ~^/wp-admin/ "";
    ~^/wp-json/ "";
}

map $limit_bypass:$limited $final_limit_key {
    ~^1:       "";
    default    $limited;
}

limit_req_zone $final_limit_key zone=one:10m rate=3r/s

The code above will exclude wp-admin, wp-json, wp-content, and wp-includes from rate limiting, since these endpoints include assets, media gallery and AJAX requests. Also, blocking wp-json or wp-admin could cause problems with WP-Admin or some plugins.

Then, somewhere in your server block you will have to apply the rule we’ve created:

location / {
  index index.php index.html;
  try_files $uri $uri/ /index.php?$args;

  #apply the rate limiting rules:
  limit_req zone=one burst=3 nodelay;
  limit_req_status 429;
}

Set up a jail in Fail2Ban

First, install Fail2ban if you haven’t already. In Ubuntu/Debian you can do this via apt:

sudo apt install fail2ban

Like I said before, Fail2Ban can be set up to work by watching logs and banning IPs that match certain patterns. We will start by creating our custom rules, copy the default config:

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Now open the .local file and define your rules like this:

[nginx-req-limit]
ignoreip = <any IP you want to exclude>
enabled = true
filter = nginx-custom-filter
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 3600
bantime  = 7200
port     = http,https

These numbers depend on your traffic. The idea is simple: if an IP hits your site too many times than what we’ve defined in nginx, it will get banned for two hours (7200 seconds). You can change that to be stricter or more forgiving.

After saving the file, create the filter in /etc/fail2ban/filter.d/nginx-custom-filter.conf

[Definition]
failregex = ^<HOST> - 429 -
ignoreregex =

So as you see, in order for this to work, our logs have to be in this specific format. In the nexts step we are going to adjust the log format in Nginx.

After saving the file, reload Fail2Ban:

sudo service fail2ban restart

Check that the jail is active:

sudo fail2ban-client status

Adjust log format in Nginx

The default format in Nginx is not what we want, so we will have to create a custom log format.

To use a custom log format, add the format name at the end of the access_log directive (can be anything), like this:

access_log /var/log/nginx/access.log custom

Then, define the log format in your nginx.conf (needs to be within the http block):

log_format custom '$remote_addr - $status - $remote_user [$time_local] '
                  '"$request" $body_bytes_sent '
                  '"$http_referer" "$http_user_agent" "$gzip_ratio"';

Reload Nginx:

sudo nginx -s reload

Now Fail2Ban will watch the access log and react to any “429” response codes in the access log.

Testing the Setup

You can use a simple script to spam your server with repeated requests:

for i in {1..200}; do curl -s https://example.com >/dev/null; done

After a moment, check Fail2Ban:

sudo fail2ban-client status nginx-http

You should see your IP on the ban list. Try loading your site again and it should fail (it will most likely spin forever).

Implementation in Cloudflare

You can set up a similar system in Cloudflare by going to Security → Security Rules → Rate Limiting Rules. Create a new rule and enter this expression:

(not starts_with(http.request.uri.path, "/wp-content/") and not starts_with(http.request.uri.path, "/wp-includes/") and not starts_with(http.request.uri.path, "/wp-json/") and not starts_with(http.request.uri.path, "/wp-admin/"))

Then set the rate limit to 8–10 requests per 10 seconds (the period can only be set to 10 seconds).

For the action, choose “Block” - please note that the duration can only be set to 10 seconds.

So Cloudflare is more limiting in its options, but rate limiting is available even on the free plan, making it a good alternative if you don’t want to configure it manually on your server.