October 4, 2014

Receive a report about (banned) IP using newbrute

When you manage servers that are accessible from the web, you have to deal with bots (unfortunately). One of my OpenBSD server is regularly attacked, despite the strict and restrictive pf(4) config. I also tried to write a "paranoid" config for sshd(8). This combo seems to be kinda efficient for now.

Even if the dumb "dudes" are unable to log on, my logs are "spammed" by those unsuccessful attempts. Most of the time (around 90%), the attacks came from China. Nothing impressive indeed, because you probably knew that already. Sometimes, I needed to ban an entire subnet to prevent the brute force shot every 10 minutes, as I did with The "smarter" ones try to connect one time per 15 minutes, the others are stupid enough to trigger the PF's wrath, in less than 10 seconds. Yep, you shouldn't mess with that guy.

The special table which includes all those banned addresses, is very generous. Usually, two or three IPs are added everyday (some days are more "productive" though). I don't want to miss the additions (for administration & entertainment), so I wrote a tiny script designed to send me a mail, when one or more addresses join the table. It's like a report.

But why a mail? OpenBSD includes a useful feature that warns you, when important security changes occur on the system (new file, permissions) via security(8). This is great to "follow" the machine. I wanted to have something in that vein.

set -e


trap 'print "An error occured. Exiting." && exit 1' ERR
trap 'clean_oldtable' EXIT

function clean_oldtable {
	[[ -f /tmp/${PF_TABLENAME}_OLD ]] && rm "/tmp/${PF_TABLENAME}_OLD"

First of all, we use set -e. When an error happens ($? != 0), the script launches the ERR trap (here a message) and closes. There is a function named clean_oldtable which is designed to clean an existing file, in /tmp. It's called just before the script ends.

if [[ ! -r $PF_TABLEFILE ]]; then
	print -u2 "No existing table file found"
	exit 1

pfctl -t "$PF_TABLENAME" -Tshow >"$PF_TABLEFILE"

One time per day, I save the black list to a file called /etc/bruteforce, using the command pfctl(8). I decided to do that in case the system is halted or restarted, I don't want to lose the "holy Grail". This file is required by the script and therefore it won't run without it. The next lines are obvious, the "previous" catalog is copied to /tmp (I like to work inside that directory) and then, pfctl(8) output is redirected to the specified file in /etc, as I said it earlier.

if ! diff -q "/tmp/${PF_TABLENAME}_OLD" "$PF_TABLEFILE" >/dev/null 2>&1; then
		| awk '/^\+/&&!/\+\+\+/{gsub(/\+/,"");print}')"

	print "The following address(es) was/were added to the $PF_TABLENAME table:\n\n$PF_TABLEDIFF" \
		| mail -s "New IP address(es) summary" root
	print "No IP added in the $PF_TABLENAME table"

Maybe you're a regular reader (it would mean we are three or four)... If so, you know I like to "work" with awk(1) and diff(1). The pure efficiency. I'm used to unified format, that's why I chose -u option. The awk(1) regex is quite simple: we match the lines starting with + symbol and we do not "record" the +++ /etc/foo summary, from the header. + needs to be escaped by \ considering it refers to a metacharacter (from what I know, OpenBSD has a nawk variant). gsub is included to erase the leading "plus" sign.

I could achieve the same result with the lovely while IFS= read and save one process, but many people would have probably warned me that I SHOULD try awk(1). And I didn't want to introduce any error. Assuming the diff(1) exit status is not equal to zero, a mail is sent. If not, a message is displayed. It gives me visibility when I run it manually. newbrute is executed through /etc/daily.local.

The next day when you login, you will have to verify your email(s). An email has the following form:

Date: Fri, 3 Oct 2014 01:30:02 +0200 (CEST)
From: Charlie Root <root@foo.domain>
Message-Id: <201410022330.s92NU27k015450@foo.domain>
To: root@foo.domain
Subject: New IP address(es) summary

The following address(es) was/were added to the bruteforce table:

Oh by the way, I forgot to put the link. Perhaps you will read it...