network, security

pf, OpenBSD’s [p]acket [f]ilter (2)

We introduced OpenBSD’s pf in a previous post. In the present one, we are going to start commenting a full-featured firewall configuration which uses quite a few of pf’s functionalities: macros, lists, anchors…
As we said then, OpenBSD’s FAQ contains the complete and detailed documentation.

Here is the complete set [but for those related to authpf] of firewall rules, usually stored at /etc/pf.conf (bear with me for the long quote, but I’d rather comment a complete file than do it in parts).

# 0) Start: macros and tables
ext_if="rl0"
int_if="vr0"

ext_services   = "{smtp www 222}"
in_services    = "{ssh smtp domain www}"
always_open    = "{ssh domain smtp}"
routed_services= "{nameserver domain whois ssh \
                   smtp pop3 imap2 imap3 imaps \
                   pop3s kpop ssmtp submission \
                   bootps 67 https}"

table <martians> { 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 }

table <bruteforce> persist

# 1) Options
set skip on lo

# 2) Normalization
scrub in

# 3) Queueing (none on this configuration)

# 4) Translation
nat on $ext_if from !($ext_if) -> ($ext_if:0)
nat-anchor "authpf/*"
rdr-anchor "authpf/*"
binat-anchor "authpf/*"

# 4.a) If the user is not authpf-authenticated, redirect his browser to
#        a "please authenticate" page
rdr pass on $int_if proto tcp from ! <authpf_users> to any port 80 \
         -> 127.0.0.1 port 8081

# 5) Packet Filtering
antispoof quick for {$int_if lo}
block quick from <bruteforce>
block all

#OK icmp
pass quick on $int_if proto icmp
pass out quick on $ext_if proto icmp

# 5.a) Basic passes
# everything going into the LAN from the inside should pass
pass out quick on $int_if proto tcp from any to 192.168.1.0/24
# OK ssh and $always_open to the LAN
pass in quick on $int_if proto tcp from any to self port 22
pass in quick on $int_if proto {tcp udp} from any to any port $always_open

# 5.b) authpf
# Use authpf to allow/deny services
anchor "authpf/*"
# PASS all routed services using authpf 
# into from LAN and outside to the INET
pass in quick on $int_if proto {tcp udp} from <authpf_users> \
                to any port $routed_services 
pass out quick on $ext_if proto {tcp udp} from <authpf_users> \
                 to any port $routed_services

# 5.c) external services, martians and options, overload...
# PASS in ext services, but LOG, reject martians and control traffic
pass in log on $ext_if proto tcp from ! <martians> to ($ext_if) port $ext_services \
        flags S/SA keep state (max-src-conn 10, max-src-conn-rate 15/5, \
        overload <bruteforce> flush global)

# 5.d) PASS self to the outside (INET)
pass out on $ext_if proto tcp from self to any port $always_open

# 5.e) Is this necessary?
# Anything else to the outside should pass in case we have forgotten something.
pass out on $ext_if all

There are 6 standard regions in a pf.conf file, as the man page says. Usually, one starts with a collection of macros and tables, which are two kind of variables:

  • Macros: store lists of values, which can be either strings or numbers. From the example:
    ext_if="rl0"
    [...]
    ext_services = "{smtp www 222}"
    

    Values are written between quotation marks and lists of values between curled braces. Recursion is allowed (but not used in our example).

  • Tables: store collections of IP addresses. They are used for fast search and for dynamic storage/release. Per the example:

    table <martians> { 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 }
    table <bruteforce> persist
    

    The first one serves as a list of “never-to-be-allowed” IPs. The second one is useful to block brute-force attacks against the server from the outside, as we shall see later on. The persist keyword makes the table exist even if there are no active rules referencing it (so that even if the table is empty, it can be filled later on with some values).

After this standard initialization, one must set the default options. In our case there is just one:

set skip on lo

which means “do not perform any filtering on the lo interface” (lo, as usual, refers to the loop-back interface). This option is quite normal.

There are options for setting the default blocking policy, the number of available states at a time, optimization options…

Then come the traffic normalization settings, which are used “to sanitize packets so that there are no ambiguities in packet interpretation on the receiving side”. Normalization is invoked with the scrub keyword. The line

scrub in

is also usual.

After the normalization directives come the queueing rules, which serve for traffic control. We are not delving into this (interesting) point because the firewall we are studying does not use traffic control (ok, it should, but it does not).

Still before stating the filtering rules (which is what you are probably expecting to read by now), one must configure the translation of packets. This refers to NAT and redirects (that is, translate either the source or the destination part of packets). Notice that translation occurs before filtering, as we shall recall later on.

nat on $ext_if from !($ext_if) -> ($ext_if:0)

