Protecting a single host with IPFW

A while ago I did some experimenting with the FreeBSD operating system.

One of the first tasks that I set out for myself was to get acquainted with the firewall. Or, should I say, one of the firewalls. In FreeBSD there are three different firewalls to choose from: IPFW, PF and IPFilter.

In this article we will use IPFW.

IPFW is a versatile firewall and could very well be used to set up a router if you like. That’s out of scope for this article though. If you’re interested in doing that, two useful starting points are /etc/rc.firewall and the IPFW chapter in the FreeBSD Handbook.

The goal of this article is to show you how to protect a single host. In my case, the host was a FreeBSD droplet at DigitalOcean that had its own public IP address.

If you scroll down to the end of the article, you will see the whole script.

What we want to achieve

Yes, this ruleset disables IPv6 traffic. “Why?” you might ask. I simply didn’t feel the need for IPv6 in my use-case. If you want it, you will need to modify the script yourself.

Warning!

When you enable a firewall you also run the risk of locking yourself out of the host if your ruleset is broken. Before you enable IPFW, make sure you have an alternative way of accessing the system, in case you happen to botch your ruleset. If you use DigitalOcean like me, see their article about regaining access to droplets.

Enabling the rules

The script is available at GitHub. Download the latest version from there and save it in a suitable location. I chose /etc/ipfw.rules

Before you can use IPFW you need to enable it. As root, run these two commands:

sysrc firewall_enable="YES"

sysrc firewall_script="/etc/ipfw.rules"

Then reboot. The rules will be loaded at boot.

What the rules do

Let’s take a look at the rules!

The first three rules handle local traffic. They are culled from rc.firewall.

${fw} allow ip from any to any via lo0

This rule makes sure that any local packets will pass – i.e. traffic for the loopback network interface.

${fw} deny ip from any to 127.0.0.0/8

${fw} deny ip from 127.0.0.0/8 to any

If any other device than lo0 tries to use a loopback IP address, we will simply drop those packets. Please note that any packet that actually use lo0 will be handled by the first rule and therefore they won’t be dropped.

${fw} deny ipv6 from any to any

As I mentioned earlier, this rule will drop any IPv6 packets.

${fw} reass ip from any to any in

If any IP fragments are received, we will reassemble them.

${fw} check-state

This is where we configure IPFW to act as a stateful firewall in order to keep track of inbound and outbound packets. So what will happen when a packet hits this rule? The man page explains it pretty clearly: “Checks the packet against the dynamic ruleset. If a match is found, execute the action associated with the rule which generated this dynamic rule, otherwise move to the next rule.”

${fw} allow ip from me to any out keep-state

Here we tell the firewall to allow any outbound traffic – i.e. any traffic that was sent from this host to any other host. Since we add the keep-state directive, any replies will be allowed.

In the following three lines we allow for inbound “application traffic”:

${fw} allow tcp from any to me 22 setup keep-state

${fw} allow tcp from any to me 80 setup keep-state

${fw} allow tcp from any to me 443 setup keep-state

The ports we allow are 22, 80 and 443 – i.e. SSH, HTTP and HTTPS. Modify these lines to allow for any protocols that you want.

We use the “setup keep-state” pattern so that outside hosts are allowed to initiate connections to these ports, and any subsequent related packets are also allowed.

${fw} unreach port udp from any to me 33435-33524

This line is culled from Jef Poskanzer’s IPFW rules. Figuring out how it works is left as an exercise for the reader 🙂

${fw} allow icmp from any to any icmptypes 0,3,4,8,11

ICMP is a neat protocol. We want to allow some of the message types, but not all of them.

We will allow the following:

Type Description
0 Echo Reply
3 Destination Unreachable
4 Source Quench
8 Echo Request
11 Time Exceeded

Please see Wikipedia’s ICMP article for additional details regarding the ICMP message types.

Well, that’s it. Now we’re done!

Please note that IPFW will also insert a default rule with the number 65535 that will deny ip from any to any. So if a packet doesn’t match any of the rules in our script, it will be dropped. Sucks to be them!

The entire script

#!/bin/sh

ipfw -q -f flush

fw="ipfw -q add"

${fw} allow ip from any to any via lo0

${fw} deny ip from any to 127.0.0.0/8

${fw} deny ip from 127.0.0.0/8 to any

${fw} deny ip6 from any to any

${fw} reass ip from any to any in

${fw} check-state

${fw} allow ip from me to any out keep-state

${fw} allow tcp from any to me 22 setup keep-state

${fw} allow tcp from any to me 80 setup keep-state

${fw} allow tcp from any to me 443 setup keep-state

# Allow traceroute to function
${fw} unreach port udp from any to me 33435-33524

${fw} allow icmp from any to any icmptypes 0,3,4,8,11

Author: Carl Winbäck
Published: 2021-02-08

Back to the main page