Securing ssh with iptables

This is an old post. It may contain broken links and outdated information.

In the previous post, I discussed one possible method of keeping undesirables from connecting to your server via ssh: using the DenyHosts TCP wrapper to watch authentication attempts and block remote hosts based on conditions you set. DenyHosts (and other TCP wrappers) are easy to set up and don’t require much maintenance, but the block list files they generate can grow to a not-insignificant size; further, your web server must spend resources matching incoming ssh connection attempts against the block lists. If you’re on a particularly resource-constrained shared host, this might have some impact on overall server performance. Plus, even in its most recent update, DenyHosts can lag a bit in its blocking—because it uses regexes run against your server’s auth.log file to figure out what it needs to do, a remote attacker blasting out a tremendous number of logon attempts per second could get far above your allowed threshold of connection attempts in before DenyHosts drops the hammer.

There are lots of other things you can do to help secure your web server’s ssh port, but one of the most powerful and flexible is to bring iptables into the mix. Iptables is an applicaiton which comes preinstalled on most modern GNU/Linux distros and which provides instructions to the Linux kernel firewall. It is not a firewall in and of itself; rather, it provides a (relatively) easy way to view and modify the way the system’s built-in firewall tracks, filters, and transforms the network packets it receives.

In this particular use case, we care about iptables’s ability to perform actions on incoming ssh packets, based on parameters we define. Specifically, we’re going to use it to track all incoming ssh requests, and then block any host that tries to connect too many times. This is a simpler and more robust approach than the one DenyHosts takes, and the advantages are that it is self-maintaining and not dependent on log file parsing to work.

(Special thanks to my friend and mentor RB for passing along his feedback on the previous post and the instruction on how to get rolling with iptables!)

Looking at iptables

If you’ve never done any configuration with iptables before, the you almost certainly have no iptables rules defined. To check, run the following command and examine its output:

$ sudo iptables --list

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

In the example above, there are only the three default iptables chains defined, and no specific rules other than the default ACCEPT policy, meaning that if a packet matches none of the chain’s defined rules, it is accepted and allowed past the firewall. Since we haven’t set up any rules, right now iptables is doing nothing and letting all packets through.

If you were already using your web server’s firewall as its actual perimeter defense, you would likely want the chains’ policies to be default deny instead of accept; however, if you were already using the firewall like that it’s very likely you wouldn’t be reading this article because you’d already know what you’re doing.

Our goal here is to make iptables watch ssh traffic, which we’ll be receiving on TCP port 22, and if there are too many connection attempts within a certain period of time—I’m going to use one minute in this example, though you can use whatever interval makes you happy—then we want to log the host that’s trying to connect and then drop all of its packets. As soon as sixty seconds have elapsed between connection attempts, iptables will forget the remote host and it will be allowed to try to connect again.

To accomplish this, we need to add three rules to the INPUT chain, and we also need to create a new chain to handle the logging and dropping and then add a couple of rules to it as well. Additionally, we need a method of making the rules and chains persistent between reboots.

Appending INPUT rules

Iptables can be configured via the command line by running the iptables command (with root privileges) with the appropriate arguments. So, to add the first of our three rules to the INPUT chain, do this:

$ sudo iptables -A INPUT -p tcp -m tcp --dport 22 -m state --state NEW -m recent
    --set --name DEFAULT --rsource

The syntax is a little archaic, but the line tells iptables that you want to append a rule onto the existing INPUT chain. The -p tcp argument indicates that this rule will apply only to TCP packets. Most of the rest of the arguments rely on the -m option, which stands for match and tells iptables that the rule applies to packets which match the specific attributes we’re looking for. Here, the rule will be applied to packets that signal the start of new connections headed for TCP port 22. If a packet matches those attributes, iptables will note the remote host’s address in a temporary list.

The second rule will actually perform an action using a different chain, and in order to append it, we’ll need to first create the chain that it’s going to reference:

$ sudo iptables -N LOG_AND_DROP

We’ll build the necessary rules for this new chain in the next section. For now, here is the command for the second INPUT rule:

$ sudo iptables -A INPUT  -p tcp -m tcp --dport 22 -m state --state NEW -m recent
    --update --seconds 60 --hitcount 4 --name DEFAULT --rsource -j LOG_AND_DROP

