Pf

On a workstation where you are the only user, you can use a very simple /etc/pf.conf:

set skip on lo0 # don't filter localhost packets
ext_if = "em0" # replace em0 with your external interface

set block-policy drop # by default, drop packets. You can also set block-policy reject
set loginterface $ext_if # log that interface

block all  # block all traffic by default
pass in inet proto icmp icmp-type 8 code 0 # icmp packets
pass in inet proto icmp icmp-type 3 code 4 # icmp needfrag (MTU)
pass in inet6 proto ipv6-icmp icmp6-type {2 128} keep state
pass out all # pass all outgoing traffic

This will allow the necessary ICMP traffic (useful for network diagnosis) while blocking all other incoming connections.

(As a general rule, the last matching rule determines the action.)

I generally don't whitelist by IP addresses because I've had times where I needed to access a system from a different IP. I also avoid OS fingerprinting because, although it is available, it's not 100% accurate.

To load the ruleset once you've edited it, run:

$ doas pfctl -f /etc/pf.conf

To disable the firewall (useful for diagnosing the network), run:

$ doas pfctl -d

To enable it again:

$ doas pfctl -e

For a server, you will want to, at a minimum, allow incoming ssh packets:

set skip on lo0 # don't filter localhost packets
ext_if = "em0" # my external interface is em0

set block-policy drop # by default, drop packets. You can also set block-policy reject
set loginterface $ext_if # log that interface

pass in proto tcp from 192.168.1.1 to port ssh
pass in inet proto icmp icmp-type 8 code 0 # icmp packets
pass in inet proto icmp icmp-type 3 code 4 # icmp needfrag (MTU)
pass in inet6 proto ipv6-icmp icmp6-type {2 128} keep state
pass out all # pass all outgoing traffic

Replace 192.168.1.1 with your IP.

As a general rule, your servers should also accept incoming http and https connections. This is necessary for running a web server and also for acquiring a properly signed SSL certificate. Here is the /etc/pf.conf:

set skip on lo0 # don't filter localhost packets
ext_if = "em0" # my external interface is em0

set block-policy drop # by default, drop packets. You can also set block-policy reject
set loginterface $ext_if # log that interface

pass in proto tcp from 192.168.1.1 to port ssh
pass in inet proto icmp icmp-type 8 code 0 # icmp packets
pass in inet proto icmp icmp-type 3 code 4 # icmp needfrag (MTU)
pass in inet6 proto ipv6-icmp icmp6-type {2 128} keep state
pass in proto tcp to port {http https}
pass out all # pass all outgoing traffic

To see how many packets are arriving:

$ doas pfctl -f /etc/pf.conf

This will empty the existing state tables for pf. Then, run

$ doas pfctl -sr -v

This will show you how many packets are arriving. Since you emptied the state tables, if you now see 1000s of packets coming in, those packets came in the last few seconds, indicating that you are certainly under attack.