[Bug 256410] pf: Add pf_default_rules option

From: <bugzilla-noreply_at_freebsd.org>
Date: Fri, 04 Jun 2021 09:53:17 UTC
https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=256410

            Bug ID: 256410
           Summary: pf: Add pf_default_rules option
           Product: Base System
           Version: Unspecified
          Hardware: amd64
                OS: Any
            Status: New
          Severity: Affects Some People
          Priority: ---
         Component: misc
          Assignee: bugs@FreeBSD.org
          Reporter: thomas@gibfest.dk

Created attachment 225540
  --> https://bugs.freebsd.org/bugzilla/attachment.cgi?id=225540&action=edit
The patch for /etc/rc.d/pf and /etc/defaults/rc.conf

Out of the three firewalls in FreeBSD pf is the only one without a way to make
it default to deny everything:

- IPFW has "65535 deny ip from any to any" unless IPFIREWALL_DEFAULT_TO_ACCEPT
is set.
- ipfilter has something called IPFILTER_DEFAULT_BLOCK, I think. I don't really
know anything about ipfilter.

With pf, if no rules are loaded all traffic is permitted. This can be
considered a feature or a problem, recently it was a very big problem in my
end.

I propose the attached patch for /etc/rc.d/pf and a couple of new rc.conf
variables pf_default_rules_enable and pf_default_rules to improve this
situation.

The patch simply checks the exit code of the pfctl command which loads the
pf.conf ruleset, and if the exit code is 1 and pf_default_rules_enable is set
to YES, then it loads the rules in pf_default_rules, which defaults to a single
rule: "block drop log all".

BACKGROUND:
A little backstory might help clear up why I think this is a valuable and
neccesary change.

We (our company) had ended up with a pf.conf with a typo in it. The typo was
that the leading "$" sign of a macro was missing. This makes pf treat it as a
hostname instead of a macro, so it does a DNS lookup to process the firewall
rule. The typo could have been a missing quote or something else too, the
central point is the pf.conf was invalid, and it had been missed (humans being
human and all that).

The server in question was then rebooted, and suddently no firewall rules were
loaded, permitting all traffic to the networks behind it, leading to some bad
times at the office before it was discovered.

In the postmortem clean-up it was determined that since /etc/rc.d/pf had failed
to load the ruleset during bootup, and there being no "default block
functionality", there was 0 rules loaded so it of course permitted everything.

On these particular firewall machines it would have been MUCH better to have a
non-functional firewall than a fully open one. YMMV, and the default in my
patch is to keep the status quo, of course.

With this patch and pf_default_rules_enable set to YES I would have discovered
the issue much sooner and with no security incidents, which is vastly
preferable in this situation.

THE PATCH:
Mostly it is just a new "if" block around the pfctl -f call inside pf_start().
I included some warnings (for both cases) while here.

The only files touched are /etc/rc.d/pf and /etc/defaults/rc.conf.

EXAMPLE:
Note: All of these examples are with the attached patch applied.

I made two simple pf.conf files where one has small but significant typo:
[tykling@nuc2 ~]$ cat /etc/pf.conf 
foo="192.0.2.42"
pass in quick on em0 from any to $foo
[tykling@nuc2 ~]$ cat /etc/pf-typo.conf 
foo="192.0.2.42"
pass in quick on em0 from any to foo
[tykling@nuc2 ~]$ 

Note how "foo" is missing the leading $ sign meaning pf interprets it as a
hostname, which can't be resolved in this case.

First the default behaviour with no changes in rc.conf and a valid ruleset:

[tykling@nuc2 ~]$ sudo sysrc pf_default_rules_enable
pf_default_rules_enable: NO
[tykling@nuc2 ~]$ sudo sysrc pf_default_rules       
pf_default_rules: block drop log all
[tykling@nuc2 ~]$ sysrc pf_rules
pf_rules: /etc/pf.conf
[tykling@nuc2 ~]$ sudo service pf restart
Password:
Enabling pf.
[tykling@nuc2 ~]$ sudo pfctl -s rules | wc -l
       1
