Alt Text

The problem

If you run your own VPS, dedicated or home linux server you probably know the annoyance of automatic ssh bruteforce attempts, have a look in your logs ( eg: /var/log/auth.log or /var/log/secure.log ) you will probably found a load of brute force attempts on ssh going over and over:

Feb 20 08:11:35 akuma sshd[4029]: Failed password for invalid user fluffy from 181.196.57.117 port 49798 ssh2
Feb 20 08:11:35 akuma sshd[4029]: Connection closed by 181.196.57.117 [preauth]
Feb 20 09:28:24 akuma sshd[4128]: Failed password for root from 61.136.171.198 port 57823 ssh2
Feb 20 09:28:24 akuma sshd[4128]: Received disconnect from 61.136.171.198: 11: Bye Bye [preauth]
Feb 21 04:35:45 akuma sshd[5900]: Failed password for root from 125.65.165.235 port 54543 ssh2
Feb 21 04:35:45 akuma sshd[5900]: Received disconnect from 125.65.165.235: 11: Bye Bye [preauth]
Feb 21 05:34:17 akuma sshd[5984]: Connection closed by 81.218.70.216 [preauth]
Feb 21 05:48:36 akuma sshd[5999]: Invalid user a from 101.227.170.42
Feb 21 07:09:16 akuma sshd[6113]: input_userauth_request: invalid user aditza [preauth]
Feb 21 07:09:16 akuma sshd[6113]: Failed password for invalid user aditza from 210.21.110.58 port 39674 ssh2
Feb 21 07:09:17 akuma sshd[6113]: Received disconnect from 210.21.110.58: 11: Bye Bye [preauth]
Feb 21 23:30:00 akuma sshd[1531]: Did not receive identification string from 108.174.147.29
Feb 21 23:37:48 akuma sshd[1544]: Failed password for root from 108.174.147.29 port 35588 ssh2
Feb 21 23:37:48 akuma sshd[1544]: Received disconnect from 108.174.147.29: 11: Bye Bye [preauth]
Feb 22 00:22:49 akuma sshd[1613]: reverse mapping checking getaddrinfo for 179.89.26.218.internet.sx.cn [218.26.89.179] failed - POSSIBLE BREAK-IN ATTEMPT!
Feb 22 00:22:49 akuma sshd[1613]: Failed password for root from 218.26.89.179 port 34896 ssh2
Feb 22 00:22:49 akuma sshd[1613]: Connection closed by 218.26.89.179 [preauth]
Feb 22 07:06:57 akuma sshd[2241]: Failed password for root from 199.71.214.66 port 57057 ssh2
Feb 22 07:06:57 akuma sshd[2241]: Received disconnect from 199.71.214.66: 11: Bye Bye [preauth]
Feb 22 10:16:44 akuma sshd[2501]: Failed password for root from 91.192.132.140 port 51344 ssh2
Feb 22 10:16:45 akuma sshd[2501]: Received disconnect from 91.192.132.140: 11: Bye Bye [preauth]
Feb 22 13:42:50 akuma sshd[2805]: reverse mapping checking getaddrinfo for 158.179.178.222.cq.cq.cta.net.cn [222.178.179.158] failed - POSSIBLE BREAK-IN ATTEMPT!
Feb 22 13:42:50 akuma sshd[2805]: Invalid user a from 222.178.179.158
Feb 22 13:42:50 akuma sshd[2805]: input_userauth_request: invalid user a [preauth]
Feb 22 13:42:50 akuma sshd[2805]: Failed password for invalid user a from 222.178.179.158 port 47986 ssh2
Feb 22 13:42:51 akuma sshd[2805]: Received disconnect from 222.178.179.158: 11: Bye Bye [preauth]

This is bad for several and obvious reasons that i will not discuss here, so how to block effectively this waste of resources and mitigate the problem?

A simple Netfilter/iptables approach

Since i don't want to maintain another service and introduce unnecessary overhead (yes i'm talking to you: denyhosts and fail2ban) i will use netfilter/iptables in order to slow down these attacks at the point they become ineffective implementing this simple policy:

  • Keep track of every single new ssh connection attempt.
  • Block source IP addresses attempting more than 3 ssh connections per minute (60 seconds).
  • Block source IP addresses attempting more than 5 ssh connections in 10 minutes (600 seconds).

