Don't touch me there: An aggressive pf(4) filtering setup
If some unknown person came up to your house and started trying to open each window, jiggling each door handle, and punching in random codes on your garage-door opener, you could safely assume that they wanted trouble and that you should stop them. Similarly, if some unknown computer on the internet started probing at ports on your server where you offer no services, it should trip alarm-bells and you should cut off this miscreant from interacting with your server. Especially SSH access. You know what services your machine provides. If anybody attempts to connect to some other service on your machine, in all likelihood they don't have your best interests in mind.
In this post,
we configure
pf
on
OpenBSD
to catch inappropriate connection-attempts
and block the offender.
Assumptions
This post assumes a few things about you:
- OS installed that can run
pf
-
While I wrote this article using
pf
on OpenBSD, many of the aspects should translate to FreeBSD even though FreeBSD's version ofpf
represents an older snapshot from when their codebases diverged. Particularly, I found that FreeBSD'spf
lacks thematch
keyword. Substitutingpass
formatch
might make those work, but I haven't tested that. - Administrative rights
- This should go without saying, but if you don't administer the machine, you can't modify the firewall configuration.
- Basic CLI
-
In this case,
we have minimal work to do at the command-line
but it helps to know your way around.
OpenBSD has a template
/etc/examples/pf.conf
you might want to copy as a starting-point; you might find it easier tocd
into/etc
directory; or you might usetmux
andssh
to access the server. Note that editingpf.conf
can leave your server inaccessible (like cutting off the tree-branch while sitting on it), so you'll want to make sure that you have console access — locally, via a serial connection/KVM, or VNC connection. - Basic networking knowledge
- The basic networking ideas of UDP, TCP, ports, services, etc. If you need to get up to speed here you might want a resource like Michael W. Lucas's Networking for System Administrators, O'Reilly's TCP/IP Network Administration, or some other such book.
- Edit text-files
-
This focuses primarily on modifying your
pf.conf
which you can edit them with your favorite$EDITOR
, whethernano
,emacs
/mg
,vi
/vim
/neovim
,ed
,cat
, a magnetized needle and a steady hand, or butterflies. But this guide assumes that you can edit text files and save them.
Setup
For purposes here,
I'll use OpenBSD's
doas
command to elevate privileges
but you can use
sudo
if you have it installed on FreeBSD
or execute these commands
under a
su -
to
root
if you like to live dangerously.
This assumes that
doas
(or
sudo
)
has a configuration that lets you run the commands in question.
This also assumes starting with an empty
(or mostly-empty if you did
doas cp /etc/examples/pf.conf /etc/
)
pf.conf
file.
If you already have an existing configuration,
you'll need to work out how these changes integrate
with your existing setup.
Goals
This post covers several tricks to protect the actual SSH port.
- change port on which SSH listens
- flag any IP addresses that touch our server's minefield and prevent them from accessing SSH
- if anybody actually finds the SSH port and hammers on it, ban them too
- limit the country/countries from which we can SSH
Change the
sshd
port number
For purposes of this post,
I configure
sshd
to listen on port
2345
but you should choose
a different one that suits you.
This doesn't give much
in the way of security,
but it reduces some of the noise
in your log-files
and makes the
minefield
work by making it unknown
and surrounded by trip-ports.
Change the listening port
Edit your
/etc/ssh/sshd_config
file and specify the
Port
by adding/editing the line to read
Port 2345Save the file and restart
sshd
doas rcctl restart sshdNote: If you make this configuration change while SSHed into the server, you might cut off your access. As mentioned, make sure that you have console, serial, or VNC access in case you need to recover from making an error here.
Change the default connectin port
On your local machine,
I find it painful to remember
how to specify the port to various
ssh
commands.
Some take
-p $PORTNUM
,
some take
-P $PORTNUM
,
and yet others take
-o Port=$PORTNUM
.
To simplify this,
edit (or create) your
~/.ssh/config
to specify the port-number there
host example HostName example.com Port 2345Now
ssh
,
scp
,
sftp
,
rsync
,
etc,
should all default to the right port-number
without you needing to specify it explicitly.
Creating a minefield
Rate-limiting connections
Limiting by country
To begin with, I know that I will almost certainly never connect to SSH from outside the United States. IPDeny.com provides regularly-updated lists of IP-ranges for each country that I can use as a rough determination of country-of-origin. Yes, this can get thrown off by VPNs but it cuts off a surprising number of attackers.
Create a
_geoip
user
To limit attack surface,
I create a
_geoip
user to perform the downloading
cron
task nightly.
I put this user in the "daemon" login-class,
specify an empty password,
and then disable password logins
doas adduser _geoip Use option ``-silent'' if you don't want to see all warnings and questions. Reading /etc/shells Check /etc/master.passwd Check /etc/group Ok, let's go. Don't worry about mistakes. There will be a chance later to correct any input. Enter username []: _geoip Enter full name []: GeoIP downloader Enter shell csh git-shell ksh nologin sh [ksh]: Uid [1002]: Login group _geoip [_geoip]: Login group is ``_geoip''. Invite _geoip into other groups: guest no [no]: Login class authpf bgpd daemon default pbuild staff unbound [default]: daemon Enter password []: Disable password logins for the user? (y/n) [n]: y Name: _geoip Password: **** Fullname: GeoIP downloader Uid: 1002 Gid: 1002 (_geoip) Groups: _geoip Login Class: daemon HOME: /home/_geoip Shell: /bin/ksh
Create a script to download & install GeoIP info
First, create the folders that
pf
will look in to find the GeoIP tables.
Create a
tmp/
directory within so that
we can download there,
compare the results with what we already have,
and move the new data atop the old atomically:
DEST="/usr/local/share/geoip" doas mkdir -p "$DEST/tmp" doas chown _geoip:_geoip "$DEST{,/tmp}" doas chmod 755 "$DEST" doas chmod 750 "$DEST/tmp"I switch to the
_geoip
user and create the script to use
doas su - _geoip mkdir -p ~/bin touch update_zone.sh chmod +x update_zone.shI then used my
$EDITOR
to populate that
update_zone.sh
file with the following script:
For FreeBSD, you would usefetch
,curl
, orwget
, instead offtp
and adjust the parameters accordingly.#TODO cron job
#TODO configure doas/sudo to let _geoip restart pf
For the first
NOTES: - use `overload` to put offenders in a table - need to know valid ports - using geo-blocking - while can use for UDP, can spoof the sender, so best to stick to TCP - ipdeny.com has country information https://www.ipdeny.com/ipblocks/ OpenBSD port range: sysctl net.inet.ip.port{,hi}{first,last} FreeBSD port range: sysctl net.inet.ip | grep -e first -e last