Dev/Sec/Ops with a splattering of humour

Prevent Brute Force attacks with Fail2Ban & WordPress

Really old post – This post is from 3 years, 4 months and has not been updated, some if not all of the information maybe out of date. Proceed with caution.

You can also watch this video on YouTube

This video is a snippet of the content for a one day WordPress security Workshop I’m running in Leeds, UK on the 27th of November. Tickets are still available, so for details pop over to Courses & Workshops.

I previously wrote about my Must Use Plugins which detailed the plugins found in the must-use folder of the site I was working on. Within that post I mentioned a plugin called fail2ban.php. This simple plugin just sets a 401 status header, which is returned when the a visitor fails to login after a post request.

The code is 3 lines, here is the whole plugin:

Plugin Name: Fail2Ban filter
Version: 1.0
Description: Sets a 401 Status Code which shows in access logs for use with fail2ban
Author: Tim Nash
Author URI: https://timnash.co.uk
Code Modified from  Konstantin Kovshenin original - http://kovshenin.com/2014/fail2ban-wordpress-nginx/
function fail2ban_login_failed_401() {
    status_header( 401 );
add_action( 'wp_login_failed', 'fail2ban_login_failed_401' );

The 401 header is recorded in my access logs, along with other visitor details – this is a standard access log and standard behaviour on most servers. Because access logs are plain text files, they can easily be monitored by other applications, and one such application is fail2ban. A few people have asked for some more details on how to set up Fail2ban to work with the plugin, so I have created a short video tutorial above along with more detailed config explanation below.

Fail2Ban & WordPress config

Fail2Ban is a utility service found on most Linux systems (if you are on a shared host, you may need to ask your hosting provider if it’s installed, and if additional filters can be applied). It monitors log files and then acts as a simple interface to a series of actions including interacting with IPTables, the default unix firewall. If it’s not installed already, then you can install it on Ubuntu with:

sudo apt-get install fail2ban

Once installed configuration is simple. Fail2Ban monitors log files, and it identifies fails (which is what it looks for) using a Regular Expression. These regular expressions are stored in its filters section /etc/fail2ban/filters.d/. By default it comes with numerous common application filters, from SSH to Apache/Nginx config.

A typical filter config looks something like:

# fail2ban filter configuration for WordPress Logins


failregex = .*POST.*(wp-login\.php|xmlrpc\.php).* 401

ignoreregex =

This is basically two parts, the failregex and ignoreregex. The failregex is what Fail2ban is looking for and considers a “fail” – a failed login in our case. The regex is the regex used to parse your access log. In the above, it’s checking for 401 status headers on post responses on wp-login.php and xmlrpc.php within the logins. The check is not specific to path, so in theory if you have wp-login.php in a sub directory somewhere and someone does a post request which returns a 401 it will be also matched. This is unlikely but worth remembering. In such a scenario we could apply a ignoreregex to ignore calls from that specific file.

If you don’t put in

ignoreregex =

Fail2ban will prompt with a warning at restart, so even if you don’t have anything to ignore, include the line and just leave the regex blank.

With a filter now in place, we now want to set up the jail configuration which is located at /etc/fail2ban.jail.conf. This is a large file and mostly commented out, I add my unique config options at the bottom, so page down to the bottom and add:

# Jail for unauthorised WordPress login attempts
# If you are using APACHE or multiple access logs change as appropriate

enabled = true
port = http,https
filter = wordpress-auth
logpath = /var/log/nginx/access.log
maxretry = 3
bantime = 3600
  • Enabled – we need to enable it for the magic to happen
  • port – declare what ports it’s monitoring, whether giving a number or the port service. In this case I have specified HTTP and HTTPS, though we could have put 80,443
  • filter – the filter that we made previously – in this case I made one called wordpress-auth.
  • logpath – The location of the logs we are monitoring. We are monitoring the default Nginx logs, but this could be a specific host access file, or apache2 logs for example.
  • maxretry – the number of times the IP has to show in the regex results within a given period for jailing to occur. By default this is 3 results within 600 seconds, you can change the 600 by specifying a findtime.
  • bantime – the length of time the IP is banned for in seconds

With the plugin enabled, and filters set up and configured, it’s just a case of restarting fail2ban and you are good to go. Fail2ban will monitor the access.log looking for failed logins, banning as needed.

Writing to a syslog

There is an existing WordPress plugin in the WordPress.org repo called WP fail2ban which works slightly differently then the workflow in our video. Rather than using a status header and relying on it being picked up in an access log, it writes to the syslog directly using LOG_AUTH. This means if for some reason your access log is not accessible or not being written to then entries are being logged (assuming PHP can write to the syslog). However it also means lots of spurious entries are being made to your syslogs. If you’re suffering from a wide spread brute force attack this means your syslogs are going to get filled up rapidly, making other errors hard to find. There are solutions for that though, and most people don’t comb through syslogs by hand unless they like hitting their heads against brick walls.

There is however an advantage to writing to a separate log or even the syslog, and that’s when you are running fail2ban across multiple sites and boxes. By centralising the location of notices of failed logins to a single location, you can then monitor that one location, otherwise you will need to set up individual jail configs for every site.

Still, if you’re running a single host or centralised access logs then using access logs is quick and easy to setup with no additional overheads. It also doesn’t rely on PHP having to do any log manipulation. If you run multiple sites on multiple servers then you may wish to consider centralising the logs, in such cases you may wish to use a tool like monolog to write a dedicated centralised log file.

Why a 401 not a 403

My little example plugin is based on Konstantin Kovshenin’s plugin with one exception, it throws a 401 instead of a 403. Why? Well it’s a matter of semantics.

  • a 403 – is a Forbidden status, the user has attempted to retrieve a resource that they do not have access to.
  • a 401 – is a Unauthorised, the user has attempted to retrieve a resource however they have not supplied correct credentials to access the resource.

Pretty similar, but they are different and could potentially have different use case within wp-login. If we expand our little plugin in the future, to check IPs and block an account if the account is accessed from multiple IPs in a short space of time, we may wish to indicate a different response.

In such a scenario:

  • We issue the 401, as before when a credential is incorrect
  • We issue the 403 if the credential is correct but we are not allowing access to the resource due to too many IPs.

Now there is a good argument to suggest we shouldn’t issue separate codes in such a scenario, as we are in effect letting the attacker know they have correct credentials, however it does demonstrate the potential differences.

Going beyond banning

Fail2Ban is a pretty flexible system, as along with its filters.d folder, it also has an actions.d which provide a modular way to do additional actions, ranging from emailing you on a successful jailing to whatever you want, just like filters you can create your own actions.

For example I find that email notifications become too much on heavy trafficked sites, also recently I’ve been switching off sendmail by default on servers. However I still like to know when an IP has been banned, so we could create a action that looks something like this to communicate with Slack via an incoming webhook for example:

# Fail2Ban configuration for Slack Notification
# Author Tim Nash

# Option: actionban
# Notes: Command executed when banning IP.
# Values: CMD

actionban = curl -X POST --data-urlencode 'payload={"channel": "#metaworld", "username": "webhookbot", "text": "Fail2Ban Reports IP  has been banned by  filter", "icon_emoji": ":ghost:"}' https://example.slack.com/services/hooks/incoming-webhook?token=yourtoken


# Default name of the chain

Saving it called slack.conf, you can get your token directly from Slack webhook section.
Again it’s very similar to our filter file from earlier, but this time we’re firing of a CURL request, that could be replaced with a shell script or just about anything. We can then re-edit our jail.conf to add our action.

# Jail for unauthorised WordPress login attempts
# If you are using APACHE or multiple access logs change as appropriate

enabled = true
port = http,https
filter = wordpress-auth
action = slack[name=wordpress]
logpath = /var/log/nginx/access.log
maxretry = 3
bantime = 3600

That’s it, now when an IP is banned a notification is sent to Slack to our metaworld channel, and I can switch off the email notification within the jail.conf.

Banning repeat offenders

By default Fail2Ban just temporarily bans IPs, however quite quickly you’ll find bots that simply learn to wait 1 day and then try again. In such circumstances you want to be able to block repeat offenders permanently. I recently started using Phil Hagens Repeat Offender Script which, after a given period blocks an IP completely.

So there we have it – a simple, effective solution for stopping brute force attacks, without using WordPress plugins (well except for those 3 lines) but relying on your servers tried and tested existing solution. While brute force attacks are becoming an ever increasing problem for WordPress sites, it’s important to remember they didn’t start with WordPress and we already have highly effective tools for dealing with such attacks.

WordPress Security Training Course

I’m running a one day WordPress security Workshop in Leeds, UK on the 27th of November. Tickets are still available – for details see Courses & Workshops.

Update 13/11/2014 – Just clarified the Slack webhook usage and where you can get the token from.

Have your say?

Sorry Comments are now closed, feel free to tweet me!