“tight” iptables firewall for servers

This page describes and documents the “tight” policy stance applied in the iptables script iptables.tight.sh discussed in my post of 9 September 2012. Its sister, loose, policy is described and documented here.

Note that we policy drop on all three chains (INPUT, OUTPUT and FORWARD) in the filter table. This means that we must specify allowed outbound connections as well as allowed inbound. This is safer than allowing a default outbound to anywhere, but inevitably is more complex since we must specify exact rules for each outbound connection (and its return). Note also that I add a (redundant) set of “DROP” directives at the end of this script. You do not need to do this, but I find it helps me when reading the output of “iptables -nvL” because it then expicitly lists the drops at the end of the rules. (I can’t shake old Cisco IOS habits…..)

First some defines. This allows us to vary the policy applied by the script without editing the body of the script itself.

IPTABLES=/sbin/iptables # the iptables program itself
OURIP=”x.x.x.x” # Our local IP address (on eth0 for example)
SSHIPS=”x.x.x.x y.y.y.y” # ip address(es) allowed to SSH in to this server.
DNSIPS=”x.x.x.x y.y.y.y” # address(es) of our DNS servers
NTPIPS=”x.x.x.x y.y.y.y z.z.z.z” # allowed NTP server(s)
MAILOUT=”x.x.x.x” # address of our mail relay if we want to send mail
MAILIN=”x.x.x.x” # server(s) allowed to connect to us if we are a “smart” mail relay
OTHERIP=”x.x.x.x” # address of another server we wish to connect to (e.g. via ssh)
REPOIPS=”x.x.x.x y.y.y.y” # address(es) of our distro update repository (or mirror)
WEBALLOWED=”x.x.x.x y.y.y.y” # address(es) of webservers we must connect to (e.g. akismet)
SERVICES=”80 443″ # defines the services we allow – here only http and https
SSH=”122″ # define our (non standard) ssh port

First flush all rules from all chains in the filter table and zero the packet and byte counters

$IPTABLES -F
$IPTABLES -Z

Set the default policy on all chains. We use DROP rather than REJECT. This can be a religious decision. Strict adherence to standards implies we should “REJECT” with a helpful ICMP error message so that unwanted connections do not simply hang or timeout for odd reasons. However, doing so can mean that incoming packets with a spoofed source address can get replies sent to that source address when they are not expecting them. DDOS bots exploit this behaviour. I’d rather break standards than help a DDOS bot.

This policy is implemented where no match is otherwise made.

$IPTABLES -P INPUT DROP
$IPTABLES -P OUTPUT DROP
$IPTABLES -P FORWARD DROP

Some people advocate logging dropped packets. I don’t. Unless you have a very large (separate) partition for /var you can very rapidly run out of logspace – particularly if your server is deliberately targeted. This can result in a self-imposed denial of service as your root partition fills up.

Now the rules:

Anti spoof rules to drop all RFC1918 addresses, martian networks, multicasts, all ones and all zeros etc. This should not really be necessary because the ISP should apply these filters at the border routers. But just in case …… (and I routinely see lots of broadcast traffic).

Note however, that if you are running a VPN endpoint (such as openvpn) on your server, you will need to modify these rules otherwise your VPN access will be blocked.

$IPTABLES -A INPUT -s 10.0.0.0/8 -j DROP
$IPTABLES -A INPUT -s 172.16.0.0/12 -j DROP
$IPTABLES -A INPUT -s 192.168.0.0/16 -j DROP
$IPTABLES -A INPUT -s 169.254.0.0/16 -j DROP
$IPTABLES -A INPUT -s 224.0.0.0/4 -j DROP
$IPTABLES -A INPUT -d 224.0.0.0/4 -j DROP
$IPTABLES -A INPUT -s 240.0.0.0/5 -j DROP
$IPTABLES -A INPUT -d 240.0.0.0/5 -j DROP
$IPTABLES -A INPUT -s 0.0.0.0/8 -j DROP
$IPTABLES -A INPUT -d 0.0.0.0/8 -j DROP
$IPTABLES -A INPUT -s 1.1.1.1 -j DROP
$IPTABLES -A INPUT -d 1.1.1.1 -j DROP
$IPTABLES -A INPUT -d 239.255.255.0/24 -j DROP
$IPTABLES -A INPUT -d 255.255.255.255 -j DROP

