Defending against SSH Brute force attacks

The Problem


The problem is malicious people trying dictionary attacks against an SSH server.

We can detect the attack in the /var/log/secure file, for instance:


Apr 5 18:09:54 testguest sshd[1220]: Invalid user andres from ::ffff:218.54.172.190
Apr 5 18:09:57 testguest sshd[1220]: Failed password for invalid user andres from ::ffff:218.54.172.190 port 2446 ssh2
Apr 5 18:10:00 testguest sshd[1222]: Invalid user barbara from ::ffff:218.54.172.190
Apr 5 18:10:02 testguest sshd[1222]: Failed password for invalid user barbara from ::ffff:218.54.172.190 port 2583 ssh2
Apr 5 18:10:06 testguest sshd[1224]: Invalid user adine from ::ffff:218.54.172.190
Apr 5 18:10:08 testguest sshd[1224]: Failed password for invalid user adine from ::ffff:218.54.172.190 port 2720 ssh2
Apr 5 18:10:11 testguest sshd[1226]: Invalid user test from ::ffff:218.54.172.190
Apr 5 18:10:13 testguest sshd[1226]: Failed password for invalid user test from ::ffff:218.54.172.190 port 2860 ssh2
Apr 5 18:10:16 testguest sshd[1228]: Invalid user guest from ::ffff:218.54.172.190
Apr 5 18:10:19 testguest sshd[1228]: Failed password for invalid user guest from ::ffff:218.54.172.190 port 3001 ssh2

Summary / Best Bang for your Buck

  • Change the port sshd is listening to. Edit the sshd config file (/etc/ssh/sshd_config for instance) and change the value of Port to something other than 22 and restart the sshd server (/etc/init.d/sshd restart). This will reduce dramatically the number of attacks you'll get.

  • Use strong passwords for all your Linux users (long, mixing lower and upper case, symbols and numbers) or use ssh keys that will make the whole ssh attack pointless (but you have the keys to manage now)

Let's take a look at several approaches to protect ourselves from these attacks.

Somebody must have written a script about this


Yes, there are several scripts out there, like Tattle - Automatic Reporting Of SSH Brute-Force Attacks (Perl), sshd_sentry (Perl) and DenyHosts (Phyton)

Banning guilty IP addresses


Another option is to ban the ip address from the attackers that are recorded in the previous log by using iptables.

So here's a little shell script I wrote as an exercise that will parse the /var/log/secure file for the guilty IP addresses and then will reject any incoming packets from them by inserting a drop rule in the INPUT chain before its two last rules of our current iptables (the one to accept incoming SSH connections and the last default deny all). To check the before and after effect: # iptables -L -n


# Fernando Duran, August 2004
# Ban ssh brute-forcing IP addresses with iptables

#!/bin/bash
# delete old ip bans, restart iptables
# /etc/init.d/iptables restart
/sbin/service iptables restart

# bad login lines
ssh_line=/tmp/ssh_line
touch $ssh_line

# all ip addresses, duplicated after each script execution
all_ip=/tmp/blacklist
touch $all_ip

# intermediary merge (old + new ips) file
merge_ip=/tmp/blackmerge
touch $merge_ip

# list of unique banned ip addresses
black_ip=/var/log/blacklist
touch $black_ip

# extract guilty ip addresses (Red Hat, other distros may vary the format)
grep sshd /var/log/secure | grep Failed | grep invalid > $ssh_line
cut -d : -f 7 $ssh_line | cut -d \ -f 1 | sort | uniq > $all_ip

# get rid of duplicates
echo -n "" > $merge_ip
cat $all_ip | sort | uniq > $merge_ip
cat $black_ip | sort | uniq >> $merge_ip
cat $merge_ip | sort | uniq > $black_ip

# insert all unique ips into iptables
lines=`/sbin/iptables -L INPUT -n | wc -l`
rules=`expr $lines - 3`
file=`cat $black_ip`
for ip in $file; do
/sbin/iptables -I INPUT $rules -s $ip -j DROP
#echo " $ip banned"
done

The way to restart iptables and the format of /var/log/secure will have to be adapted to your Linux distro.
The script is meant to be ran periodically in order to add new bad IPs to our black list, for example we can run it as a cron job every hour for instance.

The problem is that the script doesn't solve much. The attacker can try hundreds of user/passwords until the script is fired, and we'd be placing more burden on the system if we'd run it frequently, like every minute or so.

Limiting the rate of connections


Netfilter's iptables is very powerful, and one of its features allows us to mitigate brute force attacks and DoS attacks by limiting the rate of packets matching a rule.

So for instance we can append these rules:


/sbin/iptables -A INPUT -p tcp --dport 22 --syn -m limit --limit 1/m --limit-burst 2 -j ACCEPT
/sbin/iptables -A INPUT -p tcp --dport 22 --syn -j DROP

The effect is that the firewall will only admit two (for example) incoming new SSH connections per minute (it will reset the "burst" counter every minute).

So this will be enough to discorage those idiots right?
No, actually it may make things worse because we can lock ourselves out if we want to log in with SSH and at the same time there's an attack going on; then iptables will be preventing any incoming SSH connection (including ours).

If the previous shell script to ban ip addresses is used then this problem is less severe since we'll "only" have to wait for the cron job to run the script and ban the bad ip address so that later iptables can let us in. Another thing we can do is to place before those rules one that will grant SSH access to our (known) ip.

Using SSH keys


A way to get rid once and for all of the SSH brute force attacks is to allow SSH logins only with encryption keys. The price to pay is that we have to create the keys first (not a problem unless we have many users) and we have to take care of the private keys (have secure copies of them).

Basically we have to create a pair of public and private key in the client computer and copy the public key in the server.

- Linux:


# ssh-keygen -t rsa

(We can also use dsa encryption instead of dsa)
If we enter a passphrase then we'll have to enter it every time that we wan to connect to the server using SSH.

The private key is generated into $HOME/.ssh/id_rsa, and the public key into $HOME/.ssh/id_rsa.pub
It's the public key that we have to append to the server's .ssh/authorized_keys file, located in the home directory of the user we want to authenticate.

- In Windows, if we are using Putty we can use puttygen to create the pair, then save the private key to the local computer (we can protect the file with a passphrase) and similarly copy the public key to the same file in the server. The formats of the keys in Putty are different that the ones generated with OpenSSH, but if we already have an OpenSSH one puttygen can convert it to its format.

At the server, if there's no .ssh or authorized_keys file for that user, we can create them when running as that user:


$ cd ~
$ mkdir .ssh
$ touch .ssh/authorized_keys
$ chmod 600 .ssh/authorized_keys

After testing that we can log in with the keys, we can change the SSH server configuration file so that sshd will only accept keys as form of authentication.

Edit /etc/ssh/sshd_config and set:

PasswordAuthentication no

It's also a good idea to restrict the accepted version of SSH:

Protocol 2

If we use the "no password" setting it's very important to keep one or more copies of our private keys in removable media like a flash USB thumb drive or similar, so we can access the server in case we have not access the hard drive of the client computer.

After the changes the SSH server has to be restarted:


# service sshd restart
(or # /etc/init.d/sshd restart in Debian System-V style)