This line shows:

  • That macros are referenced using the $ sign, as shell variables.
  • The use of ( parenthesis ) around an interface: this is translated to the IP address of that interface. This is especially useful when dealing with DHCP’d interfaces.
  • The use of ! as a logical “not”.
  • The :0 after an interface name: which means “do not include to interface aliases”.
  • The nat keyword: perform a (directional) NAT. The rules translates into:
    "NAT" packets going through $ext_if coming from IPs different from the one of $ext_if so that they get the source address of $ext_if.

There is an interesting redirection rule in our file:

# 4.a) If the user is not authpf-authenticated, redirect his browser to
#        a "please authenticate" page
rdr pass on $int_if proto tcp from ! <authpf_users> to any port 80 \
         -> 127.0.0.1 port 8081

In this firewall, the authentication utility authpf is used. This utility lets one filter connections by user and not just by IP. To this end, each user must log into the firewall using a special session “authpf” which creates specific rules once the user logs in and deletes them when he logs out. This way, one can fine-grain packet filtering on the user level. Especially interesting for wifi networks and for “content filtering”.
The rule above makes each attempt of connection to a web server be redirected to the localhost (port 8081, where a mini-server is listening, which just shows a page saying “please log into the system”) when the user is not authenticated using authpf. This is an authenticated firewall. pf, when used with authpf “marks” each packet with the username, and keeps track of authenticated users with the authpf_users table. Rules can be made to be applied for authenticated users only (or, as in this case, to non-authenticated users).
Notice, finally, that tables are referred to using the <table> notation (between angles).
And, at long last, we come to the true packet filtering rules, of which we are commenting just some.

# 5) Packet Filtering
antispoof quick for {$int_if lo}

# default block
block quick from <bruteforce>
block all
  • As we said in our previous post on pf, rules are applied in “last-match” order. Only the last rule applying to a packet is applied to it. UNLESS the keyword quick appears in one. If a rule matching a packet has the quick keyword, then that rule is applied to the packet and parsing of the pf.conf file stops.
  • First of all, we antispoof anything quickly at the inside interface and the loopback one. This means “block all traffic with a source IP from the network(s) directly connected to the specified interface(s) from entering the system through any other interface”. (from man pf.conf).
  • Then, we block (also quickly) anything coming from the <bruteforce> table of addresses (which, as we shall see, is dynamically filled with IPs trying to overload the system).
  • And we state the default policy: block all. So, if a packet does not match any of the forthcoming rules, it is blocked. This is a usual directive.

After this brief start come some real rules:

  • ICMP traffic is usually OK. The following rules should be clear enough:
    # basic passes
    pass quick on $int_if proto icmp
    pass out quick on $ext_if proto icmp
    
  • We are not commenting the following in this post:
    anchor "authpf/*"
    
  • But authpf is really useful now: let pass only those packets which come from authenticated users (actually IPs from which a user has authenticated with authpf, but this will come).
    # PASS all routed services using authpf 
    # into from LAN and outside to the INET
    pass in quick on $int_if proto {tcp udp} from <authpf_users> \
                    to any port $routed_services 
    pass out quick on $ext_if proto {tcp udp} from <authpf_users> \
                     to any port $routed_services
    

    Notice that one must let packets into the firewall AND out of it, and that once a packet is "marked" with the username, it stays so even though it may have been redirected (those packets going outside have already been NAT'ed, as we said above).

  • And we try to keep brutes off with the following:
    # 5.c) external services, martians and options, overload...
    # PASS in ext services, but LOG, reject martians and control traffic
    pass in log on $ext_if proto tcp from ! <martians> to ($ext_if) port $ext_services \
            flags S/SA keep state (max-src-conn 10, max-src-conn-rate 15/5, \
            overload <bruteforce> flush global)
    

    which means: let everything [actually any SYN not SYN/ACK packet and later ones (keep state)] in from not "martians" to the outside IP on the external services BUT

    1. only let 10 connection from the same source at the same time
    2. only let 15 connection attempts per 5 minutes
    3. if some source exceeds those limits, add the source IP to the <bruteforce> table AND
    4. in that case, kill all connections from that IP "now"
  • The last rule is pretty smart.

The last-but-one is easy: let traffic from the inside (self) on the "always open ports" go out:

# 5.d) PASS self to the outside (INET)
pass out on $ext_if proto tcp from self to any port $always_open

Notice the keyword self which is substituted by a table of all the IP addresses of all of this host's network interfaces.
As a last comment, let me say that:

  • Thanks for reading :)
  • Things can be done differently, but this file is short, clear, and functional.
  • Things can be done better, as well. So: comments are welcome.

1 Comment

speak up

Add your comment below, or trackback from your own site.

Subscribe to these comments.

Be nice. Keep it clean. Stay on topic. No spam.

You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

*Required Fields