IPFW Firewall Script (Suitable for OS X)

I posted an article the other day about how to configure OS X to use a custom firewall script but explicitly didn’t discuss the actual rules, in this article I’m going to focus only on the firewall rules and assume that you are using an IPFW configuration that is working correctly on some OS that uses IPFW. Please note that I have tested these rules on OS X only but I can’t see why they wouldn’t work on any other IPFW enabled system.

So, without further ado, here is my /etc/rc.firewall (with all specific IP addresses blanked out of course!):

#!/bin/sh

#
# Declare variables
#
IPFW='/sbin/ipfw'
LOOPBACK_INTERFACE=lo0
PRI_DNS=XXX.XXX.XXX.XXX
SEC_DNS=XXX.XXX.XXX.XXX

#
# Flush the rules so we start with an empty pallet
#
$IPFW -f -q flush

#
# Do some house keeping
#

# allow through all genuinely local packets
$IPFW add 00001 allow ip from any to any via $LOOPBACK_INTERFACE

# deny all spoofed local packets
$IPFW add 00010 deny ip from 127.0.0.0/8 to any in
$IPFW add 00011 deny ip from any to 127.0.0.0/8 in
$IPFW add 00012 deny ip from 224.0.0.0/3 to any in
$IPFW add 00013 deny tcp from any to 224.0.0.0/3 in

# allow through established connections
$IPFW add 00020 allow ip from any to any established

# filter ICMP packets to only allow through some types
$IPFW add 00030 allow icmp from any to any icmptypes 0,3,4,8,11,12
$IPFW add 00031 deny icmp from any to any

# allow through DNS
$IPFW add 00040 allow udp from me to $PRI_DNS 53 keep-state
$IPFW add 00041 allow udp from me to $SEC_DNS 53 keep-state

# allow myself to make outgoing connections
$IPFW add 00050 allow tcp from me to any keep-state
$IPFW add 00051 allow udp from me to any keep-state

# allow through all DHCP packets
$IPFW add 00060 allow udp from any to any 67
$IPFW add 00061 allow udp from any to any 68

# deny UDP
$IPFW add 00070 deny udp from any to any

#
# Allow services
#

# ssh just from within my departments range
$IPFW add 00100 allow tcp from XXX.XXX.XXX.0:255.255.255.0 to me 22

#
# Allow in exceptions for me
#

# allows the FTP server to connect back to me for active FTP
$IPFW add 00200 allow tcp from XXX.XXX.XXX.XXX 20 to me

#
# end with deny all
#
$IPFW add 65534 deny ip from any to any

OK, so now I’m going to go through this bit by bit to explain why I’m doing things the way I am. There is logic behind each line and I’ve spent a lot of time and effort this week getting this right. I’ll also mention some pitfalls to look out for along the way.

I should also point out what I was trying to achieve with these rules. The first thing is that these rules allow me to go out on any port to anywhere I want, they are not designed to stop me doing stuff. Secondly, these rules are designed to expose as few as possible services on this machine and any services that are exposed should be exposed to as few hosts as possible. Finally, these rules are also designed to filter out a lot of the UDP rubbish floating round on the network I work on because of all the Windows boxes on it.

So, into the rules, the way I have it set up is that the only parts that you should need to edit are the top bit that defines the variables and the bottom bit that defines the services you want to open up, the bits in between that should not need to be edited as there are no specific IP addresses anywhere in them.

The first line is obviously a shebang line, if you don’t know what that is please stop reading now because you are not ready to set up a custom firewall script! So, the next bit is the definition of some variables:

IPFW='/sbin/ipfw'
LOOPBACK_INTERFACE=lo0
PRI_DNS=XXX.XXX.XXX.XXX
SEC_DNS=XXX.XXX.XXX.XXX

The first one is obviously defining the location of the IPFW binary and the second line is the definition of the loop-back interface. Be careful here, some scripts you see use lo* here and that is very dangerous, using it made my machine un-usable till I changed it back to lo0. The next two lines define the IP addresses for the DNS servers that the firewall should allow the machine to use.

The next thing you have to do is to flush all the rules that may be in the firewall already out so you know exactly what will be in there when your script ends, to do this we use the flush command but we must also use the -f flag to force it to do the flush without asking us if we are sure.

$IPFW -f -q flush

The next stage is to ensure that all loopback traffic can flow but that no one can spoof the localhost address on another interface, anyone doing so is definitely up to no good!

