Aggressive Ssh Protection With Pf
This pf(4) configuration mostly comes from Tim’s blog, and a bit of change in order to work on FreeBSD 13.1 which also works on FreeBSD 14.3.
For the IP blocks, IPDeny.com provides regularly-updated lists of IPv4 & IPv6 ranges for each country, you can download it and put it under somewhere like /user/local/share/geoip.
user$ DEST="/usr/local/share/geoip"
user$ doas mkdir -p "$DEST/tmp"
user$ doas chmod 755 "$DEST"
user$ doas chmod 750 "$DEST/tmp"
Then download the IP blocks you want as below:
user$ DEST="/usr/local/share/geoip"
user$ V4URL="https://www.ipdeny.com/ipblocks/data/aggregated/cn-aggregated.zone"
user$ V6URL="https://www.ipdeny.com/ipv6/ipaddresses/aggregated/cn-aggregated.zone"
user$ V4DEST="$DEST/ipv4_cn-aggregated.zone"
user$ V6DEST="$DEST/ipv6_cn-aggregated.zone"
user$ ftp -o "$V4DEST" "$V4URL"
user$ ftp -o "$V6DEST" "$V6URL"
One more word for SSH protection, it’s better to forbit Root login as well as Password login, this can be done in file /etc/ssh/sshd_config.
cat /etc/pf.conf
##########
# Macros #
##########
# see also `sysctl net.inet.ip.portrange.{first,last}`
minefield="10000:65535"
ssh_alternate_port=11222
##########
# Tables #
##########
table <bruteforce> persist
table <troublemakers> persist
table <domesticv4> persist \
file "/usr/local/share/geoip/ipv4_cn-aggregated.zone"
table <domesticv6> persist \
file "/usr/local/share/geoip/ipv6_cn-aggregated.zone"
###########
# Options #
###########
set skip on lo
#########
# Rules #
#########
block return # block stateless traffic
pass # establish keep-state
# block brute-forcers
block quick proto tcp from <bruteforce> \
to any port $ssh_alternate_port
block quick proto tcp from <troublemakers> \
to any port $ssh_alternate_port
# non-domestic v4/v6 connections simply not allowed to touch SSH
# IPv4 GeoIP blocks: https://www.ipdeny.com/ipblocks/data/aggregated/cn-aggregated.zone
# IPv6 GeoIP blocks: https://www.ipdeny.com/ipv6/ipaddresses/aggregated/cn-aggregated.zone
#
block quick inet proto tcp \
from ! <domesticv4> \
to any port $ssh_alternate_port
block quick inet6 proto tcp \
from ! <domesticv6> \
to any port $ssh_alternate_port
# these made it through to the SSH port but abusing it
pass proto tcp from any to any \
port {$ssh_alternate_port} \
flags S/SA keep state \
(max-src-conn 5, max-src-conn-rate 5/5, \
overload <bruteforce> flush global \
)
## these are just randomly probing
# port 22 is a dead-giveaway
# because we know we moved our SSH server elsewhere
# Also, we have no telnet or SMB
# so anyone poking there
# is up to no good
pass in proto tcp \
from any \
to any port { \
telnet, \
ssh, \
pop3, \
netbios-ns, \
netbios-ssn, \
microsoft-ds \
} \
synproxy state \
tag trouble
# tag stuff in the range $minefield as trouble
pass in proto tcp from any to any port $minefield \
synproxy state \
tag trouble
# unless it's to our hole-in-one
pass in proto tcp from any to any port $ssh_alternate_port tag good
# if we're still trouble, add to the troublemakers
pass proto tcp from any to any port $ssh_alternate_port \
tagged trouble \
synproxy state \
(max-src-conn 1, max-src-conn-rate 1/10, \
overload <troublemakers> flush global \
)
pass proto tcp from any to any port {http https} \
keep state (max-src-conn 100, max-src-conn-rate 20/3)