Bash script to block brute force attacks with .htaccess

Your site is under attack. May be not right now, but several times a day. Password guessing, security vulnerabilities - that kind of stuff. In the error log it looks like this:

[Thu Jul 13 05:28:49 2017] [error] [client 196.64.59.18] ModSecurity: Access denied with code 401 (phase 2). Operator GT matched 0 at USER:bf_block. [file "/etc/apache2/mod_security/custom/wpbrute.conf"] [line "20"] [id "9999001"] [msg "ip address blocked for 5 minutes, more than 15 login attempts in 3 minutes."] [hostname "giraffesdoexist.com"] [uri "/wp-login.php"] [unique_id "WWdngc26uBEAAGIsOAEAAABG"]

The server, Apache in my case, handles these attacks routinely, but morons come back over and over again. Apart from performance implications there's a small chance of successful attack, so I made a simple script to block hackers via .htaccess.

This script runs by a cron job and has three parameters:

blockbruteforcers.sh ../logs/error_log ../domains/.htaccess 200

It searches error_log for ip-addresses that appear there more than 200 times and adds those to .htaccess with "deny from" prefix. Here's the script source code, don't forget to add "Execute" attribute when you create it on your server:

#!/bin/bash
if [ $# -ne 3 ]; then
  echo "Incorrect number of arguments. Should be 3: apache error log path, .htaccess file path, number of entries."
  exit
fi

filename=$2
num_entries=$3
error_log=$1

#If .htaccess file does not exist, create it and add 2 first lines
if [ ! -f "$filename" ]; then
  echo order allow,deny >> "$filename"
  echo allow from all >> "$filename"
fi

#Check if .htaccess file has new line at the end. If not - add it.
c=`tail -c 1 "$filename"`
if [ "$c" != "" ]; then
  echo >> "$filename"
fi

msg=""
arr=($(grep -o 'client [0-9.]*' "$error_log"| sort -r | uniq -c | sort -nr | awk -v pNum=$num_entries '$1>pNum {print $3}'))
for i in "${arr[@]}"
do
  if ! grep -q -P "$i$" "$filename"; then
    if [ ! -z "$msg" ]; then
	  msg+="\n"
	fi
    msg+="deny from ${i}"
  fi
done

if [ ! -z "$msg" ]; then
  echo -e "$msg" >> "$filename"
  echo "$msg"
else
  echo "no matching records found"
fi