Securing WordPress with Fail2Ban

On Christmas some bored Latvian (according to whois) ‘script kid’ decided to try-and-break into one of my WordPress installations with brute force login attempts. Not a problem as such, because I had already taken precautions against such attempts, but his idiotic fooling around caused the account to become locked, preventing me from logging in!!! I had to take the server offline for about an hour or so, before I could log in myself.

I didn’t want to allow that to happen again, especially because that stupid kid continued his attacks. I started searching for ways to block the login attempts by-IP-address, and found out that it was possible to configure the (already installed) program fail2ban to protect WP logins, too. Here’s a howto about doing it the Gentoo Linux way:

The prerequisites: already working WP installation, fail2ban and rsyslog. Or in Gentoo speak:

emerge -av fail2ban rsyslog
rc-update add fail2ban default
rc-update add rsyslog default

If you have some other system logger already installed, please stop and remove it before starting rsyslog. In case you want to continue using that other syslogger, you have to figure out the exact syntax for its configuration file for yourself. I suppose because you are already running WP, you have a ‘mysql’ use-flag in your make.conf. If not, add it before typing in the above.

First, you need to add the following to the top (just after the <?php tag) of the functions.php file within the WP theme you are using.

const SYSLOG_FACILITY = LOG_LOCAL1;

add_action('wp_login_failed', 'log_failed_attempt');

function log_failed_attempt( $username ) {
    openlog( 'wordpress('.$_SERVER['HTTP_HOST'].')', LOG_NDELAY|LOG_PID, SYSLOG_FACILITY);
    syslog( LOG_NOTICE, "Wordpress authentication failure for $username from {$_SERVER['REMOTE_ADDR']}" );
}

This constructs a syslog facility called LOCAL1. I have three WordPress installations on virtual hosts on my server, so I repeated the above on all three, substituting LOCAL1 with LOCAL2 and LOCAL3. The code above hooks into the WP login failed action, and tells it to log the failed login attempt with the IP address, username used and website it is coming from.

Next, edit /etc/rsyslog.conf

# Save WP invalid login attempts to log for Fail2Ban
local1.*	/var/log/wp_f2b.log
local2.*	/var/log/wp_f2b.log
local3.*	/var/log/wp_f2b.log

This creates the rule and the file to accept the log attempts.

(UPDATE Feb 19th,2015: with rsyslog-8.x.x and up, it is not advisable to edit the rsyslog.conf file itself; instead, create a file with a name 10-wp.conf in /etc/rsyslog.d/, and copy the code above into it.)

Restart rsyslog

rc-service rsyslog restart

At this point you should be able to test it to verify the attempts (if any) are being logged:

tail -f /var/log/wp_f2b.log

Next step is to create the filter for fail2ban; first edit/create /etc/fail2ban/jail.local

[wordpress]
 
enabled  = true
filter   = wordpress
action   = iptables-multiport[name=WordPress, port="http,https"]
           sendmail-whois[name=WordPress, dest=email@domain.tld, sender=fail2ban@domain.tld]
logpath  = /var/log/wp_f2b.log
ignoreip = 127.0.0.0/8 your_ip_here
maxretry = 5
findtime = 600
bantime  = 600

Substitute your email address to the default ones above. You can also alter the maxretry, findtime and bantime (in seconds) values. Be sure to whitelist your own IP-address in the ignoreip section, otherwise you might ban yourself by accident.

If you have the BulletProof Security or a similar security plugin installed, I suggest that you set the maxretry value at least one smaller than the security plugin’s corresponding value, so fail2ban can ban the intruder before the account becomes locked-up.

Then create /etc/fail2ban/filter.d/wordpress.conf

# Fail2Ban configuration file
#
[INCLUDES]
before = common.conf
[Definition]
_daemon = wordpress
failregex = ^%(__prefix_line)sWordpress authentication failure for .* from <HOST>$
ignoreregex =

You could now test things with:

fail2ban-regex /var/log/wp_f2b.log /etc/fail2ban/filter.d/wordpress.conf

Last, you should create a file /etc/logrotate.d/wordpress in case the log file grows too large

/var/log/wp_f2b.log {
    size 30k
    create 0600 root root
    rotate 5
}

That’s it! Fail2ban should now automatically ban the IP-addresses where the failed login attempts come from. And while you are at it, you should also configure fail2ban to protect ssh, ftp or other possible services you have on your server.

– –

Original source: TSCADFX

WordPress and NextGen gallery (in)compatibility

It seems that the NextGen 2.x picture gallery plugin for WordPress is not fully compatible with WordPress versions 3.7 and up. I had to downgrade NextGen to version 1.9.13 to make it work again. Apologies to anyone who has been having difficulties in accessing the embedded picture galleries, for example in this post.

New server

This site, Kawaii Dieselpunk, is now running on a new (well, newer) server hardware.

What prompted me to upgrade the system was that at some point I suddenly realized with a chill, that the hard drive I had used when building the old webserver was already about 7 years old… No, there were no signs of HD trouble, SMART data was ok etc., but I felt that I was pushing my luck with a HD that old!

So, when my workmate offered to sell his old workstation (Intel Core 2 instead of an AMD Athlon XP, quite an upgrade) very cheaply, I took the opportunity and started moving the site to the new hardware. At the same time I upgraded the disk system to RAID1, which system has already been in use on my workstation, and on the file/print-server in my LAN, for several years.

While the RAID system offers some level of protection against data loss, it is NOT, repeat NOT, a substitute for a properly working backup system. However… it has already saved me and my data twice!!!

Replacing WordPress wp-cron with a cron job

There is a build-in feature in WordPress for scheduling various events, which is called wp-cron. For some reason or other it seems to work very sporadically, and in my case for backup scheduling, most of the time it didn’t work at all, causing missed backups. So, I had to do something about it…

On servers running on a Linux machine there is another much more reliable option for scheduling tasks: Linux’s own cron. In the next tutorial I’m trying to explain the required steps to switch from wp-cron to a real cron. I’m assuming here that you have at least basic Linux/Unix skills.

1) First, edit wp-config.php and add the following line to disable WordPress built-in cron

define('DISABLE_WP_CRON', true);

2) Add a cron job using

crontab -e

In this example I want to do the backup to Dropbox (using BackWPup plugin) on Fridays at 18.00. For more information about the crontab syntax please see the various cron tutorials in the web.

* 18 * * 5 wget -O /dev/null http://yoursite.com/wp-cron.php?doing_wp_cron > /dev/null 2>&1

Replace ‘yoursite.com’ with your website URL.

3) Test it

wget -O /dev/null http://yoursite.com/wp-cron.php?doing_wp_cron

If this works without error, all is well, and your new scheduling should be working. But if you get an error message like ‘403 Forbidden’, something is seriously wrong. This error caused me a lot of headscratching, but I finally found out that the BulletProof Security plugin was blocking wget as a User Agent. Consequently I needed to do the following alteration to the .htaccess file

- RewriteCond %{HTTP_USER_AGENT} (libwww-perl|wget|python|nikto|curl|scan|java|winhttp|clshttp|loader) [NC,OR]
+ RewriteCond %{HTTP_USER_AGENT} (libwww-perl|python|nikto|curl|scan|java|winhttp|clshttp|loader) [NC,OR]

In short, if you have any problems with the cron, please double- and triple-check your .htaccess settings!

Backup drive check and GPU/disk temperature

I thought to share here some of the bash scripts I’ve made for maintaining my Gentoo Linux system.

1. Checking for a correct detachable drive before backing up with rsync.

In my case, the script checks whether the correct eSATA backup drive is mounted before doing the actual backup. It first checks whether there are anything mounted on /mnt/esata, and tries to mount a drive if possible; a failure produces an error message. If a drive is found, it checks the UUID of the drive to confirm that the drive is really a correct one, to prevent accidental loss of data.