Drop invalid packets. There is some dispute as to whether this is still necessary. Most “odd” tcp flag combinations should be handled correctly in modern kernel implementations. However, I leave this in because in my experience, my servers get hit by such traffic. Try it yourself. Leave the rules in place for a few days and then take a look at the packet counts against those rules.

$IPTABLES -A INPUT -m state –state INVALID -j DROP
$IPTABLES -A FORWARD -m state –state INVALID -j DROP
$IPTABLES -A OUTPUT -m state –state INVALID -j DROP

localhost connections are always allowed (failure to allow this will break many programs which rely on localhost)

$IPTABLES -A INPUT -i lo -j ACCEPT
$IPTABLES -A OUTPUT -o lo -j ACCEPT

Allow DNS queries to our trusted servers. Both TCP and UDP are required. And, yes, I know UDP is connectionless. I know that the concept of an ESTABLISHED connection is therefore questionable, but the syntax allows this and I find it useful. If the formulation offends your sense of decency, then by all means change it.

for SERVERS in $DNSIPS ;
do
$IPTABLES -A OUTPUT -s $OURIP -p udp -m udp -d $SERVERS –dport 53 -m state –state NEW,ESTABLISHED -j ACCEPT
$IPTABLES -A INPUT -d $OURIP -p udp -m udp -s $SERVERS –sport 53 -m state –state ESTABLISHED -j ACCEPT
$IPTABLES -A OUTPUT -s $OURIP -p tcp -m tcp -d $SERVERS –dport 53 -m state –state NEW,ESTABLISHED -j ACCEPT
$IPTABLES -A INPUT -d $OURIP -p tcp -m tcp -s $SERVERS –sport 53 -m state –state ESTABLISHED -j ACCEPT
done

Allow us access to our NTP servers. If you are not sure where these are, check the ntp configuration file.

for SERVERS in $NTPIPS ;
do
$IPTABLES -A OUTPUT -s $OURIP -p udp -m udp -d $SERVERS –dport 123 -m state –state NEW,ESTABLISHED -j ACCEPT
$IPTABLES -A INPUT -d $OURIP -p udp -m udp -s $SERVERS –sport 123 -m state –state ESTABLISHED -j ACCEPT
done

Allow us to connect out to our distro repository webserver or ISP mirror (for apt-get, or yum updates for example). If you are running unattended security updates, you may wish to test that this works with an “apt-get update” before moving on.

for REPO in $REPOIPS ;
do
$IPTABLES -A OUTPUT -s $OURIP -p tcp -m tcp -d $REPO –dport 80 -m state –state NEW,ESTABLISHED -j ACCEPT
$IPTABLES -A INPUT -s $REPO -p tcp -m tcp –sport 80 -d $OURIP -m state –state ESTABLISHED -j ACCEPT
done

Allow connection to other web servers we need to access (e.g. akismet if running wordpress). This ruleset could end up covering a large range of servers. Furthermore, because the IP addresses of those servers could change over time, you could find yourself experiencing odd connectivity problems because you have forgotten this ruleset. It is mainly for that reason, that I constructed the “loose” policy script. You may wish to consider that if you find the list of servers in $WEBALLOWED is becoming cumbersome.

for SERVERS in $WEBALLOWED ;
do
$IPTABLES -A OUTPUT -s $OURIP -p tcp -m tcp -d $SERVERS –dport 80 -m state –state NEW,ESTABLISHED -j ACCEPT
$IPTABLES -A INPUT -s $SERVERS -p tcp -m tcp –sport 80 -d $OURIP -m state –state ESTABLISHED -j ACCEPT
done

Allow us SSH access for remote management. I like to limit my SSH access to one known secure source. You may have more than that. I recommend you avoid the lazy approach of allowing all inbound SSH, even if you are using fail2ban. (And as an aside, I don’t like fail2ban anyway, because I have a deep seated dislike of setuid scripts. They are are dangerous.)

for IP in $SSHIPS ;
do
$IPTABLES -A INPUT -s $IP -p tcp -m tcp -d $OURIP –dport $SSH -m state –state NEW,ESTABLISHED -j ACCEPT
$IPTABLES -A OUTPUT -d $IP -p tcp -m tcp -s $OURIP –sport $SSH -m state –state ESTABLISHED -j ACCEPT
done

Note – I do not recommend this practice, but if we want to allow SSH OUT of this server (to another server for example) we must reverse the logic of the above ruleset. Thus:

