Port knocking is mostly a bad idea. But people keep wanting to do it, for some false sense of security. If you don't consider it a security control but a way to keep garbage out of your logs, it might be valid. In my case I'm using an old USG Pro 4 running OpenBSD as my firewall and I'd prefer to avoid writing stuff to the logs, as I'd prefer the flash not to wear out sooner than needed, definitely not thanks to background radiation on the internet.
Here is a pf.conf fragment using the OpenBSD 7.9 source limiter feature:
# Chosen by fair dice roll
knock1 = "24601"
knock2 = "29202"
# no need for knocking for these hosts
table <good-hosts> persist {
192.0.2.0/24 # replace with whatever you trust
}
table <stage1-passed> persist {}
table <stage2-passed> persist {}
table <bad-hosts> persist {}
source limiter "stage1" id 1 entries 1000 \
limit 2 rate 10/100 \
table <stage1-passed> above 1
source limiter "stage2" id 2 entries 1000 \
limit 2 rate 10/100 \
table <stage2-passed> above 1
source limiter "bad" id 3 entries 10000 \
limit 2 rate 10/100 \
table <bad-hosts> above 1
# ssh port knocking
anchor to self {
pass in quick proto tcp from {<good-hosts> <stage2-passed>} to port {>= 1024, 22}
block return-rst in quick proto tcp from <bad-hosts>
block return-rst in quick proto tcp to port 22
pass in quick proto tcp to port $knock1 source limiter "stage1" (no-match)
pass in quick proto tcp from <stage1-passed> to port $knock2 source limiter "stage2" (no-match)
# source limiter needs a "pass" rule, ensure you have rules to block access
# to ports >= 1024 you need to protect.
pass in proto tcp to port >= 1024 source limiter "bad" (no-match)
block return-rst proto tcp to port >= 1024
}
Once this was configured, I had no more ssh brute force attempts in the logs:
$ zgrep 'Jun 2' /var/log/* 2>/dev/null
9
Ah, peaceful 🧘♂️
Using return-rst means that it is harder to observe when the host has been
blocked, essentially turning the source limiter into a thing which does not
block anything but instead sets state.
Configuring the ssh client
To get into this you need to hit the source limiter twice, for each port. We can use OpenSSH's Match tagged keyword to make this nicer.
Add something like this to the end of ~/.ssh/config:
Match Final Tagged knock Exec "telnet %h 24601; telnet %h 24601; telnet %h 29202; telnet %h 29202; true"
Then use it with ssh -P knock your-host. You should see 4 connection refused
lines from telnet, then your SSH connection.
Alternatively rather than using -P knock, you can use the LocalNetwork
match to make this happen automatically depending which network you are on.
Match Final LocalNetwork !10.x.y.0/24 Host *.domain Exec "telnet %h 24601; telnet %h 24601; telnet %h 29202; telnet %h 29202; true"
(Unfortunately because of the ssh config parser that has to be on one line.)
The limits are arranged so a host is more likely to get blocked than accidentally find the ports, even via scanning. However this shouldn't be treated as a security control, it's mostly a way to stop clogging the logs with scans, without having to run yet another daemon to do it.