PF Seek for some help for securing email server with PF

Hello all,

I have the following setup on a VPS of mine.
I have created an email server using jails (BastilleBSD). So far everything works great. On the actual VPS (the host) I have the following /etc/pf.conf:

Code:
# Main Variables
ext_if = "vtnet0"
host_ssh_port = "8199"
icmp_types = "{ echoreq unreach }"


# Jail Variables
jail_proxy = "192.168.100.10"
jail_mail = "192.168.100.25"


set block-policy drop
scrub in on $ext_if all fragment reassemble max-mss 1440
set skip on lo
set skip on bridge0


# Bastille Jails tables handling Jails' IPs
table <jails> persist

# IPv4 private address ranges
table <private> const { 10/8, 172.16/12, 192.168/16 }
nat on $ext_if from 192.168.100.0/24 to ! <private> -> ($ext_if:0)
nat on $ext_if from <jails> to any -> ($ext_if:0)
rdr-anchor "rdr/*"


#rdr via ipv4 to mail
rdr pass on $ext_if proto tcp from any to ($ext_if) port { 25, 465, 587, 143, 993 } -> 192.168.100.25

#rdr via ipv4 to nginx-proxy
rdr pass on $ext_if proto tcp from any to ($ext_if) port { 80, 443 } -> 192.168.100.10

block in all

#PASS ICMP
pass inet proto icmp icmp-type $icmp_types

pass out quick keep state
antispoof for $ext_if inet


# Allow incoming SSH (port 8199)
pass in inet proto tcp from any to any port $host_ssh_port flags S/SA keep state

I would like to implement rate litiming for all email ports, but so far I am not able to properly config PF. As far as I know, when I have such config with jails and NAT, I cannot directly implement rate limiting to "rdr on". So what are my options here?
 
Don't use rdr pass, it causes all other rules to be skipped. Split this up with a separate rdr and pass rule.

Code:
     If the pass modifier is given, packets matching the translation rule are
     passed without inspecting the filter rules:
 
OK, I found the way of doing it... I think, at least it is working, not sure if the is the most performant way of doing it:


Code:
# Main Variables
ext_if = "vtnet0"
host_ssh_port = "8199"
icmp_types = "{ echoreq unreach }"

# Jail Variables
jail_proxy = "192.168.100.10"
jail_mail = "192.168.100.25"

set block-policy drop
scrub in on $ext_if all fragment reassemble max-mss 1440
set skip on lo
set skip on bridge0

# Bastille Jails tables handling Jails' IPs
table <jails> persist

# IPv4 private address ranges
table <private> const { 10/8, 172.16/12, 192.168/16 }

# Cloudflare IP ranges table
table <cloudflare> persist

# NAT rules
nat on $ext_if from 192.168.100.0/24 to ! <private> -> ($ext_if:0)
nat on $ext_if from <jails> to any -> ($ext_if:0)

# Redirect rules
rdr-anchor "rdr/*"

# Redirect mail traffic to mail jail
rdr on $ext_if proto tcp from any to ($ext_if) port { 25, 465, 587, 143, 993 } -> $jail_mail

# Redirect HTTP/HTTPS traffic to nginx-proxy
rdr on $ext_if proto tcp from any to ($ext_if) port { 80, 443 } -> $jail_proxy

# Default blocking policy
block in all

# Allow ICMP traffic
pass inet proto icmp icmp-type $icmp_types

# Allow outgoing traffic
pass out quick keep state

# Antispoofing
antispoof for $ext_if inet

# Allow incoming SSH
pass in inet proto tcp from any to any port $host_ssh_port flags S/SA keep state

# Allow HTTP and HTTPS traffic only from Cloudflare IPs
pass in on $ext_if proto tcp from <cloudflare> to $jail_proxy port { 80, 443 } flags S/SA keep state

# Allow Email traffic only from anyCloudflare IPs
pass in on $ext_if proto tcp from any to $jail_mail port { 25, 465, 587, 143, 993 } flags S/SA keep state
 
One thing..

Are you running this server for yourself, small team or global?
If yourself or small team, consider to change 465, 587, 143 and 993 from the open world. You don’t need to have this exactly ports for you clients, you just get a lot of attacks. You only need port 25 to be on 25.
If this is a global server, I understand.

And do you really need all 465, 587, 143 and 993?
 
… not sure if the is the most performant way of doing it:

set block-policy drop
scrub in on $ext_if all fragment reassemble max-mss 1440
set skip on lo
set skip on bridge0

# Default blocking policy
block in all

# Allow outgoing traffic
pass out quick keep state

# Antispoofing
antispoof for $ext_if inet

# Allow incoming SSH
pass in inet proto tcp from any to any port $host_ssh_port flags S/SA keep state

# Allow HTTP and HTTPS traffic only from Cloudflare IPs
pass in on $ext_if proto tcp from <cloudflare> to $jail_proxy port { 80, 443 } flags S/SA keep state

Here are some performance optimizations you can make to your pf.conf packet filter firewall configuration.

man pf.conf (for details on this stuff).

Rearrange your declarations in the order that results in the elimination of unnecessary packet processing, like so…

First, add the "set ruleset-optimization" option, right above your set skip…

# enable profiling to optimize the ruleset (none | basic | profile)
# use "none" (the default) when developing rules so you can see
# what you're doing, then try "basic" or "profile" for production.
# basic will probably work fine for you, you might find that the profile option
# leads to errors you might not be able to debug before the heat death of the universe.

### Options - the "set" options should be above all rules.

set ruleset-optimization basic

set skip
- this option in particular should appear in the pf.conf file above any and all the rules, because it prevents the need for pf processing anything on the interfaces you skip (usually only the loopback interface, but looks like in your case also a bridge)

### Traffic Normalization
# NOTE: pf wants scrub normalization *before* antispoof,
# I'm not entirely certain why, so leaving that question for future pondering

scrub - only normalize packets you don't skip

antispoof - next drop any obvious martians before other processing


### Filtering Rules - The order of certain basic rules might have performance implications, as follows.

default block rule (block in all or whatever) - your rules section should start with this, immediately after the scrub, because pf is a last-match-wins firewall, and putting this *after* other rules or in the middle of filtering rules will cause problems at some point, and makes it more difficult to reason about the firewall rules

pass in quick - add the quick for the ssh rule, and the cloudflare rules, and move them up the list


Further optimizations are probably possible but these are basic optimizations that should work for you.

Best of luck to you on your pf packet filtering firewall journey! 🍀

EDIT: I originally had the scrub and antispoof normalizations reversed, and pf seems to enforce scrub-before-antispoof.
 
Back
Top