$IPTABLES -A OUTPUT -s $OURIP -p tcp -m tcp -d $OTHERIP –dport 22 -m state –state NEW,ESTABLISHED -j ACCEPT
$IPTABLES -A INPUT -s $OTHERIP -p tcp -m tcp –sport 22 -d $OURIP -m state –state ESTABLISHED -j ACCEPT

Allow us to mail out (e.g. for alerts or reports to the admin) or we are a mail server. Note that simply adding port 25 (or 587) to the list of ports at $SERVICES will NOT allow the server to send out mail to other servers.

$IPTABLES -A OUTPUT -s $OURIP -p tcp -m tcp –dport 25 -m state –state NEW,ESTABLISHED -j ACCEPT
$IPTABLES -A INPUT -d $OURIP -p tcp -m tcp –sport 25 -m state –state ESTABLISHED -j ACCEPT

If we are a mail relay and only want to accept incoming mail from a specific server (or servers) we could use:

$IPTABLES -A INPUT -d $OURIP -p tcp -m tcp –dport 25 -s $MAILIN -j ACCEPT

or if we are only allowed to send mail out via a smart host elsewhere, we could use:

$IPTABLES -A OUTPUT -s $OURIP -p tcp -m tcp -d $MAILOUT –dport 25 -m state –state NEW,ESTABLISHED -j ACCEPT
$IPTABLES -A INPUT -d $OURIP -p tcp -m tcp -s $MAILOUT –sport 25 -m state –state ESTABLISHED -j ACCEPT

Now allowed services to/from anywhere. Typically these will be web services. But note again that we only allow out the responses to inbound connections. We do not permissively allow out all connections.

for SERVICE in $SERVICES ;
do
$IPTABLES -A INPUT -p tcp -m tcp -d $OURIP –dport $SERVICE -m state –state NEW,ESTABLISHED -j ACCEPT
$IPTABLES -A OUTPUT -p tcp -m tcp -s $OURIP –sport $SERVICE -m state –state ESTABLISHED -j ACCEPT
done

Now allow selected ICMP messages – outbound source must always be our ip to prevent outbound spoof and we only really want to allow types 0, 3 and 8 (ping reply, destination unreachable and ping). Blocking all icmp is NOT a good idea. In particular, fragmentation error reporting is is vital to the PMTU discovery process – see RFC 2923 for example and https://en.wikipedia.org/wiki/Path_MTU_Discovery. Marc Slemko wrote an excellent article about this back in 1998.

So – ping from outside inwards – but rate limit to 1/sec. This helps prevent us being used in a spoofed address ping flood. But note that rate limiting here may not be necessary on most modern distros. Check the contents of the files: /proc/sys/net/ipv4/icmp_ratelimit and /proc/sys/net/ipv4/icmp_ratemask. Note, however, that by default icmp_ratelimit only applies to ICMP error messages and source quench, not all ICMP replies (or icmp echo). See frozentux for some useful information. For a listing of all ICMP types and codes, see the IANA reference here.

$IPTABLES -A INPUT -p icmp -m icmp -d $OURIP –icmp-type echo-request -m limit –limit 1/sec -j ACCEPT
$IPTABLES -A OUTPUT -s $OURIP -p icmp -m icmp –icmp-type echo-reply -m limit –limit 1/sec -j ACCEPT

and from inside outwards

$IPTABLES -A OUTPUT -s $OURIP -p icmp -m icmp –icmp-type echo-request -m limit –limit 1/sec -j ACCEPT
$IPTABLES -A INPUT -p icmp -m icmp -d $OURIP –icmp-type echo-reply -m limit –limit 1/sec -j ACCEPT

Now destination unreachables (type 3). Be a good net citizen and respond appropriately. ICMP type 3, code 4 in particular is necessary if we don’t want to break PMTU discovery.

$IPTABLES -A INPUT -p icmp -m icmp -d $OURIP –icmp-type destination-unreachable -j ACCEPT
$IPTABLES -A OUTPUT -s $OURIP -p icmp -m icmp –icmp-type destination-unreachable -j ACCEPT

Lastly, the belt to the braces above. These lines are not necessary, but they give a reassuring positive affirmation in the output of “iptables -nvL” that you /do/ have a default drop in place at the end of your ruleset.

$IPTABLES -A INPUT -j DROP
$IPTABLES -A OUTPUT -j DROP
$IPTABLES -A FORWARD -j DROP

That’s all folks…

Permanent link to this article: https://baldric.net/tight-iptables-firewall-for-servers/