Using Fail2Ban to protect WordPress

I posted some previous ideas on this that were okay, but they turned out to be less-than-ideal solutions. They work, but one of the blogs I watch over is a bit busy, and having Fail2Ban watching the Apache access.log was putting excessive load on the CPU.

So here’s a nicer approach, but it needs a bit more fiddling:

As /etc/fail2ban/filters.d/apache-phpmyadmin.conf

failregex =  [[]client <HOST>[]] (File does not exist|script ').*(phpMyAdmin|phpmyadmin|dbadmin|mysql|myadmin|w00t|muieblackcat|mysqladmin).*
ignoreregex =

As /etc/fail2ban/filters.d/apache-wp-login.conf

failregex =  [[]client <HOST>[]] WP login failed.*
ignoreregex =

As /etc/fail2ban/filters.d/apache-wp-timthumb.conf

failregex =  [[]client <HOST>[]] (File does not exist|script ').*(timthumb\.php).*
ignoreregex =

The relevant sections of /etc/fail2ban/jail.local should be something like these. This allows a few failed logins, but only one attempt to hit a phpMyAdmin directory or the TimThumb exploit. But maxretry and findtime can be whatever you want.

enabled = true
port    = http,https
filter  = apache-wp-login
logpath = /var/log/apache*/*error.log
maxretry = 3
findtime = 120

action = %(action_mwl)s
enabled = true
port    = http,https
filter  = apache-phpmyadmin
logpath = /var/log/apache*/*error.log
maxretry = 1
findtime = 60

action = %(action_mwl)s
enabled = true
port    = http,https
filter  = apache-wp-timthumb
logpath = /var/log/apache*/*error.log
maxretry = 1
findtime = 60

So now all of the jails are watching error.log, which hopefully gets significantly less traffic than access.log. But we need to make sure WordPress logs the information we need.

If pretty permalinks is turned on, WP handles 404s and does not output to a log. Add a 404.php to the active theme that looks like this, or if there is already one, just add the error_log line:

<?php get_header(); ?>
<div id="post-0" class="post error404 not-found">
	<h1><?php _e('Page Not Found', "magazine-basic"); ?></h1>
	<div class="storycontent">
		<p><?php _e('The page you requested could not be found.', "magazine-basic"); ?></p>
	</div><!-- .storycontent -->
</div><!-- #post-0 -->
error_log("File does not exist: " . rtrim($_SERVER['DOCUMENT_ROOT'], "/") . $_SERVER['REQUEST_URI'], 0);

And add this to the functions.php in your theme too, to handle the login attempts:

	// Log login errors to Apache error log
	add_action('wp_login_failed', 'log_wp_login_fail'); // hook failed login

	function log_wp_login_fail($username) {
		error_log("WP login failed for username: $username");

Restart Fail2Ban server to pickup the changes, start some logging, and do some testing.

sudo service fail2ban restart
tail -n 100 -f /var/log/fail2ban.log
Leave a comment ?


  1. Awesome tutorial but when I try to restart fail2ban I get an error

    # sudo service fail2ban restart
    Stopping fail2ban: [ OK ]
    Starting fail2ban: [FAILED]

    I didn’t touch the original config file. Instead I create a jail.local file like you explained in the tutorial. I also tried to delete this file and add the sections to the existing jail.conf but fail2ban won’t restart with any of these directives.

  2. I isolated the problem to the missing “action” directive in [apache-wp-login].

    For some reason, including this…

    action = %(action_mwl)s

    makes fail2ban not start.

    • It might depend on what actions you have available. action_mwl with mta=sendmail expects a script at actions.d/sendmail-whois-lines. If you change the mta and don’t create a new script in actions.d this might cause your problem. Also, care needs to be taken with file and directory permissions with F2B.

  3. Thanks for this. I have been using a couple of plugins, one to stop the brute forcing logins (something that really should be standard) and one to block access to a list of ip addresses, but the more sites you have on the server, the more time this takes to keep the lists up to date. Plus it doesn’t deal with the tinthumb etc attempts.

    • Pretty much the same problem I was having with my original implementation. I’m glad this helped 🙂

  4. Frederique Rijsdijk


    Nice one this.. although it would be nicer if we don’t have to modify files from wp itself, but add a file somewhere. Is that possible? (so no overwrites with updates).

    • If you want to block failed login attempts, you need something somewhere in a log for F2B to hook into. Both changes here are within the active theme so it’s not really changing any WP code and is upgrade safe.

  5. Also remember to change /etc/php.ini because if you’re logging to the /var/log/php.log file it won’t log the ip address.

    change that line to
    error_log =

    Also note that if you get errors you might need to reference this site to use ip route command.

    • The default behaviour is not to log to /var/log/php.log, but you’re quite right that this should be amended or commented out if it has been enabled.

  6. Just want to say thanks for posting this. I already use WordFence internally to WordPress, and Fail2Ban for server attacks.

    I think it might be belt and braces to use F2B for WordPress in my case, but I like the idea of blocking at IP level.

    BTW, how long are you setting ‘bantime’ to as by default it is only 10 minutes.

    • Nothing wrong with belt and braces protection 🙂

      Bantime can be set to different levels within the filter parameters in jail.local. So I set bantime at 3600 globally, but for filters like the TimThumb I set bantime for 86400,

  7. Hey very nice post. I had lots of attempts at brute forced login so I made an mu plugin out of your code above.

  8. Evitar fuerza bruta en WordPress | Omar Benbouazza - pingback on April 15, 2013 at 9:31 am
  9. This gist is way to detect uncommon logins/probes.

  10. test - pingback on April 9, 2014 at 10:49 am

Leave a Comment

NOTE - You can use these HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Trackbacks and Pingbacks: