Raspberry PI 4 Boot from USB HDD not working GPT

This is more of a note for me than a guide or help for anyone else.

At present the PI4 does NOT support boot from USB, but you can work around this like the old days by using an SD card for the /boot and setting this to load the root from USB Stick/HDD.

However, this only appears to work for me using MBR partition type and just hangs when using GPT. This may be because I’m also encrypting the disk and using headless network boot. But that side does work and the disk/paritions are decrypt and list in /dev/disk/by-label but the OS just will NOT boot for me. Copying everything to the exact same partition layout and labels under MBR and it just works.

It seems a weird problem, as the SD card that’s actually booting and has the kernel is MBR and I don’t see why it would then care, the kernel has to be aware of GPT and as I said lists everything correctly. I can even decrypt and mount during the dropbear network boot bit, but the OS just wont boot. Workaround at present is to place the OS on it’s own drive/stick and use GPT on bigger disks purely for data. I’ve only found this while trying to use a 5Tb disk for both OS and Data. the 2Tb disk I can use MBR on without loosing space so the workaround isn’t really needed, just use MBR.

Raspberry PI 4 Blink Power LED (Locate in Cluster)

I bought a small cluster rack for my Raspberry PI 4’s. I have 4 of them racked and the idea was to keep it simple, assign static IP addresses .80,.81,.82,.83 with {NAME}-00, {NAME}-01 etc, starting at the bottom. Makes complete sense! however before I bought the rack they were all laid out flat and I thought in order left to right. When it came time to rack them I carefully screwed them all in place and powered up. All good. That is until I needed to work on No.3, shut it down and found it 2nd up in the rack 🙁 No.1 was next and No.2 on top. I don’t think I could have made the order worse if I was trying to lol.

Anyway, I finally got a bit of time to sort out the order. I’d already powered down No.2 and I know 100% No.0 is on the bottom. but that still left 50/50 that I’d be moving the correct PI (having to take the entire rack apart). So I thought I must be able to blink the ACT LED. Well a quick test and yes you can but disk access overrides it, so it’s not exactly great. but then I found contrary to what one of the guides say that the PWR LED can be controlled, it’s not just hard wired to 3v (this may be true on earlier PI’s, no idea and I don’t need to test it atm).

So a nice quick script:

nano locate_pi.sh
#!/bin/bash

# Setup the keyboard interrupt.
trap '{ echo "Stop Blinking. Setting LED to ON." ; echo 1 > /sys/class/leds/led1/brightness ; exit 0 ; }' INT

# Blink the Power LED
echo "Blinking Power LED..."
i=0
while true ; do
echo $i > /sys/class/leds/led1/brightness
sleep 1
i=$(((i==0?1:0)))
done

Make it executable:

chmod +x locate_pi.sh

Then you can run it:

./locate_pi.sh

And hey presto a nice flashing LED on the PI in a rack. Now you could be really fancy and assign a few different patterns of flashes by doing a bit of coding in the ‘while’ or simply adjusting the sleep time if your lazy. That could allow you to find more than 1 pi if you set them to blink differently but for my use a simple blink is plenty (for now).

As always, hope this helps someone. you could even move the locate_pi.sh to /bin or /usr/bin to allow you to just run it from wherever you are.

NB: I’m assuming writing to /sys/class/leds/led1/brightness can only be done by root (unless you change the permissions), as such this script will need to be run as root to work but I haven’t test as another user to know if this is true.

Remove/Hide WooCommerce Mine Status Filter

So this has been annoying me for ages.

I don’t really get what’s it for, more-so because I’m the only shop admin! Either all the orders are “Mine” or none of them.
I remember searching for why previously and came across a bug during a WooCommerce Database update that caused orders to be re-assigned to the owner id 1 if they were set at 0 or something similar. But it didn’t really explain the purpose of this filter and I didn’t get any further in my understanding of why anyone might find it useful.
There was a suggestion that it would come into play for order that were created using the ‘Add Order’ button. Now I can see that may be useful, I do occasionally create orders manually for customers who are having problems and this could be a way of finding them, but there’s no way I’ve manually create the hundreds or thousands that’s there (depending on which site I’m looking at, we have a few).