Implementing this behaviour using iptables is quite simple, assuming an home server with the ssh service reachable both via lan and the internet (with a single network interface) you can use this simple ruleset:

iptables -I INPUT 1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 ! -s 192.168.1.0/24 -m conntrack --ctstate NEW -m recent --set --name SSH
iptables -A INPUT -p tcp --dport 22 ! -s 192.168.1.0/24 -m conntrack --ctstate NEW -m recent --update --seconds 60 --hitcount 2 --rttl --name SSH -j DROP
iptables -A INPUT -p tcp --dport 22 ! -s 192.168.1.0/24 -m conntrack --ctstate NEW -m recent --update --seconds 600 --hitcount 4 --rttl --name SSH -j DROP

Let's break the above ruleset line by line:

  1. This rule is needed in order to operate in a stateful firewall approach, incoming traffic with a related or established state will be accepted.
  2. With this rule we are going to create a new list named SSH out of people attempting to establish new ssh connections (keep track of every new ssh connection attempt).
  3. For every new incoming ssh connection: check the hitcount (using the previously created SSH list) if the are more than 2 new login attempts in 60 seconds drop the connection then update last seen timestamp.
  4. Block it more! this rule acts as the previous but for 5 attempts in 600 seconds, feel free to add other rules matching a larger time window and hitcount as needed.

Note the ! -s 192.168.1.0/24 in our example rules (this is our lan network space), this is needed because we don't want to apply this policy to incoming ssh connections coming LAN side but only WAN side.If your machine has a dedicated internet facing interface you can use that instead to only match WAN connections (eg: specify the interface with -i eth1replacing the source address statement).

Let's test it

Apply this ruleset and wait a bit, hours or days it depends, and check what's going on (look for the rule hit counters with a verbose rule listing), you will see something like that in a couple of days:

~ # iptables -L -nv
Chain INPUT (policy ACCEPT 811 packets, 75409 bytes)
 pkts bytes target     prot opt in     out     source               destination           
 12M   17G ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
 33  1784            tcp  --  *      *      !192.168.1.0/24       0.0.0.0/0            tcp dpt:22 ctstate NEW recent: SET name: SSH side: source   
 16   912 DROP       tcp  --  *      *      !192.168.1.0/24       0.0.0.0/0            tcp dpt:22 ctstate NEW recent: UPDATE seconds: 60 hit_count: 2 TTL-Match name: SSH side: source   
  0     0 DROP       tcp  --  *      *      !192.168.1.0/24       0.0.0.0/0            tcp dpt:22 ctstate NEW recent: UPDATE seconds: 600 hit_count: 4 TTL-Match name: SSH side: source

Good, the ruleset worked and there are already dropped connection attempts, to see what's going in detail you can inspect the SSH list too by using: cat /proc/net/xt_recent/SSH 

src=218.7.37.194 ttl: 112 last_seen: 116701 oldest_pkt: 1 116701
src=76.76.105.218 ttl: 106 last_seen: 18887 oldest_pkt: 1 18887
src=89.107.155.11 ttl: 114 last_seen: 139381 oldest_pkt: 1 139381

You can also manipulate the SSH list manually, for example to get rid of the first entry in the list use the command: echo -218.7.37.194 > /proc/net/xt_recent/SSH

Fine Tuning our setup, avoid self-bans

Let's assume you are connecting remotely, in this scenario you'll want to avoid self-bans when you are firing up multiple parallel sessions, using a simple sshrc script you will be able to automatically popout your source address from the SSH list after every successful login (the sshrc command is started just before the user shell, see: man ssh). An example sshrc script for user root would be:

## Reset SSH conntrack rate limiting once logged.
echo $SSH_CLIENT | awk '{printf "-%s\n", $1}' > /proc/net/xt_recent/SSH

Are we done here?

Nope, this simple and quick recipe will only mitigate some kind of ssh attacks dropping their rate (for example it will not block slow rate attacks), so this is not a definitive solution but a far-from-perfect workaround that must be used in conjunction with other solutions like a proper (ssh) hardening setup.

Comments