This rule tells iptables to look for packets that match the previous rule’s parameters, and which also come from hosts already added to the watch list. If iptables sees a packet coming from such a host, it will update the “last seen” timestamp for that host. The --seconds 60 and --hitcount 4 arguments are used to narrow further the hosts we want to block—if a host tries to connect four or more times within sixty seconds, it matches that part of the rule and we jump (-j) to the LOG_AND_DROP chain. More on that momentarily.

The last rule we need to add to INPUT is this:

$ sudo iptables -A INPUT  -p tcp -m tcp --dport 22 -j ACCEPT

This tells iptables what to do with TCP traffic to port 22 which doesn’t match the previous rule. Strictly speaking, this rule isn’t necesssary with the INPUT chain set to policy ACCEPT, since all unmatched packets will be accepted anyway, but it’s good practice to have this line in here in case you ever want to modify the chain’s default policy to REJECT.

Appending LOG_AND_DROP rules

So we’ve got packets from hosts matching our parameters being directed to our newly-added LOG_AND_DROP chain, which doesn’t yet have any rules in it. We need to add some rules to tell iptables what to do with packets that get sent here—and as the name implies, we’re going to log and then drop them. First the logging:

$ sudo iptables -A LOG_AND_DROP -j LOG --log-prefix "iptables deny: " --log-level 7

We’re appending this rule to the LOG_AND_DROP table, and we use the -j (jump) operator to pass the packet’s information to the logging facility, causing a log entry to be added to /var/log/syslog with the packet’s information, which will include all kinds of useful stuff in it. The --log-prefix "iptables deny: " argument prepends “iptables deny:” to the log messages, which will make them easier to extract and sort, and the --log-level 7 argument ensures maximum verbosity in the logging.

That’s the log; now what about the drop? Simple:

$ sudo iptables -A LOG_AND_DROP -j DROP

After they are logged by the first rule, all packets are then dropped—that is, the packet is discarded silently by your server, without sending any error messages to the packet’s source.

That’s it for the iptables configuration, but there are two additional steps we can do to keep things nice, clean, and automated.

Automating iptables startup

With things set as they are right now, iptables will not retain the rules you’ve fed it if you reboot or power-cycle the server. There are several methods to make the rules sticky, but if you’re running a Debian-based GNU/Linux distro like Ubuntu, by far the easiest is to install the iptables-persistent package, which adds an init script for iptables that handles restoring its configuration on system startup from a saved file:

$ sudo aptitude install iptables-persistent

When iptables-persistent is first installed, it will ask if you want to dump the current iptables IPv4 and IPv6 rulesets to text files and use those files as your persistent rulesets. Say “yes”. If you need to do any further iptables modifications, make sure that you keep these rule files up to date—you can either dump your iptables rules manually by redirecting the iptables-save command’s output, or you can manually edit the rules file which iptables-persistent creates.

Logging iptables to a different location

Under Debian-based distros, iptables will do its logging to /var/log/syslog, a file which is already pretty crowded with stuff. You might want to have it log its denied connection attempts to a different file so that it’s easier for you to parse through your logs. Since most Debian-based distros are using rsyslogd for log automation, we can simply tell the daemon to kick all log entries that include our “iptables deny:” prefix to a different file.

To do this, make a new file named iptables.conf under /etc/rsyslog.d/, and add the following lines inside:

:msg,contains,"iptables deny: " /var/log/iptables.log
& ~

Now, any log entries containing “iptables deny:” will be written to /var/log/iptables.log. The second line (& ~) ensures that the lines will only be written to the new log file; without it, entries will be added to both syslog and the new file you define.

Lastly, since it’s likely this log file will grow if left alone, we’ll add a logrotate entry for it so that the system’s log rotation daemon automatically compresses and rotates it along with all the other log files. Create a file named iptables under /etc/logrotate.d/ and add these lines:

/var/log/iptables.log
{
    rotate 7
    daily
    missingok
    notifempty
    delaycompress
    compress
    postrotate
        invoke-rc.d rsyslog reload > /dev/null
    endscript
}

This tells the logrotate daemon to create a new iptables log file every day, and to compress and keep only the last seven days of log files.

Wrapping up

Done! Iptables is now able to watch incoming ssh connections and drop packets from hosts which try to connect too often in too short a period of time. Both the connection attempt threshold and the interval are modifiable to your needs, though the default values given here should be fine for most applications. Coupled with disallowing all but essential accounts to connect remotely (as described in the previous blog post), this will serve to deter almost any brute-force connection attempts, since there are plenty of easier targets out there. Remember the bit about the hobbit and the dragon!