There are three firewall options on FreeBSD. The in-house built IPFW, the ‘old’ IPF (known as IP Filter) and PF ported from OpenBSD. PF is a very popular piece of software which was originally sparked from an issue on the IPF license that prevented making changes publicly available, which the OpenBSD did all the time. When they realised of this issue, they pulled the code and came up with a solution of their own, based on the concepts and functionality of IPF but with a completely different code base. As time went on PF evolved and has now become a very nice firewall which some companies make us of it, such as Apple in macOS or Oracle inside Solaris.
If you find the articles in Adminbyaccident.com useful to you, please consider making a donation.
Use this link to get $200 credit at DigitalOcean and support Adminbyaccident.com costs.
Get $100 credit for free at Vultr using this link and support Adminbyaccident.com costs.
Mind Vultr supports FreeBSD on their VPS offer.
Leaving historical references aside, a few things must be considered when talking about PF on FreeBSD. The code in use as in version 12.0 is still based on the PF version found on OpenBSD 4.5. Obviously the code has been adapted to FreeBSD facilities and bugs have been fixed. Some companies such as Netgate make use of this code since they are the creators of pfSense, a routing software distribution very famous because of its performance, ease of use, and a match to Cisco’s and the like competition, using the right hardware which Netgate builds. Others, like Apple, have also taken FreeBSD’s implementation of OpenBSD, probably because other parts of macOS come from FreeBSD. So, having the old manual referencing that version of OpenBSD can be useful. Furthermore, a read on «The Book of PF» can be very helpful too.
A word of warning. The firewall is typically doing the heavy lifting security wise and the rest is left out or other threads are considered harmless. Many organizations fall into this issue because it is an easy one to fall into, not because they are not smart, not because they are just wrong. It is easy to take this approach and because the network applicance is the first line of defense, it is often times the first thing a nefarious actor encounters. But that approach is ‘Wrong’ because security should be composed, since having an only point of failure will eventually drive anyone to a certain fail. Plus it is not always that easy to have a nice firewall ruleset that covers every aspect, it is safe, etc.
Unlike many other firewalls PF applies the philosophy of ‘last match wins’ in the ruleset syntax. This is a big difference from IPFW, Iptables and other commercial tools which apply the opposite. Network administrators of other systems will have to get used to this difference but luckily enough PF has a very nice and clear syntax which allows to sit up front to a ruleset written months before and quickly understand what is doing without much thinking, almost just reading.
Another distinctive sign of PF is the performance concern when assembling rulesets is not needed because of an internal optimizer that works on that specific. The words of one of the PF main contributors, Henning Brauer, in this regards are very clarifying. For example one may end up having a set of rules which are equal except on the target ip. The optimizer will summarize all the rules into one that hits a table where all the ip’s have been collected. Another nice trick the optimizer can apply is the reordering of the rules, without changing their meaning, so if a packet needs to be filtered against a rule which has an specific interface, and that interface is not found until rule 101, that block of rules will be skipped and will start comparing rules where the specific interface is found, which will be rule number 101 and beyond.
To enable PF on FreeBSD we need to add a series of directives into one of the main configuration files /etc/rc.conf. To do so we can manually edit the file with an editor or use the sysrc command like follows.
To enable PF and being able to start it up as a service at boot time we can use this:
sudo sysrc pf_enable="YES"
To declare where is the ruleset located we’ll add the following:
sudo sysrc pf_rules="/etc/pf.conf"
Since we want events to be logged, specially those denied accesses (a source of knowing of a potential attack), we’ll write the following sentence:
sudo sysrc pflog_enable="YES"
In order to declare where those logs will be placed we’ll make use of:
sudo sysrc pflog_logfile="/var/log/pflog"
So, if we want to check the lines have been correctly added we can do:
albert@BSD:~ % cat /etc/rc.conf | grep pf
pf_enable="YES"
pf_rules="/etc/pf.conf"
pflog_enable="YES"
pflog_logfile="/var/log/pflog"
albert@BSD:~ %
Before starting up PF we will add a very simple ruleset into the pf.conf file so it does something instead of letting traffic in and out openly. We can add the following, but be aware that we will be left out if we are connected via SSH.
block in all
pass out all
We can now start the service, but remember, SSH connections will be blocked afterwards.
albert@BSD:~ % sudo service pf start
Enabling pf.
albert@BSD:~ %
albert@BSD:~ % Timeout, server 192.168.1.125 not responding.
This is not very useful because there is no way to connect to the box other than the console and if we prettend this to be offer services, incoming requests will not be satisfied. Let’s dig.
First of all we need to write down what we want to accomplish with the box and that will allow us to draft a configuration. This principles may help you in the design of it.
Premise a) List your needs. Your needs will tell you what ports are needed to be open and what interfaces will be used.
Premise b) List your security concerns. This will help you select the optimal action for each rule.
Premise c) List your best guess on the most matched rules and plan to even break the order from the numbered premises above.
A very simple example of a minimal configuration file can be found in the Absolute FreeBSD book from Michael W. Lucas. A very useful book everyone must have a copy of.
ext_if="em1"
set skip on lo0
scrub in
block in
pass out
pass in on $ext_if proto tcp from any to ($ext_if) port {22, 53, 80, 443}
pass in on $ext_if proto udp to ($ext_if) port 53
pass in on $ext_if inet proto icmp to ($ext_if) icmp-type { unreach, redir, timex, echoreq }
I’d let you read his nice and detailed explanation on how and why but it is worth commenting a few bits. As you can see there are three different blocks. First we have the interfaces, then the default policy and lastly a few rules that will allow some traffic just for certain ports. Since PF is a ‘last match wins’ type of firewall it is interesting to ‘block in’ all the traffic and then set a series of rules which are the exceptions to that default blocking everything policy. And yes, this policy described above allows SSH, DNS queries, as well as HTTP and HTTPS.
As a reminder this setting will protect the workstation, the server, wherever you may install this configuration, at the ‘Network Layer’ which is the 3rd one. It is useful and recommendable if you install this on a server to use tools that also protect the ‘Application Layer’ such as a WAF (Web Application Firewall) a tool you can read a how to in this other article.
Arrived to this point and before going beyond. PF has a nice command called pfctl. With it one can flush the rules, enable or disable PF, print the rules on the screen, etc. Straight from the handbook:
pfctl |
Enable PF. |
pfctl |
Disable PF. |
pfctl |
Flush all NAT, filter, state, and table rules and reload /etc/pf.conf . |
pfctl |
Report on the filter rules, NAT rules, or state table. |
pfctl |
Check /etc/pf.conf for errors, but do not load ruleset. |
Let’s draft a different configuration file with some more complexity. Before anything, you must know a configuration skeleton can be found at /usr/share/examples/pf/pf.conf. All the inputs are commented out and some may be not needed at all, but there you have a simple structure. For our case now we first need to declare the interfaces at use. If we are going to use one box as a workstation or web server we may have one or several NICs available, however if we prettend the box to act as a gateway we definitely need two of those. We’ll draft first a workstation example.
We will first declare the interfaces:
## Set interfaces
net="em0"
## Set the ip address (not needed if using DHCP)
# net_ip="192.168.1.125"
That we set is called a ‘macro’. We can also use macros to group ips, or services, so for example if we want to declare the non routable addresses, we can put them altogether in one macro.
## Define the non-routable reserved addresses RFC 1918
nonr = "{127.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12, \
10.0.0.0/8, 169.254.0.0/16, 192.0.2.0/24, \
0.0.0.0/8, 240.0.0.0/4, 224.0.0.0/3}"
This can also be achieved by using a table to collect those addresses instead of using macros. A table though is more specific to ip addresses, on the contrary to macros which are more flexible and admit other types.
table <rfc1918> const { 127.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12, \
10.0.0.0/8, 169.254.0.0/16, 192.0.2.0/24, \
0.0.0.0/8, 240.0.0.0/4, 224.0.0.0/3 }
The ‘const’ attribute states this table cannot be modified once it’s been created.
We can also, let’s imagine we are developing some web or helping to do so, add the web ports in a macro.
## Set webservices
web = "{http, https}"
PF is ‘clever’ enough to understand port numbers and services names since it can access them at /etc/services. Take a look at that file if you’ve never done it, since all common services are listed there alongside with the associated port number.
## Set other services
int_tcp_services = "{domain, ssh, http, https, ntp, smtp, smtps}"
int_udp_services = "{domain, ntp}"
In this way we have grouped the services we will need on this workstation, so we do not need to write one line for each.
We have to realize that we’ve blocked access inside our LAN, for example for a machine inside 192.168.1.0/24, which is a typical range of a home router. We can write a macro stating the friendly ips we want to give access to.
We will now allow free traffic to the loopback interface lo0. Since PF is a ‘last match wins’ firewall we will make use of ‘quick’, which tells the firewall once the rule is matched not to continue scanning for more rules and act according to the match.
## Let free traffic on the loopback interface lo0
set skip in lo0
The interface where logs will be obtained from does also need to be declared.
## Set the interface to log events from
set loginterface $net
The scrub directive is used to normalize packets so the there are no conflicts when resolving the last destination of the packet and furthermore it reassembles fragmented ones. This can also be a protective measure since some attacks consists on sending malformed packets with invalid flags.
## Normalization and reassembly of packets.
scrub in all
To avoid spoofing (packets stating they are coming from an address they are not really coming from), which PF can help preventing we can add something as:
## Block spoofed addresses
antispoof log quick for $net_if
It is now time to set the default policy, after having specified some macros, allowed free traffic on the lo0 interface, set the interface to be logged, normalized packets and got rid of spoofing attemps.
## Default behavior: block all traffic
block all
We start by blocking everything. We have to remember PF works reverse as other firewalls and in here the idea is the mast match wins. So we start blocking everything and we’ll let in and out the few bits we are interested in.
It may be needed to allow port 22 open (SSH) to our workstation in order to have remote access. But we may need to specify only one address on the LAN to have that access. Leaving it open to ‘any’ outside ip is a security concern and although you may have secured your connections to only work with keys, the port will be surely banged over and over by bots. Setting a Fail2Ban solution on this will help in this particular, although blacklistd may be even better since it reads from sockets and integrates easily to PF instead of reading log files as Fail2Ban does.
## Allow SSH on specific local address
pass in quick on $net_if proto tcp from 192.168.1.100 to any port ssh
pass out quick on $net_if proto tcp from any to 192.168.1.100 port ssh
In this case the box can be accessed from an address in the LAN range. Mind this is a type of address in the non-routable range we declared above as nonr. Because of this we set the ‘quick’ keyword so no further rules are inspected and access is immediately granted to the defined address on port 22. If we don’t place the ‘quick’ we won’t be able to access from the LAN since we later on we will declare another rule blocking accesses from non-routable addresses.
We need to resolve domains, so we can make use of the browser and access the world wide web. And again as before we need to get the answers to the DNS queries from a non-routable address. Since we will block traffic from those in a rule below (remember ‘last match wins’), we declare this rule above and setting the ‘quick’ keyword.
## Allow DNS queries to the home router located in 192.168.1.1
pass in quick on $net_if proto udp from 192.168.1.1 to any port domain
pass out quick on $net_if proto tcp, udp from any to 192.168.1.1 port domain
In here we have declared the specific address of the DNS server. This is a typical case for a home setup. Just change the address to the one in your network or office setup.
A bit of allowing pings, so you can troubleshoot network related issues.
## Allow all icmp traffic
pass quick on $net_if proto icmp
pass quick on $net_if proto icmp6
Time to block the non-routable addresses. These addresses have been reserved for LAN environments. So traffic coming from those may be legitimate, but only from the ones in your range, say 192.168.0.0/8 but not from 10.0.0.0/8.
## Block non-routable addresses
block drop in quick on $net_if from $nonr to any
block drop out quick on $net_if from any to $nonr
Since this is your LAN, presumably, you may not want to block those nonr addresses, however it is always more secure to allow just the necessary boxes to have access and leave the rest out. A non-trust philosophy in your LAN, specially in a shared environment, results in a much safer one.
Enough blocking, it is now time to allow the services we declared above for both the tcp and udp bits.
## Allow incoming traffic to services hosted by this machine
pass in on $net_if proto tcp from any to port $int_tcp_services
pass in on $net_if proto udp from any to port $int_udp_services
Traffic needs to get out too.
## Allow all outgoing traffic
pass out quick on $net_if
So, if we arrange it altogether the pf.conf file will look like this:
################ Start of PF rules file #####################
################# Workstation Example #######################
#############################################################
## Set interfaces
net_if = "em0"
## Set the ip address (not needed if using DHCP)
# $net_if_ip="192.168.1.104"
## Define the non-routable reserved addresses RFC 1918
nonr = "{127.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12, \
10.0.0.0/8, 169.254.0.0/16, 192.0.2.0/24, \
0.0.0.0/8, 240.0.0.0/4, 224.0.0.0/3}"
## Set webservices
web = "{http, https}"
## Set other services
int_tcp_services = "{domain, ssh, http, https, ntp, smtp, smtps}"
int_udp_services = "{domain, ntp}"
## Let free traffic on the loopback interface lo0
set skip on lo0
## Set the interface to log events from
set loginterface $net_if
## Normalization and reassembly of packets.
scrub in all
## Block spoofed addresses
antispoof log quick for $net_if
## Default behavior: block all traffic
block all
## Allow SSH on specific local address
pass in quick on $net_if proto tcp from 192.168.1.100 to any port ssh
pass out quick on $net_if proto tcp from any to 192.168.1.100 port ssh
## Allow DNS queries to the home router located in 192.168.1.1
pass in quick on $net_if proto udp from 192.168.1.1 to any port domain
pass out quick on $net_if proto tcp, udp from any to 192.168.1.1 port domain
## Allow all icmp traffic
pass quick on $net_if proto icmp
pass quick on $net_if proto icmp6
## Block non-routable addresses
block drop in quick on $net_if from $nonr to any
block drop out quick on $net_if from any to $nonr
## Allow incoming traffic to services hosted by this machine
pass in on $net_if proto tcp from any to port $int_tcp_services
pass in on $net_if proto udp from any to port $int_udp_services
## Allow all outgoing traffic
pass out quick on $net_if
################ End of PF rules file #####################
But why this configuration and not a different one, a more ‘open’ and easy one. It may not look simple to the untrained eye, but believe me, this last configuration is simple. However it is a bit more tight than others. For example, SSH connections are just allowed from one source. DNS queries are just allowed to one ip, although this already happens with other configs. Traffic on the LAN is also stopped. And this last point can seem a bit extreme on a home network but not so much if you are sharing the LAN with other untrusted users. In an office environment this ought to be mandatory since an infected machine can start propagating malicious packets and displaying a sort of malicious activity inside the LAN. Blocking everything not intended to happen, even inside a somehow ‘trusted’ network can be helpful to mitigate those infections. It is not a magic measure since allowed traffic can still be a threat but countering every other non inteded relation between a box and the rest of the nextwork is always useful.
Pf is capable of filtering NAT, multiple NICs, and be a fully fledged networking appliance such as the ones sold by Netgate, a company which builds hardware and writes PfSense. In future articles those aspects will be covered on raw FreeBSD but also in PfSense.
Sources:
Calomel blog entry of PF:
https://calomel.org/pf_config.html
The FreeBSD handbook section:
https://www.freebsd.org/doc/handbook/firewalls-pf.html
The Book of PF from No Starch Press:
Absolute FreeBSD from No Starch Press:
https://mwl.io/nonfiction/os#af3e
If you find the articles in Adminbyaccident.com useful to you, please consider making a donation.
Use this link to get $200 credit at DigitalOcean and support Adminbyaccident.com costs.
Get $100 credit for free at Vultr using this link and support Adminbyaccident.com costs.
Mind Vultr supports FreeBSD on their VPS offer.