Host-based firewalls are an important component in the security practitioners tool chain. Layering security mechanisms for a defense-in-depth stance provides redundancy to protect valuable assets. As I was reminded the other day, threat vectors connect threats to vulnerabilities, but ultimately exploiting a vulnerability is about access to, or destruction of an asset. Patching for known vulnerabilities is part of the constant vigilance required, but unknown vulnerabilities still remain. When one up-stream security mechanism fails, or is bypassed by some means, another should still remain behind it so that reaching the asset is more difficult than defeating a single security mechanism.
Of course, no network-based security measure protects assets behind application software — but this is another topic, for another day. Firewalls limit the attack surface to specific services and applications and may also help prevent some kinds of abuses.
The Linux host-based firewall, iptables, has a wealth of modules for various purposes. One particularly useful one is the limits module. Let’s explore how this can be used to protect against some basic attacks.
Syntax and Example
First, a quote (with annotations in blue) from the Linux IP filtering HowTo:
- This module must be explicitly specified with `-m limit’ or `–match limit’. It is used to restrict the rate of matches, such as for suppressing log messages. It will only match a given number of times per second (by default 3 matches per hour, with a burst of 5). It takes two optional arguments:
- followed by a number; specifies the maximum average number of matches to allow per second. The number can specify units explicitly, using `/second’, `/minute’, `/hour’ or `/day’, or parts of them (so `5/second’ is the same as `5/s’).
- followed by a number, indicating the maximum burst before the above limit kicks in.
This match can often be used with the LOG target to do rate-limited logging. To understand how it works, let’s look at the following rule, which logs packets with the default limit parameters:
# iptables -A FORWARD -m limit -j LOG
The first time this rule is reached, the packet will be logged; in fact, since the default burst is 5, the first five packets will be logged. After this, it will be twenty minutes before a packet will be logged from this rule (again, the default –limit is three per hour), regardless of how many packets reach it. Also, every twenty minutes which passes without matching a packet, one of the burst will be regained; if no packets hit the rule for 100 minutes, the burst will be fully recharged; back where we started.
Note: you cannot currently create a rule with a recharge time greater than about 59 hours, so if you set an average rate of one per day, then your burst rate must be less than 3.
You can also use this module to avoid various denial of service attacks (DoS) with a faster rate to increase responsiveness.
# iptables -A FORWARD -p tcp --syn -m limit --limit 1/s -j ACCEPT
Furtive port scanner:
# iptables -A FORWARD -p tcp --tcp-flags SYN,ACK,FIN,RST RST \
-m limit --limit 1/s -j ACCEPT
Ping of death:
# iptables -A FORWARD -p icmp --icmp-type echo-request -m limit \
--limit 1/s -j ACCEPT
This module works like a “hysteresis door”, as shown in the graph below.
rate (pkt/s) ^ .---. | / DoS | / Edge of DoS -|.....:................................ = (limit * | /: limit-burst) | / : .-. | / : / | / : / End of DoS -|/....:..............:.../.........../. = limit | : :`-' `--' -------------+-----+--------------+------------------> time (s) LOGIC => Match | Didn't Match | Match
Say we say match one packet per second with a five packet burst, but packets start coming in at four per second, for three seconds, then start again in another three seconds.
<--Flood 1--> <---Flood 2---> Total ^ Line __-- YNNN Packets| Rate __-- YNNN | mum __-- YNNN 10 | Maxi __-- Y | __-- Y | __-- Y | __-- YNNN |- YNNN 5 | Y | Y Key: Y -> Matched Rule | Y N -> Didn't Match Rule | Y |Y 0 +--------------------------------------------------> Time (secs) 0 1 2 3 4 5 6 7 8 9 10 11 12
You can see that the first five packets are allowed to exceed the one packet per second, then the limiting kicks in. If there is a pause, another burst is allowed but not past the maximum rate set by the rule (1 packet per second after the burst is used).
While the above examples are technically correct, I prefer to explicitly specify the limit and burst rates, rather than relying on default values. One important concept to understand is rule matching. All iptables rules are examined in order, and the first match wins. However, a limits rule does not act as a match! You might have a limits rule, followed by one that accepts packets. This can be confusing. If you limit packets, it should be followed by a rule that drops (or logs, followed by rules that drop) additional packets of the same “type“. This can be implicit, i.e. you use a policy which drops packets not processed by matching rules.
One thing of note regarding the following examples… there are many ways to organize the rule chains. I prefer to denote them by the direction of flow, but you may organize them anyway that makes sense to you. Also, I like to keep the number of rules on the default chains (e.g. INPUT) as small as possible, and shunt categories of traffic that might be acceptable to custom rule chains, so that non-qualifying traffic quickly follows the policy, which is set to drop. The flow for the example INPUT chain (the chain for local traffic is not shown) is as follows:
The custom chains are in the dashed-line box in orange. All traffic eventually falls into one of two buckets — drop or accept.
Now let’s look at a very practical example. Let’s say that you have a mail server that should only accept TCP traffic for the SSH and SMTP protocols, along with ICMP echo requests. Additionally, you’d like to protect it against TCP SYN floods, inordinate amounts of incoming UDP traffic (if you allow any), and limit rates for ICMP traffic. We’ll put together a script you can use during system startup — you could include iptables rules as part of network startup, but I prefer a separate “service” because you can start/stop it for testing, making changes, and debugging any issues. You can retrieve the entire script here.
First we set up some variables to make our script easier to understand (notice the options available for specifying single hosts and subnets; single hosts can be specified with or without CIDR notation). Most match parts of our fictitious network, but others match broadcast or multicast address spaces. Our mail server is a gateway to an internal Exchange server.
Now the standard case statement for start/stop/restart arguments to the script.
case "$1" in
Now we flush any existing rules and delete all chains.
echo "Setting iptables policies..."
# Flush old rules, delete chains
A drop policy will force all packets not explicitly matched by a rule to be dropped.
# Construct the default policy -- drop everything
echo " Setting policies..."
$IPT -P INPUT DROP
$IPT -P OUTPUT DROP
$IPT -P FORWARD DROP
We establish five chains: one each for local traffic, stateful matches, inbound traffic, outbound traffic, and a special one for logging anything of interest:
$IPT -N local
$IPT -N stateful
$IPT -N inbound
$IPT -N outbound
$IPT -N log
Now we set up stateful inspection for TCP and UDP. We then add rules to the INPUT chain to jump here when we find appropriate traffic. If traffic does not match an existing flow, we return to the INPUT chain for further processing.
echo " Setting stateful inspection..."
$IPT -A stateful -p tcp -m state --state ESTABLISHED,RELATED -j ACCEPT
$IPT -A stateful -p udp -m state --state ESTABLISHED,RELATED -j ACCEPT
$IPT -A INPUT -p tcp -j stateful
$IPT -A INPUT -p udp -j stateful
$IPT -A OUTPUT -p tcp -j stateful
$IPT -A OUTPUT -p udp -j stateful
Now we add our rate limiting rules, first for TCP SYN packets, and then for new UDP flows. If the prescribed limits are exceeded, subsequent packets are logged and dropped. Of course, further on we’ll rate limit the logging too. Again, our order is 1) limit, 2) log, 3) drop.
# Inbound traffic to accept
echo " Setting rules for inbound traffic..."
# Rate limiting rules:
# Set rates with burst for new conns, then log/drop the excess
$IPT -A inbound -p tcp --syn -m limit --limit 4/s --limit-burst 10 -j ACCEPT
$IPT -A inbound -p tcp --syn -j LOG
$IPT -A inbound -p tcp --syn -j DROP
$IPT -A inbound -p udp -m state --state NEW -m limit \
--limit 1/s --limit-burst 5 -j ACCEPT
$IPT -A inbound -p udp -m state --state NEW -j LOG
$IPT -A inbound -p udp -m state --state NEW -j DROP
$IPT -A inbound -p tcp --dport 25 -j ACCEPT
$IPT -A inbound -p tcp -s $lan --dport 22 -j ACCEPT
$IPT -A inbound -p icmp --icmp-type 8 -m limit \
--limit 1/s --limit-burst 5 -j ACCEPT
$IPT -A inbound -p icmp --icmp-type 11 -m limit \
--limit 1/s --limit-burst 5 -j ACCEPT
We now add the appropriate rules to the INPUT chain to jump the inbound chain as appropriate.
$IPT -A INPUT -p tcp --dport 25 -j inbound
$IPT -A INPUT -p tcp -s $lan -j inbound
$IPT -A INPUT -p icmp -j inbound
# Drop these to avoid logging them $IPT -A INPUT -p udp -d $bcast1 -j DROP $IPT -A INPUT -p udp -d $bcast2 --dport 67 -j DROP $IPT -A INPUT -d $mcast -j DROP
Notice that since we’re not actually accepting any UDP traffic, we don’t actually add a rule to jump to the inbound chain. If we do in the future, we can add the INPUT chain rule, and appropriate inbound chain rules; we have the rate limiting rules already in place. Also notice that we chose to simply drop some kinds of broadcast traffic to avoid logging these expected packets.
Now let’s look at the logging rules (the rest of the script is left to the reader to work through and understand):
# Log all dropped packets
echo " Setting logging rules..."
$IPT -A log -j LOG -m limit --limit 4/minute --limit-burst 3 \
--log-level info --log-prefix "IPTABLES: "
$IPT -A log -j DROP
$IPT -A INPUT -j log
$IPT -A OUTPUT -j log
We’ve limited logging to four entries per minute, but with a burst of 3. Again, if we consume the burst limit, we gain back one entry every 15 seconds. If nothing is logged for 45 seconds, the burst is fully re-charged.
Adding a host-based firewall adds to a defense-in-depth stance. It does add complexity (you will have the similar rules in several places, making maintenance, testing, and debugging more difficult), but if your risk profile warrants it, using a firewall like iptables is helpful.