“loose” iptables firewall for servers

This page describes and documents the “loose” policy stance applied in the iptables script iptables.loose.sh discussed in my post of 9 September 2012. Its sister, tight, 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” # replace x.x.x.x with 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” # address(es) of our NTP server(s)
SERVICESIN=”80 443″ # the services we run and allow access to – here only http and https
SERVICESOUT=”80″ # the services we wish to connect out to – here only http. DNS and NTP are handled above.
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 targetted. 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 implementation. 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 inbound 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

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 inbound connections to our permitted services.

for SERVICE in $SERVICESIN ;
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

Allow outbound connections initiated by us, but only to specified services (let’s not be too lax eh?). Unlike the “tight” policy (q.v.), this ruleset runs the risk of us allowing connections out to servers we should not trust. If our $SERVICESOUT define permits access to port 80 (or 443) a hostile program internally could call out to a command and control server or a staging server and could then download further malware such as a rootkit. However, this rule is easier to manage. You pays your money….

for SERVICE in $SERVICESOUT ;
do
$IPTABLES -A OUTPUT -s $OURIP -p tcp -m tcp –dport $SERVICE -m state –state NEW,ESTABLISHED -j ACCEPT
$IPTABLES -A INPUT -d $OURIP -p tcp -m tcp –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.

$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 we /do/ have a default drop in place at the end of our 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/loose-iptables-firewall-for-servers/