How to automatically block IPs that do a dictionary attack on your SSH server
By Robin Smidsrød on Oct 7, 2011 | In Software, Perl
Have you ever noticed that the sshd on your publicly facing machines gets bombarded with dictionary attacks several times per day? This problem is mostly an annoyance, as it fills up the logs with lots of User authentication failed, wrong password for <username> messages. There are of course several ways to work around this problem, and the most common one is to run sshd on another port than 22. I find that approach cumbersome, because it means you'll always have to configure your client software to connect to a non-standard port, and in lots of cases a firewall at your location might be blocking the traffic as well. Isn't there a way to block these bothersome users instead?
I recently read an article in Linux Journal #210 that talked about a new feature in the Linux kernel called ipset. It allows you to create sets that store IP addresses which can dynamically be added to and removed from. This sets can be configured as selectors in iptables rules, so that you can perform actions on any IP address in the set, like dropping the packets. You can also create iptables rules that add or remove an IP from a set.
In Debian it is straight-forward to install ipset. Type in these commands to install ipset and the required kernel module. It should ensure it is automatically recompiled whenever you do a kernel upgrade.
$ apt-get install ipset xtables-addons-source
$ module-assistant auto-install xtables-addons
I asked around on IRC if it was possible to make sshd execute a shell script once the amount of logins from an IP went above some configurable threshold, but apparently it is not possible. Someone pointed me at fail2ban which is a system that scans your syslog looking for failed logins and turn them into blocking iptables rules (and probably more, I didn't look very closely). I thought that this was too slow, as I wanted something that triggered as soon as multiple failed logins for a user reached a certain threshold, but I wanted the block to be temporary, not permanent.
That is when I remembered that syslog (rsyslog in my case) can be configured to run the log messages through a program. I put together the program below that reads incoming messages from rsyslog with auth.info facility/level (default for sshd on Debian) and does the necessary things to ensure the offending IP is added to my autoblock ipset if it triggers a certain amount of failed logins in a short period. The autoblock ipset uses the iptree storage module that has a --timeout parameter to automatically purge entries from the set after a given time. I set it to 3600 seconds (1 hour).
What is also quite cool, is that I have a rule that will immediately put the IP of anyone that tries to connect to my SMTP port (I don't run a mail server on my firewall) on the autoblock list. This particular bit happens completely inside netfilter, so its effect is immediate. Bye bye spammers! Try to connect to my (non-existent) email server and you're instantly blocked for an hour. And you didn't even know what hit you. :)
The reason I like the temporary block is that sometimes I port-scan my own server or do other strange things with it from remote computers, and the fact that I know the block will be lifted after an hour means I can continue without having to get physical access to server to remove the block. I can just smack myself in the forehead and wait an hour and continue whatever stupid thing I was doing.
PS: Remember that lots of IRC servers like to port-scan your IP when you connect to them, so you might need to put up some exceptions for those if you're an active IRC user and your particular IRC server likes to probe the SMTP port.
12 comments
iptables -I INPUT -p tcp --dport ssh -i eth0 -m state --state NEW -m recent --update --seconds 60 --hitcount 6 -j DROP
And slow? Kidding? Its just tailing on a logfile. Even a router with some low Mhz can easily do that.
This is what we call in the industry, major unsubstantiated claims.
In the slang, bullshit.
@a: That looks kinda nice. Could you elaborate on exactly how it works? I wasn't aware that something like that existed.@Sid: The too slow argument is at the fact that fail2ban seems to use a polling based system for reading the log files, while I use a syslog callback-based method of being informed about the failed logins. I would expect a callback-based method to almost always be faster to react to an action, and not use so many resources. But I see your point that installing it on Debian is easy. The documentation on fail2ban.org doesn't make it seem that easy, though. Also, fail2ban is written in Python, and I prefer Perl. Go flame war!
@Dennis: Accepting password auth is something I like, because it allows me to just download PuTTY or any other ssh client and connect as long as I can connect to the internet and a super-restrictive firewall isn't in place. I don't even need to bring any keys with me. I do use SSH keys on the computers I normally log in from.
@Hello71: Well, I'm not writing a university paper, I'm talking about my experiments on using ipset, rsyslog and some perl code to solve something that annoyed me. If fail2ban failed to impress me at first look, I consider that a failure of communication for the project, not a failure of the project as a whole. At second look, after what Sid Burn mentioned, it seems as it isn't as complicated to setup as I thought. The fail2ban project should consider this useful input on how to atract potential new users from someone who has never heard of it before.
@All: I also wanted to try out ipset and its iptree module with automatic timeout for unbanning, as it requires less work than traditional iptables rules, because you only have to match the ip against the set (hash lookup) instead of against multiple rules (linear scanning). The use of multiple ipsets to keep state on failed logins was just my way of letting the kernel do something for me so I don't need a separate storage system with timeout features for that.
The two IPTables lines provided by 'a' implement a hit counter in IP tables. The first line causes every NEW connection on port 22 too add one to the hit counter, and the second line causes it to DROP packets after it exceeds a threshold of 6 in 60 seconds. You also need an ACCEPT line for port 22 somewhere after it (assuming you're defaulting to dropping traffic that isn't explicitly allowed, which you should be doing).
A slightly better (more explicit and clear) way of writing it would be like this:
-A INPUT -p tcp -m tcp --dport 21 -m state --state NEW -m recent --set --name abusers --rsource
-A INPUT -p tcp -m tcp --dport 21 -m state --state NEW -m recent --update --seconds 180 --hitcount 6 --name abusers --rsource -j DROPHowever, an even easier option is to replace your IPTables ssh ACCEPT line with something like this:
-A INPUT -p tcp -m tcp --dport 22 -m state --state NEW -m limit --limit 1/min --limit-burst 4 -j ACCEPTThis says that you'll only ACCEPT the NEW ssh connection at a rate of 1 per minute per IP address, with an initial burst of 4 packets allowed (some implementations will send multiple SYN packets initially save time if a packet gets lost) at an initial burst.
I've found this does an excellent job of allowing SSH for legitimate use, while making brute force or dictionary attacks completely infeasible.
@Christopher:Thanks for an excellent explanation of how to solve the problem with only iptables. I'll definitely have to try it out as soon as I find some time.
it just picks lastb sorts it uniq;s it and runs iptable to drop packets. Thats lot easier and adaptive.
fail2ban uses Gamin.
How did you arrive at the conclusion that it was polling?
@Gamin: Well, if you're using Gamin (which is a subset of FAM) you're still waiting for an event that a file has been changed and then reading it, overall resulting in a pull instead of a push event flow. My method of using a syslog handler to trigger the event (and sending the data) is more push-like and might incur slightly less resources (not tested). The overall performance difference is probably negligible.Leave a comment
| « Config::Role - Object constructor parameters from file made easy | Implementing WWW::LastFM with XML::Rabbit - Part 5 » |



