Kromey’s Adventures

Just a nerd with aspirations to delusions of grandeur

Setting Up An iptables Firewall: Part 4

28 Jul 2016

In Part 1, we set up a very basic firewall. Part 2 added some basic additional protections to our server. Part 3 started to get more proactive against certain types of potential attacks. In this part, we’re going to introduce the recent module, and leverage it to make our firewall reactive to shield us from even more attacks.

Since the first thing we did in the previous post was to mitigate one type of potential attack, it makes sense to start with a relatively simple example that can mitigate another one, this one commonly called a “SYN flood”. We won’t go into what this entails (read the linked page which includes some great pictures to illustrate it), but instead launch straight into how we can mitigate it:

-A attacks -p tcp -m tcp --syn -m recent --name synflood --set
-A attacks -p tcp -m tcp --syn -m recent --name synflood --rcheck --seconds 1 --hitcount 60 -j DROP

We’re using our tcp module again, this time with the --syn argument; this means we’re looking at only those initial SYN packets that initiate a connection. We then load the recent module (-m recent) and specify the name of the list we want to use (--name synflood).

In the first rule, all we do is add this packet to the list (--set); for each named list, packets are listed by the address of the computer they came from. The second rule then checks for packets seen from that computer (--rcheck) and, if there have been 60 or more matching packets in the past second (--seconds 1 --hitcount 60), the packet is dropped (-j DROP). (Adjust this threshold for your needs: If you run a web server that serves pages with lots of little badges on it, for instance, you might need to increase it, whereas a low-traffic email server could probably get by with a lower threshold.)

In other words, we’ve established a maximum rate of 60 new connections per second, with anything in excess being dropped. Of course, when you say it like that, couldn’t this have been implemented with the limit module instead? While we technically could, look more closely: Where the limit module specifies a rate under which we act, these rules specify a rate over which we act. Since at this point in the processing (we’re in the attacks chain here) we don’t know yet if we want to accept a packet that’s within our threshold, we can’t use a simple limit rule with a -j ACCEPT target like we did with ICMP traffic; instead we monitor the rate and only act when it’s exceeded.

This is a relatively uninteresting application of the module, however. Let’s move on, and this time use it more creatively:

-N blacklist
-N bl_drop
-A blacklist -p tcp -m tcp --dport 21 -j bl_drop
-A blacklist -p tcp -m tcp --dport 23 -j bl_drop
-A blacklist -p tcp -m tcp --dport 25 -j bl_drop
-A blacklist -p tcp -m tcp --dport 139 -j bl_drop
-A blacklist -p tcp -m tcp --dport 3389 -j bl_drop
-A bl_drop -m recent --name blacklist --set -j DROP
-A INPUT -j blacklist

We’ve created a new chain called blacklist and immediately populated it with some new rules before adding it to the end of the INPUT chain; we’ve also created the bl_drop chain. Each of the rules in the blacklist chain will send attempts to contact an FTP service (--dport 21), a Telnet service (--dport 23), an SMTP service (--dport 25), a Windows File Sharing or Samba service (--dport 139), or a RDP service (--dport 3389) to our bl_drop chain, which in turn adds them to a new list we’ve named blacklist before dropping them. In essence what we’ve done is taken any attempt to contact these commonly-exploited services and flagged the sender as a “bad guy” we don’t want getting in to our server at all. By not doing any of this until the very end of the INPUT chain, we’ve accomplished two things:

  1. We won’t accidentally blacklist ourselves if we later decide to open one of these ports and try to use it

  2. We preserve processing resources by not bothering with blacklisting anything we’ve already blocked anyway, such as martians

However, we’re not actually doing anything with this list just yet, only adding addresses to it. Let’s act on our new blacklist now:

-I INPUT 5 -m recent --name blacklist --rcheck --seconds 3600 -j blacklist
-A blacklist -j DROP

We’ve inserted this rule above the attacks chain, and also added a new rule to the blacklist chain to DROP everything that comes through it. Now, any computer that tries to connect to any of our blacklisted ports will find themselves unable to connect to anything at all – even any of our opened services – for one hour (--seconds 3600). Additionally, since we run them back through our blacklist chain again, further attempts to connect to any of the blacklisted ports will, in fact, result in the timer resetting, and they will be unable to connect for another hour.

An alternative version of this rule could use --update in place of --rcheck (and then use -j DROP as the target instead). This version would immediately DROP any packets from blacklisted addresses, but it would also cause any packet from them to update the list, resetting the timer so they’re blacklisted for another hour. This could potentially cause a legitimate client that simply pointed the wrong software at your server to become locked out for a long time, however, but the chances of that are probably quite slim. (Besides, if it becomes a problem with legitimate users accidentally pointing e.g. Telnet clients at your server, you could always just remove that rule from your blacklist chain instead.)

A third alternative could continue to use --rcheck, but swap out the target to -j DROP. This would again result in dropping packets from blacklisted addresses immediately (less processing is a great boon if you’re actively under attack), however it also means that regardless of what those potential attackers have done since being initially blacklisted, they will once again be able to connect to your open services after that first hour is up.

In any case, there’s one more thing we can do with our new blacklisting mechanism here: We can replace the targets on each rule in our attacks chain with -j bl_drop, immediately blacklisting any address that we notice any of those attacks coming from. I’ll leave crafting the appropriate commands to replace those targets as an exercise to the reader, however.

