Configuring IPFW Firewall on OS X

14 March 2012
The latest versions of Mac OS X, being based on BSD style Unix, have a lot of powerful features that are legacy to many Unix operating systems. One is the ipfw firewall. OS X actually has two firewalls by default, an application firewall that blocks access to specific programs, and the ipfw firewall, which is a much lower level firewall that operates by inspecting inbound packets and allowing or denying them based on source IP, destination IP, port and protocol. This allows a much finer grain of control for remote access to an OS X computer. This tutorial is designed to walk you through the steps necessary to configure ipfw with basic filtering to allow only connections to certain services. You can customize the scripts listed below to suit your own needs, including limiting connections to specific trusted hosts.

Configuring the Ipfw Firewall

In order to get the ipfw firewall working three separate scripts are required. The first is a custom firewall ruleset. This will specify the rules that you wish to enforce on network access to and from your machine. ipfw can do inbound and outbound filtering, which is really handy, but it's very sensitive to the order of rules. The first rule matched by a packet is the one that applies, so if a packet matches two rules in ipfw, only one will govern the action of the firewall: the first rule. The second file that you'll need to add to your system is a shell script that flushes existing firewall rules and loads up your new ruleset. This is a relatively straightforward control file that is just a BASH shell script. The third file is an XML file that serves as instructions to the Launchd daemon on the OS X system. In many ways this file is analogous to an init script on any other Unix or Linux type system. When the operating system boots, it searches in the Launchd directory for XML files that specify which services should be started. The new XML file that you'll add will tell your OS X system to run the shell script, which will flush existing firewall rules and load up your custom rules. Once configured, the ipfw firewall can only be modified by changing one of the three new files. The ipfw has a higher priority and trumps the application firewall, so some user education may be necessary to inform users that even if they allow access via the application firewall via the GUI settings manager, the ipfw firewall will block access to the application until it is modified.

Setting up the Firewall

The first step in installing your new ipfw firewall is to develop a firewall ruleset. The following is a sample ruleset that allows no inbound traffic except for responses to connections initiated by the host, and SSH connections. To create this file type:
$ sudo vi /etc/firewall.conf
You'll need to be able to sudo to be able to do this. Once vi is started copy the following text into the file:
# Sample IPFW config
# by Justin C. Klein Keane <jukeane@madirish.net>
# NB: This script is initiated by /usr/local/sbin/firewall.sh
#     firewall.sh is in turn controlled by launchd from the 
#     config at /Library/LaunchDaemons/com.apple.firewall.plist
# 
# Permit loopback
add 100 allow ip from any to any via lo*
# Permit outbound connections to maintain state
add 120 allow tcp from any to any out keep-state
add 130 allow udp from any to any out keep-state
# Permit incoming SSH
add 140 allow tcp from any to any dst-port 22
# Default deny with logging (final rule)
add 65534 deny log logamount 1000 ip from any to any in
Save and quit the file and then change the permissions on the file like so:
$ sudo chown root:admin /etc/firewall.conf
Now you need to create the shell script that will load up these custom rules. To do this edit the following file, noting that you may have to create the /usr/local and /usr/local/sbin directories if they don't already exist:
$ sudo vi /usr/local/sbin/firewall.sh
Copy and paste the following text into this shell script:
#!/bin/sh
# Flush existing rules
/sbin/ipfw -q flush
# Run IPFW and load custom rules
/sbin/ipfw -q /etc/firewall.conf
# Enable detailed logging to syslog
/usr/sbin/sysctl -w net.inet.ip.fw.verbose=1
Now that we have the shell script we need to change the permissions to protect it and make it executable like so:
$ sudo chown root:admin /usr/local/sbin/firewall.sh
$ sudo chmod 544 /usr/local/sbin/firewall.sh
Finally you need to create the Launchd script. To do this create a new file like so:
$ sudo vi /Library/LaunchDaemons/com.apple.firewall.plist
Once this is done copy and paste the following XML into the file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0 //EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Label</key>
	<string>com.apple.firewall</string>
	<key>ProgramArguments</key>
	<array>
		<string>/usr/local/sbin/firewall.sh</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
</dict>
</plist>
Save and quit this file. Next change the permissions on the file appropriately using:
$ sudo chown root:admin /Library/LaunchDaemons/com.apple.firewall.plist

Testing the Configuration

And you should be all set. You can test out your new configuration by running an NMAP scan of your machine and noting the output:
$ nmap target.domain.tld

Starting Nmap 5.51 ( http://nmap.org ) at 2012-03-14 08:33 EDT
Nmap scan report for target.domain.tld (127.0.0.1)
Host is up (0.0023s latency).
Not shown: 997 closed ports
PORT     STATE SERVICE
22/tcp   open  ssh
88/tcp   open  kerberos-sec
5900/tcp open  vnc

Nmap done: 1 IP address (1 host up) scanned in 7.37 seconds
Now we can reload our firewall with the new custom rules using the following command:
$ sudo launchctl load /Library/LaunchDaemons/com.apple.firewall.plist
After the firewall is reloaded you can check the ipfw rules using the following:
$ sudo ipfw show
00100  0    0 allow ip from any to any via lo*
00120 59 4460 allow tcp from any to any out keep-state
00130  2  256 allow udp from any to any out keep-state
00140  1  100 allow tcp from any to any dst-port 22
65534  0    0 deny log logamount 1000 ip from any to any in
65535  0    0 allow ip from any to any
Finally you can re-scan the machine with NMAP to confirm that the changes have taken place:
$ nmap -Pn target.domain.tld

Starting Nmap 5.51 ( http://nmap.org ) at 2012-03-14 08:38 EDT
Nmap scan report for target.domain.tld (127.0.0.1)
Host is up (0.0094s latency).
Not shown: 999 filtered ports
PORT   STATE SERVICE
22/tcp open  ssh

Nmap done: 1 IP address (1 host up) scanned in 4.27 seconds
At this point your machine is pretty well locked down. The ipfw configuration will survive reboots as well as tweaks in the GUI settings for the application firewall. If you want to be even more careful we might limit the SSH connections to specific machines. For instance, if we only wanted to allow connections from the local subnet we could change the rule in firewall.conf for SSH to the following:
add 140 allow tcp from 10.0.0.0/24 to any dst-port 22
Note the use of CIDR notation. This will limit SSH connections to the range 10.0.0.1-254 and deny inbound SSH connections from any other host. This can be extremely useful in permitting only connections from trusted machines.

Conclusion

Using the ipfw firewall takes a little familiarity with the command line and some comfort with using console based text editors. In addition it removes an essential piece of your machine's security configuration from any GUI management. Although GUI tools exist to help you maintain your ipfw firewall, most will cost money to acquire. Using an ipfw greatly increases the security posture of your Mac. Using the firewall helps you filter out unwanted traffic and provides a much finer grain of control than the GUI based application firewall. ipfw can also allow you to limit connections to your machine to a known good subset of the internet. Instead of allowing SSH from all over the globe, you can allow SSH from specific IP addresses or subnets, greatly reducing the chances that your machine will fall victim to a brute force password attack.