Not getting anywhere on finding to disable or hide it and just finding lots of info on custom statuses, I gave up. I did consider using CSS to just hide it but I really hate working on CSS when I have to. This wasn’t annoying enough that I had to.

But today I noticed our ‘Pending Payment’ count was rather high. We’ve had a busy month and I haven’t had time to pay attention to the small details. but almost 300 orders ‘Pending Payment’ raised my interest. That was quite cimply that the cronjob to clear them wasn’t there, and I vaguely remembered this happening a few years back. Clearing the inventory timeout, save and setting it back to 60 mins (and save) recreate the cron job. That fired straight away and cancelled half of the orders there. I gave it a few mins and force run the cronjob again, that cleared the queue right down to about 8 orders. 2 of them are valid carts going through now and 6 are manual orders that may/may not have been paid (I need to check each of them) but were holding orders (not really shipping anything) so not an issue.

Once I finished with ‘Pending Payment’ I spotted the ‘Mine’ filter again, which brought back ‘why is it there, can I get rid of it?, it’s totally pointless for me. It just takes up room.

Well people I can say, you can hide it 😀 and it’s pretty easy to do.

You can add the following to your functions.php

function hide_mine_filter_shop_orders( $views ) {
    // Unset the option from the views
    unset( $views['mine'] );
    return $views;
};
add_filter( 'views_edit-shop_order', 'hide_mine_filter_shop_orders' );

And it’s now gone:

** The pics are from 2 different sites. I’ll be applying it to all shortly 🙂

I know it’s not much, but I’ve never used it and with lots of different order status’ it much better to have one less.

I should add just in case I ever read this again, I actually put that in one of our own plugins that restores the Items Purchased column that was removed from WooCommerce Core but crucial to speedily deal with orders (that still should have been a tick option in screen options, but the devs wouldn’t take that on board).

Domoticz Bluetooth Presence Detection

Update script being used as presence detection. Previously would take arguments for -s and -b (switch and bluetooth) and only check 1 device. Required multiple cron entries, 1 for each device. This could lead to polling issues if more devices are added. This new script “Should” only poll once for all devices. There’s a pause when it’s searching for the first device but consecutive devices fly through so I think it’s working.

Python Script saved in /home/domoticz/domoticz/bluetooth/bluescan.py

#!/usr/bin/python

import bluetooth
import time
import argparse
import urllib
import urllib2
import json

parser = argparse.ArgumentParser(description='SwitchID BluetoothID')
parser.add_argument('-l', '--link', action='append', help='BluetoothMac-SwitchID', required=True)
parser.add_argument('-d', '--debug', help='Debug Output', required=False, action='store_true')
parser.add_argument('-u', '--updatefound', help='Always update Found Devices', required=False, action='store_true')
args = parser.parse_args()

def update_switch(args, switchid, status):
URL="http://127.0.0.1/json.htm?username=YWRtaW4=&password=ZG9tb3RpY3o=&type=command&param=switchlight&idx={0}&switchcmd={1}".format( switchid, status )
if args.debug:
print URL
request = urllib2.Request(URL)
response = urllib2.urlopen(request)
return response.read()

if args.debug:
print "Checking " + time.strftime("%a, %d %b %Y %H:%M:%S", time.gmtime())

for value in args.link:
s = value.split("-")
blueid, switchid = s[0], s[1]
if args.debug:
print ("Switch ID: %s" %switchid )
print ("Bluetooth ID: %s" %blueid )

#Get the current status
URL="http://127.0.0.1/json.htm?username=YWRtaW4=&password=ZG9tb3RpY3o=&type=devices&rid={0}".format( switchid )
if args.debug:
print URL
response = urllib2.urlopen(URL)
json_data = json.load(response)
current_status = json_data['result'][0]['Status']
if args.debug:
print ("Current Status: %s" %current_status )

result = bluetooth.lookup_name(blueid, timeout=10)

if (result != None):
if args.debug:
print "Found"
# status = 1
status = "On"
else:
if args.debug:
print "NOT Found"
# status = 0
status = "Off"

if current_status == status:
if args.debug:
print "Status UnChanged"
if status=="On" and args.updatefound:
update = update_switch( args, switchid, status )
if args.debug:
print update
else:
update = update_switch( args, switchid, status )
if args.debug:
print update

Cronjob as user domoticz

* * *   *       *       /home/domoticz/domoticz/bluetooth/bluescan.py -u -l C0:EE:FB:00:00:00-7 -l A0:82:1F:00:00:00-23

Had to us – as separator. : in mac address would be confusing, tried ; but conflicts with bash, was going to use a space but would need to account for that in the argument parser and – was just easier.

Google Translate Text-to-Speech Linux

Scroll down for the script, or read who/why we use it first 🙂

I’ve been using a brilliant script by Dan Fountain for a few years as part of a WooCommerce barcode scanner python program I made. It allows us to update/process orders without the need of working in the admin interface 90% of the time.
For this we use a Raspberry PI, and a handheld portable barcode scanner. One of the big things that was needed was feedback from the PI as to what it’s done or what it’s doing. I’ve attached an LCD screen and most recently added a whole web interface output (mainly for diagnostics), but when your scanning a bunch of orders especially in bulk you dont want to be looking at a very small LCD screen about 4m away. So I added speakers and TTS.

There’s a few different things the barcode program can do (get order status, add tracking code to the order) but the most important is update the status of the order.
We start processing orders first thing in the morning by scanning a ‘bulk’ QR code, then scanning each order that’s on the printer. Once they’ve all been scanned we scan another QR Code ‘Order Printed’. Quite simply this updates each order status from ‘processing’ to ‘printed’, and this is important in case Google Cloud Print fails to print an order (it does from time to time), anything left in ‘processing’ needs checking.

Anyway that’s not the important bit for this post! The important bit is the python program giving audio feedback. While we could have gone for a TTS engine local to the pi, Google Translate option gives a far better sounding voice. The above scenario would do the following:
We scan Bulk mode
Pi says ‘Bulk Capture Active’
We scan first order
PI says ‘One’
We scan second order
PI says ‘two’
and on
and on
Once all the orders are scanned, we then scan ‘Printed’
PI says ‘Bulk Capture Finished. Processing x’ where x is the number of orders.
PI says ‘One’
PI says ‘Two’
etc. etc.
Then Pi Finally says ‘Finished Bulk Processing’.

Now there’s certainly the ability to pass all the orders in one go via the WooCommerce API, but we handle them as individual requests within python so that we can do some order status checking before changing it’s status. i.e if an order has already been moved from ‘processing’ to say ‘cancelled’ we dont want to move it again to ‘printed’, at that point the PI would say ‘Error Processing Order xxxxxx’ where xxxxxx is the order number.

As you can see from the above flows, the actual text being read ends up being the same text over and over and over. The number ‘One’ can be read about 10 times as we bulk move things around queues. While it’s certainly possible to just fire the same thing at Google Translate over ad over, it’s far nicer to play friendly and cache what we can use again and again.

The code below is based on the awesome work of Dan Fountain, with the following updates:
Added Caching
Added MPG123 options (to speed up the play back a little)
Added a client to the wget request (without it google started blocking heavy requests when the cache is clear).

#!/bin/bash
#################################
# Speech Script by Dan Fountain #
#      [email protected]     #
#                               #
# Added caching by JDL          #
#################################

CACHE=/tmp/ttscache

mkdir -p $CACHE