You can find the UUID of your drive in /dev/disk/by-uuid/

#!/bin/bash

if ! grep -q /mnt/esata /proc/mounts ; then
    if ! mount /mnt/esata ; then
        echo "\033[1;31m*\033[0m Backup drive 'BackupOne' is not present."
        exit 1
    fi
fi

DRIVELETTER=`cat /proc/mounts | grep /mnt/esata | cut -f 1 -d ' '`
DRIVEUUID=`ls -l /dev/disk/by-uuid/ | grep ${DRIVELETTER/\/dev\//} | awk '{ print $9 }'`
DRIVEUSAGE=`df -h | grep ${DRIVELETTER} | awk '{ print $5 }'`

if [ ! ${DRIVEUUID} = 735aa2fb-4022-4f41-80c7-4977e51aa967 ] ; then
    echo -e "\033[1;31m*\033[0m Wrong drive mounted in /mnt/esata: ${DRIVEUUID}"
    exit 1
fi

echo -e "\033[1;32m*\033[0m Backup drive 'BackupOne', UUID=${DRIVEUUID},"
echo -e "\033[1;32m*\033[0m is present and mounted as ${DRIVELETTER}, ${DRIVEUSAGE} used."
echo -e "\033[1;32m*\033[0m Commencing rsync..."
echo

rsync /what/ever/you/want/to/sync

NOTE: I have the corresponding line in /etc/fstab:

UUID=735aa2fb-4022-4f41-80c7-4977e51aa967 /mnt/esata ext3 noatime,users 0 1

2. Getting GPU and drive temperatures to a file.

This may seem somewhat complicated… I have four internal drives with SMART-compatible temperature sensors (sdb to sde, sda is a Compact Flash card). A cron job first reads the temperature of the ATI GPU, using

/opt/bin/aticonfig --od-gettemperature

and writes it on an intermediary file /tmp/atitemperature. The cron job then runs this script which reads the temperatures of the four internal drives and the two (possibly) mounted external drives, and writes these to the other intermediary file /tmp/temperature, which, when ready, is then added to the end of /var/log/temperature.log, which in turn is read by root-tail and displayed on the screen. The reason for those stupid intermediary files is the occasional unpredictable delays in reading the temperatures, which caused the output to flicker and jump around.

The checkremovable() function checks whether the removable drive exists and has SMART capability (i.e. if not, it is an USB thumb drive).

#!/bin/bash
# Check the temperatures of display adapter and hard drives.

checkremovable() {
    if [ -b $1 ]; then
        # Check whether it is a proper hard drive or an USB Flash drive
        SDFTEMP=`/usr/sbin/hddtemp -q -f /usr/share/hddtemp/hddgentoo.db SATA:$1 2>&1`
        if [ -n $"`echo $SDFTEMP | grep "no sensor"`" ]; then
            /usr/sbin/hddtemp -q $1 >> /tmp/temperature
        else
            echo $SDFTEMP >> /tmp/temperature
        fi
    else
        echo "$1: not present" >> /tmp/temperature
    fi
}

sleep 5
cat /tmp/atitemperature | grep -A 1 "HD 46" > /tmp/temperature
/usr/sbin/hddtemp -f /usr/share/hddtemp/hddgentoo.db SATA:/dev/sdb >> /tmp/temperature
/usr/sbin/hddtemp -f /usr/share/hddtemp/hddgentoo.db SATA:/dev/sdc >> /tmp/temperature
/usr/sbin/hddtemp -f /usr/share/hddtemp/hddgentoo.db SATA:/dev/sdd >> /tmp/temperature
/usr/sbin/hddtemp -f /usr/share/hddtemp/hddgentoo.db SATA:/dev/sde >> /tmp/temperature
checkremovable /dev/sdf
checkremovable /dev/sdg

cat /tmp/temperature >> /var/log/temperature.log
Return to Top ▲Return to Top ▲