[tykling@nuc2 ~]$ sudo pfctl -s rules        
pass in quick on em0 inet from any to 192.0.2.42 flags S/SA keep state
[tykling@nuc2 ~]$ 

All is well, it loaded the one rule in the ruleset. Now an example with an
invalid ruleset, still with the above defaults in place:

[tykling@nuc2 ~]$ sudo service pf restart                                       
Disabling pf.
Enabling pfno IP address found for foo
/etc/pf-typo.conf:2: could not parse host specification
pfctl: Syntax error in config file: pf rules not loaded
/etc/rc.d/pf: WARNING: Unable to load pf.conf, and pf_default_rules_enable is
NO.
/etc/rc.d/pf: WARNING: No pf rules are loaded, this means all traffic is
permitted.
.
[tykling@nuc2 ~]$ sudo pfctl -s rules
[tykling@nuc2 ~]$    

The behaviour is the same as always, but a couple of new warn() calls informs
the admin about what happened so they might spot it in /var/log/messages.

Now the same setup but with the new feature enabled:

[tykling@nuc2 ~]$ sudo sysrc pf_default_rules_enable="YES"
pf_default_rules_enable: NO -> YES
[tykling@nuc2 ~]$ 

The valid ruleset behaves as always:
[tykling@nuc2 ~]$ sudo sysrc pf_rules="/etc/pf.conf"                            
pf_rules: /etc/pf.conf -> /etc/pf.conf
[tykling@nuc2 ~]$ sudo service pf restart                                       
Disabling pf.
Enabling pf.
[tykling@nuc2 ~]$ sudo pfctl -s rules | wc -l                                   
       1
[tykling@nuc2 ~]$ 

With the invalid ruleset the pf_default_rules are loaded:

[tykling@nuc2 ~]$ sudo sysrc pf_default_rules_enable="YES"                      
pf_default_rules_enable: NO -> YES
[tykling@nuc2 ~]$ sudo service pf restart                                       
Disabling pf.
Enabling pfno IP address found for foo
/etc/pf-typo.conf:2: could not parse host specification
pfctl: Syntax error in config file: pf rules not loaded
/etc/rc.d/pf: WARNING: Unable to load pf.conf, and pf_default_rules_enable is
set to YES.
/etc/rc.d/pf: WARNING: Loading pf_default_rules: block drop log all
.
[tykling@nuc2 ~]$ sudo pfctl -s rules
block drop log all
[tykling@nuc2 ~]$

Say I wanted a way in, a management backdoor in case this happens, I could for
example choose to pass traffic on a management interface only:

[tykling@nuc2 ~]$ sudo sysrc pf_default_rules                                   
pf_default_rules: block drop log all\npass quick on em0
[tykling@nuc2 ~]$ sudo service pf restart                                       
Enabling pfno IP address found for foo
/etc/pf-typo.conf:2: could not parse host specification
pfctl: Syntax error in config file: pf rules not loaded
/etc/rc.d/pf: WARNING: Unable to load pf.conf, and pf_default_rules_enable is
set to YES.
/etc/rc.d/pf: WARNING: Loading pf_default_rules: block drop log all\npass quick
on em0
.
[tykling@nuc2 ~]$ sudo pfctl -s rules
block drop log all
pass quick on em0 all flags S/SA keep state
[tykling@nuc2 ~]$ 

I guess that is all. I hope it made sense and that the feature will find its
way into FreeBSD. I know I will be running with it locally until such a time.
Apologies in advance if I messed up the patch, I don't do this often, and I
don't have a phabricator account (yet).

Have a lovely weekend :)

ps. I know about it being FreeBSDs job to ensure a reliable delivery of bullet
from gun to foot. But I feel like an exception is warranted here, especially
given that the two other firewalls have similar functionality.

-- 
You are receiving this mail because:
You are the assignee for the bug.