Turning away the brutes

Table of Contents
expiring table entries with pfctl
Using expiretable to tidy your tables

If you run a Secure Shell login service anywhere which is accessible from the Internet, I'm sure you've seen things like these in your authentication logs:

Sep 26 03:12:34 skapet sshd[25771]: Failed password for root from 
200.72.41.31 port 40992 ssh2
Sep 26 03:12:34 skapet sshd[5279]: Failed password for root from 
200.72.41.31 port 40992 ssh2
Sep 26 03:12:35 skapet sshd[5279]: Received disconnect from 
200.72.41.31: 11: Bye Bye
Sep 26 03:12:44 skapet sshd[29635]: Invalid user admin from 
200.72.41.31
Sep 26 03:12:44 skapet sshd[24703]: input_userauth_request: 
invalid user admin
Sep 26 03:12:44 skapet sshd[24703]: Failed password for invalid user 
admin from 200.72.41.31 port 41484 ssh2
Sep 26 03:12:44 skapet sshd[29635]: Failed password for invalid user 
admin from 200.72.41.31 port 41484 ssh2
Sep 26 03:12:45 skapet sshd[24703]: Connection closed by 200.72.41.31
Sep 26 03:13:10 skapet sshd[11459]: Failed password for root from 
200.72.41.31 port 43344 ssh2
Sep 26 03:13:10 skapet sshd[7635]: Failed password for root from 
200.72.41.31 port 43344 ssh2
Sep 26 03:13:10 skapet sshd[11459]: Received disconnect from 
200.72.41.31: 11: Bye Bye
Sep 26 03:13:15 skapet sshd[31357]: Invalid user admin from 200.72.41.31
Sep 26 03:13:15 skapet sshd[10543]: input_userauth_request: invalid 
user admin
Sep 26 03:13:15 skapet sshd[10543]: Failed password for invalid user 
admin from 200.72.41.31 port 43811 ssh2
Sep 26 03:13:15 skapet sshd[31357]: Failed password for invalid user 
admin from 200.72.41.31 port 43811 ssh2
Sep 26 03:13:15 skapet sshd[10543]: Received disconnect from 
200.72.41.31: 11: Bye Bye
Sep 26 03:13:25 skapet sshd[6526]: Connection closed by 200.72.41.31

It gets repetitive after that. This is what a brute force attack looks like. Essentially somebody, or more likely, a cracked computer somewhere, is trying by brute force to find a combination of user name and password which will let them into your system.

The simplest response would be to write a pf.conf rule which blocks all access. This leads to another class of problems, including what you do in order to let people with legitimate business on your system access it anyway. You might consider moving the service to some other port, but then again, the ones flooding you on port 22 would probably be able to scan their way to port 22222 for a repeat performance.[1]

Since OpenBSD 3.7[2], PF has offered a slightly more elegant solution. You can write your pass rules so they maintain certain limits on what connecting hosts can do. For good measure, you can banish violators to a table of addresses which you deny some or all access. You can even choose to drop all existing connections from machines which overreach your limits, if you like. Here's how it's done:

Now first set up the table. In your tables section, add

table <bruteforce> persist

Then somewhere fairly early in your rule set you set up to block from the bruteforcers

block quick from <bruteforce>

And finally, your pass rule.

pass inet proto tcp from any to $localnet port $tcp_services \
        flags S/SA keep state \
	(max-src-conn 100, max-src-conn-rate 15/5, \
         overload <bruteforce> flush global)

This is rather similar to what we've seen before, isn't it? In fact, the first part is identical to the one we constructed earlier. The part in brackets is the new stuff which will ease your network load even further.

max-src-conn is the number of simultaneous connections you allow from one host. In this example, I've set it at 100, in your setup you may want a slightly higher or lower value.

max-src-conn-rate is the rate of new connections allowed from any single host, here 15 connections per 5 seconds. Again, you are the one to judge what suits your setup.

overload <bruteforce> means that any host which exceeds these limits gets its address added to the table bruteforce. Our rule set blocks all traffic from addresses in the bruteforce table.

finally, flush global says that when a host reaches the limit, that host's connections will be terminated (flushed). The global part says that for good measure, this applies to connections which match other pass rules too.

The effect is dramatic. My bruteforcers more often than not end up with "Fatal: timeout before authentication" messages, which is exactly what we want.

Once again, please keep in mind that this example rule is intended mainly as an illustration. It is not unlikely that your network's needs are better served by rather different rules or combinations of rules.

If, for example, you want to allow a generous number of connections in general, but would like to be a little more tight fisted when it comes to ssh, you could supplement the rule above with something like the one below, early on in your rule set:

pass quick proto tcp from any to any port ssh \
        flags S/SA keep state \
        (max-src-conn 15, max-src-conn-rate 5/3, \
        overload <bruteforce> flush global)

Despite what it likely says in your /etc/services file, existing ssh implementations use TCP only, as specified in RFC4253. You should be able to find the set of parameters which is just right for your situation by reading the relevant man pages and the PF User Guide, and perhaps a bit of experimentation.

NoteYou may not need to block all of your overloaders
 

It is probably worth noting at this point that the overload mechanism is a general technique which does not have to apply exclusively to the ssh service, and it is not necessarily always optimal to block all traffic from offenders entirely.

You could for example use an overload rule to protect a mail service or a web service, and you could use the overload table in a rule to assign offenders to a queue with a minimal bandwidth allocation (see the Section called ALTQ - handling unwanted traffic in the Chapter called Directing traffic with ALTQ) or, in the web case, to redirect to a specific web page (much like in the authpf example in the Chapter called An open, yet tightly guarded wireless network with authpf).

It is worth noting that this technique does not stop the slow bruteforcers commonly known as The Hail Mary Cloud (also see the article The Hail Mary Cloud And The Lessons Learned, which likely was (or is) a deliberate attempt at avoiding this kind of measure. See the referenced overview article for more material.

expiring table entries with pfctl

At this point, we have tables which will be filled by our overload rules, and since we could reasonably expect our gateways to have months of uptime, the tables will grow incrementally, taking up more memory as time goes by.

You could also find that an IP address you blocked last week due to a brute force attack was in fact a dynamically assigned one, which is now assigned to a different ISP customer who has a legitimate reason to try communicating with hosts in your network.

Situations like these were what caused Henning Brauer to add to pfctl the ability to expire table entries not referenced in a specified number of seconds (in OpenBSD 4.1). For example, the command

# pfctl -t bruteforce -T expire 86400

will remove <bruteforce> table entries which have not been referenced for 86400 seconds.

Notes

[1]

At the time this was written, password gropers trying high ports was purely theoretical, but in early 2013, I received confirmation that such attempts were indeed happening, see There's No Protection In High Ports Anymore. If Indeed There Ever Was. for a longer description.

[2]

Introduced to FreeBSD in version 6.0