INPUT=$*
STRINGNUM=0
MPG123OPTS="-h 3 -d 4 -m --stereo -q"

ary=($INPUT)
for key in "${!ary[@]}"
  do
    SHORTTMP[$STRINGNUM]="${SHORTTMP[$STRINGNUM]} ${ary[$key]}"
    LENGTH=$(echo ${#SHORTTMP[$STRINGNUM]})
    #echo "word:$key, ${ary[$key]}"
    #echo "adding to: $STRINGNUM"
    if [[ "$LENGTH" -lt "100" ]]; then
      #echo starting new line
      SHORT[$STRINGNUM]=${SHORTTMP[$STRINGNUM]}
    else
      STRINGNUM=$(($STRINGNUM+1))
      SHORTTMP[$STRINGNUM]="${ary[$key]}"
      SHORT[$STRINGNUM]="${ary[$key]}"
    fi
done

for key in "${!SHORT[@]}"
  do
    #echo "line: $key is: ${SHORT[$key]}"

    echo "Playing line: $(($key+1)) of $(($STRINGNUM+1))"
    NEXTURL=$(echo ${SHORT[$key]} | xxd -plain | tr -d '\n' | sed 's/\(..\)/%\1/g')
    URL="http://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&q=$NEXTURL&tl=En-gb"
    URLMD5=`/bin/echo $URL | /usr/bin/md5sum | /usr/bin/cut -f1 -d" "`
    if [ -s "$CACHE/$URLMD5" ]
    then
       mpg123 $MPG123OPTS "$CACHE/$URLMD5"
    else
       echo "Getting : $URL"
       wget -U "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)" "$URL" -O $CACHE/$URLMD5
       mpg123 $MPG123OPTS "$CACHE/$URLMD5"
    fi
done

##!/bin/bash
#say() { local IFS=+;/usr/bin/mplayer -ao alsa -really-quiet -noconsolecontrols "http://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&q=$*&tl=En-us"; }
#say $*

WooCommerce Nag Notice

We all (mostly) understand updates are important and I’m sure there were only good intentions by setting it but the
‘Connect your store to WooCommerce.com to receive extensions updates and support.’
Nag notice is ridiculous. A non-dismissable notice should never be allowed. I get you dont want people to just quickly click the dismiss button, so why not put an option at the bottom of the connect page ‘Dismiss this alert. I understand what this means’ even if it only dismissed for say 3 months and then you had to do it again. It would still be annoying but at least easier to deal with.

But no, in someones infinite wisdom they’ve decided you absolutely must connect your store and have no other option. Well here’s the code to add to stop that nag notice

add_filter( 'woocommerce_helper_suppress_admin_notices', '__return_true' );

** It is your own responsibility to keep your site upto date.
** Disabling this notice, may disable other woocommerce notices.

There are of course legitimate reasons why would wouldn’t want to connect your store, managing your updates your own way should always be allowed. So dev’s stop trying to dictate how you want/think things should run, choice is the key.

WooCommerce 3.3.0+

Yesterday upgrade a store to WooCommerce 3.3.1 from whatever the hell it was on before.

Today I’ve spent the day putting things right 😠 All the issues are around the new Orders UI and it seems like petty small stuff but it’s safe to say I’m hating the new UI because I’ve wasted the day dealing with over 50 complaints.
For those unfamiliar here’s the proposed changes (the end result is a little different) https://woocommerce.wordpress.com/2017/11/16/wc-3-3-order-screen-changes-including-a-new-preview-function-ready-for-feedback/#comment-4137

I’ve so far fixed some of the issues, such as rearranging the columns (why the actual fu*k status was moved I’ll never know or understand). The below code is probably not the best way to do it, but it works

// Function to Change the order of the Columns
function woocommerce_myinitials_restore_columns( $columns ) {
    $new_order = array(
       'cb' => '',
       'order_status' => '',
       'csv_export_status' => '', // dont think this ones standard but it's part of a plugin we use.
       'order_number' => '',
       'order_items' => '',
       'billing_address' => '',
       'shipping_address' => '',
       'order_date' => '',
       'order_total' => '',
       'wc_actions' => '',
    );
    foreach($columns as $key => $value) {
       $new_order[$key] = $value;
    }

    return $new_order;
}
add_filter( 'manage_edit-shop_order_columns', 'woocommerce_myinitials_restore_columns',99 );

So that’s one issue solved. The next was being able to click anywhere in the row opens the order (yeah I’m sure that’s nice, but if you rely on tapping a touchscreen i.e. click and drag to scroll then this causes problems). The following code adds the no-link class to the tr and stops this shitty behaviour

function woocommerce_myinitials_restore_columns_add_nolink_class( $classes ) {
    if ( is_admin() ) {
        $current_screen = get_current_screen();
        if ( $current_screen->base == 'edit' && $current_screen->post_type == 'shop_order' ) {
            $classes[] = 'no-link';
        }
    }
    return $classes;
}
add_filter( 'post_class', 'woocommerce_myinitials_restore_columns_add_nolink_class' );

Thanks on this one goes to ‘rodrigoprimo’ for the initial fix and others who picked it up and added a bit to it https://github.com/woocommerce/woocommerce/pull/18708.

I’ve added the following as a stylesheet

.post-type-shop_order .wp-list-table td, .post-type-shop_order .wp-list-table th {
   vertical-align: unset;
}

.post-type-shop_order .wp-list-table td.order_status {
   vertical-align: middle;
   text-align: center;
}

This places the orders back at the top of the row, and stops the previous restoration of items sold link jumping around. but I’d rather the new status text stays in the middle inline with the checkbox, hopefully we’ll get this back to an icon soon.

All of the above I’ve added to our custom plugin, you could either do this or add them to your functions.php

Outstanding issues:
1. Getting back the Email address. There is some hope this may come back officially, but I’ll be fixing it for us tomorrow.
2. The Status being text not icons. I understand this makes more sense to new users but if you have some long statuses like we do, the text doesn’t fit and we’ve got 10 statuses all looking the same. Having coloured icons worked for us and if you weren’t sure hover the mouse. I’ll be looking to get them back to icons tomorrow.
3) Date column, just why! Why would anyone think not putting the actual date and time of the order here is a good idea. Stupid ‘X mins ago’ is no use at all.

The new preview window looks good but I really dont see it getting much use, we need the important data on the front. If it’s not that important just open the order. WooCommerce dev’s decided to screw with it but I dont think there’s an understanding that if your going as far as opening the preview window then I’m pretty sure you were used to just editing the order which is probably still going to be the case.

So summing up today, I’ve had a shit day of people moaning at me because some developers decided to improve something that really didn’t need touching. Doesn’t sound like every developer I’ve ever known 😂. I’m now getting something to eat before I go near everything I’d planned on working on today.

Setting NGINX + PHPLDAPADMIN location & php for subfolder

Spent far too long trying out different ways to get this to work. I need to setup a server listening to the local IP address to restrict things like phpldapadmin to internal requests. But hit problems with nginx appending the location to the root path, and php having no idea where to get the files from.

Here’s the config that ended up working

server {
listen 80;
root /var/www/default/;

index index.html index.htm index.nginx-debian.html;

server_name 192.168.0.3;

location = / {
        try_files $uri $uri/ =404;
}

location /phpldapadmin {
        alias /usr/share/phpldapadmin/htdocs;
        index index.php index.html index.htm;

        location ~ \.php$ {
                include snippets/fastcgi-php.conf;

                # With php7.0-fpm:
                fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
                fastcgi_param SCRIPT_FILENAME $request_filename;
                fastcgi_intercept_errors on;
        }
}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
location ~ \.php$ {
        include snippets/fastcgi-php.conf;

        # With php7.0-cgi alone:
        # fastcgi_pass 127.0.0.1:9000;
        # With php7.0-fpm:
        fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $request_filename;
        fastcgi_intercept_errors on;
}

access_log /var/log/nginx/localip-access.log;
error_log /var/log/nginx/localip-error.log;
}

dpkg: error processing package libc6:amd64 (–configure):

A nice easy one for 2am 🙁 but it took me hours to work out (and it really shouldn’t have).

Trying to update servers using apt-get upgrade. It listed about 6 packages to be upgraded, but kept throwing an error:

Setting up libc6:amd64 (2.23-0ubuntu4) ...
sh: echo: I/O error
sh: echo: I/O error
dpkg: error processing package libc6:amd64 (--configure):
subprocess installed post-installation script returned error exit status 1
Errors were encountered while processing:
libc6:amd64
E: Sub-process /usr/bin/dpkg returned an error code (1)

I spent a good few hours searching google and trying to work out what had been installed that could be causing an issue with libc6. Though I was fairly certain nothing had been (only because this is 2 days after a migration to new servers, and I’m hitting this problem on 2/4 servers, but they have extremely similar setups).

By chance I noticed:

Filesystem Size Used Avail Use% Mounted on
udev 489M 0 489M 0% /dev
tmpfs 100M 100M 0 100% /run
/dev/vda1 30G 16G 13G 55% /
tmpfs 497M 0 497M 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 497M 0 497M 0% /sys/fs/cgroup
tmpfs 100M 0 100M 0% /run/user/1000

I’ll give you a clue “look at /run” 🙁

Something is eating all the space. So I tried to run ncdu to look for large files (I know there’s other ways, but I like ncdu). But I hadn’t installed it on this new server and I can’t install it with apt-get broken.

Thinking /run is bound to be causing some issues (still not sure if it’s causing this particular issue), I reboot the server (bad move!). It locked up and had to be power cycled. Thankfully it’s a droplet and with DigitalOcean I can power cycle easily (I did try the console but it wouldn’t connect).

Anyway after a reboot, /run started at 5% used, but quickly grew to 70%. but I did managed to install ncdu, and with that I knew the problems I had with apt-get were being caused by a full /run.

After a quick (very quick) look at ncdu /run I could see hhvm.hhbc taking up approx 85Mb+

A quick check of the config and I can see hhvm is configured to do so. So I adjusted the config to put it in /var/cache/hhvm/hhvm.hhbc instead and update the systemd service script to create /var/cache/hhvm and set it’s owner.

Another reboot, everything seems fine and /run is now at 3% used.

And I’ve run apt-get upgrade successfully.

I’m thankful that I noticed, I really thought I’d screwed something on these 2 servers while migrating, and I could see another night of rebuilding new servers ahead of me.

Morale of the story: Check your not out of space when you get weird errors (yes the I/O should have rang some bells, but hey it is 2am).

Nginx + WordPress + Fail2Ban + CloudFlare

I hate being woken at 2am, to be told “we’re under attack!”. Well that’s pretty much this morning 🙁

Now to be fair, it wouldn’t have even been noticed. Our servers and setup handled it very well and it was only noticed during a switch over to a backup server with less resources during maintenance.

On checking the logs I could see lots of attempts like

X.Y.X.Y - - [28/Aug/2016:03:12:16 +0100] "POST /wp-login.php HTTP/1.0" 200 5649 "-" "-"

We’re walking 114,893 requests to just the one server. My first instinct is to add the offending IP address to our IPTables BLOCK list and just drop the traffic altogether. Except this no longer works with CloudFlare since the IP addresses the firewall will see is theirs not the offender!

No problem, CloudFlare deals with this(!?) I can just turn on ‘Under Attack’ mode and let them handle it. This is where you quickly learn it doesn’t really do much. Personally I got a lovely 5 second delay when accessing our websites with ‘Under Attack’ activated. but our servers were still being bombarded with requests. So I added the offending IP addresses to the firewall on CloudFlare. Surely that will block them from even reaching our servers! While I can’t say it had no effect, out of a number of IP addresses I had added some were still hitting our servers quite heavily.

So the question becomes ‘How can I drop the traffic at nginx level?’. Nginx is configured to restore the real IP addresses, so I should be able to block the real offenders here not CloudFlare.

Turns out to be pretty easy. Add:

### Block spammers and other unwanted visitors ###
include blockips.conf;

Into the http section in /etc/nginx/nginx.conf

http {

### Block spammers and other unwanted visitors ###
include blockips.conf;

...

}

Then create the /etc/nginx/blockips.conf:

deny X.Y.X.Y;

Just add a deny line for each offending IP. Then reload nginx (service nginx reload). I’d recommend testing your new config first (nginx -t)

Now with that done of both servers I’m still seeing requests but they are now getting 403 errors and not allowed to hit any of the websites on our servers 🙂 after another 5 minutes of attack they clearly gave up as all the requests stopped.

But we’re not done. What if they’re just changing IP addresses? we need this to be automatic. Enter Fail2Ban. I haven’t used this in some time but I know what I need it to do.

  1. Check all the websites log files for wp-login.php attempts
  2. Write to /etc/nginx/blockips.conf
  3. Reload nginx

Should be quite simple. Turns out it is, but it took hours trying to figure out how since all the guides seem to think you’ll be using Fail2Ban to configure IPTables.

Here’s the files/configuration I added for Fail2Ban

# WordPress brute force auth filter
#
# Block IPs trying to auth wp wordpress
#
# Matches e.g.
# X.Y.X.Y - - [28/Aug/2016:03:12:16 +0100] "POST /wp-login.php HTTP/1.0" 200 5649 "-" "-"
#

[Definition]
failregex = ^<HOST> .* "POST /wp-login.php.*200
ignoreregex =</pre>

/etc/fail2ban/jail.d/wordpress-auth.conf
<!--?prettify linenums=true?-->
<pre class="prettyprint">[wordpress-auth]
enabled = true
filter = wordpress-auth
action = wordpress-auth
logpath = /var/log/nginx/*access*.log
bantime = 1200
maxretry = 8
# Fail2Ban configuration file based on dummy.conf
#
# Author: JD
#
#

[Definition]

# Option: actionstart
# Notes.: command executed once at the start of Fail2Ban.
# Values: CMD
#
actionstart = touch /etc/nginx/blockips.conf
service nginx reload

# Option: actionstop
# Notes.: command executed once at the end of Fail2Ban
# Values: CMD
#
# dont do this actionstop = echo "" &gt; /etc/nginx/blockips.conf
# service nginx reload
actionstop =

# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck =

# Option: actionban
# Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionban = echo "deny &lt;ip&gt;;" &gt;&gt; /etc/nginx/blockips.conf
service nginx reload

# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionunban = sed -i "/&lt;ip&gt;/d" /etc/nginx/blockips.conf
service nginx reload

[Init]

init = 123

It’s pretty simple and will need some tweaking. I’m not so sure 8 requests in 20 mins is very good. We do have customers on some of our sites who regularly forget their password. The regex does look at the 200 code, I read that a successful auth would actually give a 304. Not sure if this is correct so will need some testing. I also found other information on how to change the code to a 403 for failed login attempt. I think this would be a huge plus, but I’m not looking into that tonight.

A few tests using curl and I can see Fail2Ban has done it’s job, added the IP to the nginx blockips file and reload nginx 😀 I’m not too worried about syncing this across servers, as shown tonight they will balance well and I’m confident that within a few minutes of being bombarded they would both independently block the IP.

So there’s my morning of working around using CloudFlare but still keeping some form of block list. Hope this helps someone. Please comment to let me know if you found any of this useful/worth reading.