The setup is like that:
Internet (public IP) — ISP router — FreeBSD 13.1 host — OpenBSD 7.2 virtual machine
the OpenBSD is running a) ssh server with Yubikey authorization, b) Wireguard VPN server, c) L2TP VPN as a back-up on the Wireguard. The ISP router is forwarding ports tcp 22 (ssh), tcp 443 (Wireguard), udp 400 and 4500 for the L2TP VPN to the OpenBSD address directly.
The pf.conf rules to make this setup work:
[root@openbsd dilyan]$ cat /etc/pf.conf
set block-policy return
set limit table-entries 400000 #due to pfbadhost
set skip on lo
ext_if = “vio0”
vpn_if = “pppx”
vpn_net = “10.0.0.0/24”
home_net = “192.168.0.0/24” #home network thru VPN
office_net = “192.168.1.0/24” # office network
table <bruteforce> persist #table for ssh-attackers
table <pfbadhost> persist file “/etc/pf-badhost.txt”
antispoof for { $ext_if, $vpn_if }
match in all scrub (no-df random-id )
block return # block stateless traffic
block quick from <bruteforce> #block the ssh-attackers
block in quick on egress from <pfbadhost>
block out quick on egress to <pfbadhost>
# By default, do not permit remote connections to X11
block return in on ! lo0 proto tcp to port 6000:6010
# Port build user does not need network
block return out log proto {tcp udp} user _pbuild
block all
pass # establish keep-state
pass inet proto icmp synproxy state #allow ICMP protocol – all
pass proto { tcp, udp } from { wg0:network, $home_net, $vpn_net, $office_net } to port { ssh, domain }
pass proto { tcp, udp } to port { ssh, 500, 4500 } keep state (max-src-conn 5, max-src-conn-rate 3/60, overload <bruteforce> flush global)
pass proto udp from {$ext_if, $office_net} to port { 123 } #allow network time protocol (NTP) port
# Rule for WireGuard VPN to NAT traffic to public net#
match out on $ext_if from wg0:network to any nat-to $ext_if
# Rules for L2TP VPN to work and NAT traffic #
pass on $ext_if proto esp # allow ESP protocol on public interface
pass on $ext_if proto udp to port { isakmp, ipsec-nat-t } # allow isakmpd UDP traffic through the public interface on ports 500 and 4500
pass on enc0 keep state (if-bound) # filter all IPSec traffic on the enc interface
pass on $vpn_if from { $vpn_net, $home_net } # allow all trafic in on and out to the VPN network
pass on $vpn_if to { $vpn_net, $home_net }
match out on $ext_if from $vpn_net to any nat-to $ext_if # nat VPN traffic going out on the public interface with the public IP
These rules will _not_ work on FreeBSD, as their implementation requires grouping the rules in particular order – Macros, Tables, Options, Normalization, NAT/Redirections and then Filtering (i.e. you need to rearrange the rules).
Once you open port 22 for ssh service on a public IP address, attackers will start attempts like ~17-20k tries per day. This is why I implemented the table, so an IP that tries to guess a username/password more than 3 times per 60 minutes to be blocked for a day. This reduced the attacks to less than 12k per day. Then I followed this how-to https://geoghegan.ca/pfbadhost.html to block malicious IPs thru the table and the attempts dropped below 9k per day.