One thing to be aware of with the blacklisting we’ve done here: A knowledgeable attacker could craft packets with forged headers that make them appear to come from legitimate users of your services, but targeted at your blacklisted ports; the result would be a denial of service for your legitimate users, who now find themselves blacklisted by your firewall through no fault of their own. While we might be able to mitigate this with use of the --hitcount argument, ultimately if you suspect this is happening the only solution is to either whitelist your legitimate users, or else drop the blacklisting altogether.

One more thing to do and then we’re done here:

-I INPUT 6 -m recent --name blacklist --remove

This rule, added immediately after our blacklisting rule, becomes a sort of “garbage collector”: The first time an address we’d previously blacklisted connects after they’ve served their time, we remove their address from our list altogether. Of course, if they’re still being bad we’ll end up immediately re-adding them, but if the address no longer belongs to a “bad guy”, or never did and was simply a legitimate user making a mistake, it reduces the resources your server requires to keep track of these lists.

A rule this simple combined with --hitcount on your blacklist itself, however, won’t work, because it means that before anyone can reach that threshold you’ve already taken them off the list. No one will ever be blacklisted. You could in this case add e.g. --seconds 86400 to this rule, requiring that you’ve not seen them do anything “bad” for a full day before you remove them.

Remember to save your firewall rules (dpkg-reconfigure iptables-persistent); with everything we’ve done up to this point, your rules file should now look like this (NB: I’ve added some additional comments to some rules here):

*filter
:INPUT DROP [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:attacks - [0:0]
:blacklist - [0:0]
:bl_drop - [0:0]
:icmp - [0:0]
:martians - [0:0]
:services - [0:0]
-A INPUT -p icmp -j icmp
-A INPUT -i lo -m comment --comment "Free reign for loopback" -j ACCEPT
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -m state --state INVALID -j DROP
-A INPUT -m recent --name blacklist --rcheck --seconds 3600 -j blacklist
-A INPUT -m recent --name blacklist --remove
-A INPUT -m comment --comment "Handle common attacks" -j attacks
-A INPUT -m comment --comment "Filter martians" -j martians
-A INPUT -m comment --comment "Open service ports" -j services
-A INPUT -j blacklist
-A attacks -p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG NONE -m comment --comment "NULL packets" -j bl_drop
-A attacks -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m comment --comment "SYN flag checking" -j bl_drop
-A attacks -p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG FIN,SYN,RST,PSH,ACK,URG -m comment --comment "XMAS packets" -j bl_drop
-A attacks -p tcp -m tcp --syn -m recent --name synflood --set
-A attacks -p tcp -m tcp --syn -m recent --name synflood --rcheck --seconds 1 --hitcount 60 -j bl_drop
-A blacklist -p tcp -m tcp --dport 21 -m comment --comment "FTP" -j bl_drop
-A blacklist -p tcp -m tcp --dport 23 -m comment --comment "Telnet" -j bl_drop
-A blacklist -p tcp -m tcp --dport 25 -m comment --comment "SMTP" -j bl_drop
-A blacklist -p tcp -m tcp --dport 139 -m comment --comment "SMB" -j bl_drop
-A blacklist -p tcp -m tcp --dport 3389 -m comment --comment "RDP" -j bl_drop
-A blacklist -j DROP
-A bl_drop -m recent --name blacklist --set -m comment --comment "Blacklist the source" -j DROP
-A icmp -m limit --limit 1/s --limit-burst 4 -j ACCEPT
-A icmp -j DROP
-A martians --source 0.0.0.0/8 -m comment --comment "'This' network" -j DROP
-A martians --source 10.0.0.0/8 -m comment --comment "Private-use networks" -j DROP
-A martians --source 100.64.0.0/10 -m comment --comment "Carrier-grade NAT" -j DROP
-A martians --source 127.0.0.0/8 -m comment --comment "Loopback" -j DROP
-A martians --source 169.254.0.0/16 -m comment --comment "Link local" -j DROP
-A martians --source 172.16.0.0/12 -m comment --comment "Private-use networks" -j DROP
-A martians --source 192.0.0.0/24 -m comment --comment "IETF protocol assignments" -j DROP
-A martians --source 192.0.2.0/24 -m comment --comment "TEST-NET-1" -j DROP
-A martians --source 192.168.0.0/16 -m comment --comment "Private-use networks" -j DROP
-A martians --source 198.18.0.0/15 -m comment --comment "Network interconnect device benchmark testing" -j DROP
-A martians --source 198.51.100.0/24 -m comment --comment "TEST-NET-2" -j DROP
-A martians --source 203.0.113.0/24 -m comment --comment "TEST-NET-3" -j DROP
-A martians --source 224.0.0.0/4 -m comment --comment "Multicast" -j DROP
-A martians --source 240.0.0.0/4 -m comment --comment "Reserved for future use" -j DROP
-A martians --source 255.255.255.255/32 -m comment --comment "Limited broadcast" -j DROP
-A services -p tcp -m tcp --dport 22 -m limit --limit 1/minute -m comment --comment "Rate-limited SSH" -j ACCEPT
-A services -p tcp -m tcp --dport 80 -m comment --comment "HTTP" -j ACCEPT
-A services -p tcp -m tcp --dport 443 -m comment --comment "HTTPS" -j ACCEPT
COMMIT

Congratulations! You now have a very solid firewall configuration. There’s still more room for improvement, of course, and in the next part we’ll add port knocking to our setup!