# allow through all genuinely local packets
$IPFW add 00001 allow ip from any to any via $LOOPBACK_INTERFACE

# deny all spoofed local packets
$IPFW add 00010 deny ip from 127.0.0.0/8 to any in
$IPFW add 00011 deny ip from any to 127.0.0.0/8 in

The next two lines are standard in all firewalls OS X’s firewall interface generates so I have added them to my rules too. They block certain types of multi-cast traffic.

$IPFW add 00012 deny ip from 224.0.0.0/3 to any in
$IPFW add 00013 deny tcp from any to 224.0.0.0/3 in

The next thing to do is to allow packets belonging to contections that have already been established to pass through the firewall without needing to be checked off all the rules again. This a great optimisation but is only any good if you have it close to the start of your rules.

$IPFW add 00020 allow ip from any to any established

At this point the next thing we are going to deal with is ICMP packets. Many people block all of these packets but I am not a fan of that. ICMP is there for a good reason and blocking it will probably make your system administrator grumpy! The way I filter ICMP I can ping people, people can ping me, I can traceroute to people and people can traceroute to me. I am however still blocking loads of other potential ICMP traffic that is just not needed (note that to get traceroute working you also need UDP to be allowed out).

$IPFW add 00030 allow icmp from any to any icmptypes 0,3,4,8,11,12
$IPFW add 00031 deny icmp from any to any

Next I allow through DNS to my specific DNS servers. These rules are technically not needed if you decide to allow yourself to send out traffic on all UDP ports using a state-full rule (see rule 00051) but I like to put these in explicitly because I sometimes like to just kill all UDP connections later in the rules.

$IPFW add 00040 allow udp from me to $PRI_DNS 53 keep-state
$IPFW add 00041 allow udp from me to $SEC_DNS 53 keep-state

The next thing I do is allow myself to make outgoing connections to any host on any TCP port and to send UDP to any port on any host. If you want to be able to use traceroute you have to allow UDP traffic out. Again, if you allow all UDP out using a state-full rule like the one below you don’t need to explicitly allow DNS so you can leave out the two lines above if you wish.

$IPFW add 00050 allow tcp from me to any keep-state
$IPFW add 00051 allow udp from me to any keep-state

One more thing to bear in mind is that if you do not include rule 00051 above to allow out and back all UDP ports then you would need to explicitly allow NTP if you wanted to use it.

The next thing to do is to make sure that you let all DHCP packets through. If you don’t use DHCP you can leave these two lines out.

$IPFW add 00060 allow udp from any to any 67
$IPFW add 00061 allow udp from any to any 68

At this point I like to deny all UDP traffic because there is just so much of it on our network but you can leave this line out if you wish, the last line will catch these packets too. The reason I put this rule here is for efficiency.

$IPFW add 00070 deny udp from any to any

Then we get to the second last part of the script where we define the services on our machine that we will allow be visible (to part of) the outside world. I like to tie things down very tightly so that I expose as few services as possible to as as few people as possible, hence, I only have two entries in this part of the script (and one is not technically a service). The first thing I do is allow SSH access to my machine but ONLY from within the subnet used by people within my department, not to anyone else on campus or beyond.

$IPFW add 00100 allow tcp from 149.157.4.0:255.255.255.0 to me 22

The second entry I have in here is needed to allow me to use active FTP to communicate with a web server I have to publish stuff to.

$IPFW add 00200 allow tcp from ccintranet.nuim.ie 20 to me

Finally, AND MOST IMPORTANLY, you must end with a rule to deny all packets not yet accepted. If you don’t your rule set will be utterly pointless and your machine will be pretty much completely open.

$IPFW add 65534 deny ip from any to any

And there you have it, my full set of Firewall rules for OS X. Now, please note that although I’ve been working with IPFW rules for many years both on OS X and FreeBSD I would still not consider myself an expert, hence, these rules come with absolutely no guarantees. I’m confident they are an excellent set of rules that combine the best bits of the other scripts I found on the web for OS X but you will need to make your own judgement call before using them.

My final bit of advice is to NEVER put a line onto your rules that you do not understand 100%, if you’re not sure what a rule does it should not be in your script! When you are done with your rules you should then check them by getting someone to nmap your machine or by using a free online service like ShieldsUp! from Gibson Research Corporation. Happy firewalling!