All new Registrations are manually reviewed and approved, so a short delay after registration may occur before your account becomes active.
Actually effective port knocking with nftables (a simple guide)
Just figured I'd share a technique to hide SSH behind a series of port knocks. While port knocking really isn't as relevant as it used to be, it's fun and can be a bit useful in reducing exposure of SSH's pre-auth attack surface area (however low it may already be). It's more effective than simply running SSH on an unusual port.
flush ruleset
define knock_port_1 = 673
define knock_port_2 = 22110
define knock_port_3 = 4261
define ssh_nat_port = 18766
table inet filter {
set knock_stage_1 {
type ipv4_addr
flags timeout
timeout 10s
}
set knock_stage_3 {
type ipv4_addr
flags timeout
timeout 10s
}
set knock_stage_3 {
type ipv4_addr
flags timeout
timeout 10s
}
chain prerouting {
type filter hook prerouting policy raw; policy accept;
tcp flags syn jump knock_check
}
chain prerouting_nat {
type nat hook prerouting priority dstnat; policy accept;
tcp dport $ssh_nat_port redirect to :ssh
}
chain knock_check {
# Knock sequence
ip saddr @knock_success tcp dport $ssh_nat_port return
ip saddr @knock_stage_2 tcp dport $knock_port_3 add @knock_success { ip saddr } goto knock_reset
ip saddr @knock_stage_1 tcp dport $knock_port_2 add @knock_stage_2 { ip saddr } delete @knock_stage_1 { ip saddr } return
tcp dport $knock_port_1 goto knock_start
# Wrong port knock_success, reset
ip saddr @knock_success delete @knock_success { ip saddr } return
ip saddr @knock_stage_2 goto knock_reset
ip saddr @knock_stage_1 goto knock_reset
}
chain knock_reset {
delete @knock_stage_1 { ip saddr }
delete @knock_stage_2 { ip saddr }
}
chain knock_start {
jump knock_reset
add @knock_stage_1 { ip saddr }
}
chain input {
type filter hook input priority filter; policy drop;
iif lo accept
ct state { established, related } accept
tcp dport ssh ct status dnat ip saddr @knock_success delete @knock_success { ip saddr } accept
}
chain output {
type filter hook output priority filter; policy accept;
}
chain forward {
type filter hook forward priority filter; policy drop;
}
In this particular example, SSH will be hidden until TCP ports 673, 22110, and 4261 are knocked (sent a single TCP SYN) in that order by the same IP within 10 seconds of each and other without any other ports being touched. After that, SSH will be exposed on port 18766, accepting a single connection from the knocking IP for 10 seconds. Note that this filter only allows SSH and nothing else. If you want to whitelist, say, a web server, you'd add tcp dport { http, https } accept to the input chain (it won't be hidden behind a knock sequence).
Knocking can be done with a program like netcat, or by using the SSH client with a timeout set low enough that only one SYN is sent but high enough that the final connection doesn't time out. For example:
for port in 673 22110 4261 18766; do ssh -o ConnectionTimeout=1 -p $port 198.51.100.14; done
Unlike all other port knocking techniques for nftables that I've seen, this is the only one that does all of:
- Restricts success to only the knocking IP: This prevents other IPs from attempting to access SSH in the window of time between a successful knock sequence and the open port closing.
- Resets the knock chain if any incorrect port is knocked by the knocking IP: This prevents an attacker from succeeding by quickly "sweeping" all possible ports in each 10 second window to guarantee that they hit the correct one each time. Surprisingly, even the port knocking examples on the nftable wiki don't do this.
- Has an explicit timeout that resets for each port knocked: This prevents nftables from accumulating a large number of entries in memory and prevents you from leaving a single IP in an almost-unlocked state if the sequence is interrupted early.
- Only allows one connection to SSH from the knocking IP upon success: This prevents an attacker positioned on the same connection from piggybacking on your success without you knowing (if they connect second, they are denied but if they connect first, you are denied and therefore alerted).
Done properly, any give IP only has a little over a 1 in 2^64 chance (more precisely, a 1 in 65535^4 chance) to succeed on each try if they guess ports at random. One possible improvement might be to require a minimum wait time between knocks so that the sequence is reset if even the correct next port is knocked if it is knocked too fast. Although even with 1000 IPs knocking 1000 ports per second each, it would still take over a quarter of a million years just to have a 50% chance of success, so trying to slow it down further would be overkill.


Comments
Nice.... will learn nft soon. I have always used just ufw.
Although ufw is definitely easier, if your only needs are the typical "allow all out, allow related/established and whitelised ports in", then all you'd need is to install nftables, set it to auto-start with
systemctl enable nftables.service, and create/etc/nftables.confwith the contents:And that's all. Now you allow SSH, HTTP, and HTTPS traffic in. No port knocking in that example, though.
Hmm, what if replay?
Then you change the sequence after a successful login. Nftables can't do that automatically. It'd be pretty easy to do by having something in
.profilethat automatically randomizes the ports in/etc/nftables.confand then prints the new knock sequence for you to save on each login.I personally use this as an emergency backup, since normally I only connect to SSH through an onion service, but if the Tor daemon is down and I can't connect, I use port knocking to be able to get directly to SSH (without having to expose it). Since that's relatively rare, I can afford to manually change the knock sequence each time I connect that way.
Nice tutorial. What sshd config settings should I be aware of that might cause self-lockout?
None. This just expects sshd to be listening on port 22. The nftables rules handle the DNAT.
To just test things out so you know it works for you without the risk of locking yourself out, add
tcp dport ssh acceptto the input chain. That defeats the purpose of port knocking, but it'll let you make sure it works before you remove that line.Ah shit, I made a functional typo in the post and now I can't edit it anymore.
@angstrom I hate to ask, but could you change the first
set knock_stage_3 {toset knock_stage_2 {and change the secondset knock_stage_3 {toset knock_success {? It's supposed to be (bold indicates changes):set knock_stage_1 { type ipv4_addr flags timeout timeout 10s } set knock_stage_2 { type ipv4_addr flags timeout timeout 10s } set knock_success { type ipv4_addr flags timeout timeout 10s }And then add a
}to the very end of the post so it ends in:type filter hook forward priority filter; policy drop; } }I really wish we could edit posts for longer.
Nice! I'm a big fan of port knocking, in my experience it has worked really well. I use KiTTY SSH client which has a port knocking setting that makes it really easy to connect once setup.
Nice tuto man!
I will just change ssh port to 65432 and call it a day.
But then you're removing the protections of CAP_NET_BIND_SERVICE, and, assuming 65432 was just an example and the actual number is between 32768 and 60999, you also risk colliding with the ephemeral port range.
Iām joking here man, my port number is low. šš
is that a 67
Thanks, I'll implement this for my OCI idler.
Thank you for tricks seems pretty useful!! Still use iptables for simple use really wish to learn others firewall, btw do you have recommended books for deep diving about nftable or some good references, I know I can ask ai but for me Self-learning experience is more important and enjoyable and also recommendations from people are also an important point for me to start.
I think nftables is actually fairly poorly documented, unfortunately. There's the nftables wiki that gives the basics, but it's quite incomplete and lacks many examples. What I'd recommend is using iptables-translate to convert your iptables rules into nftables rules. The translation isn't exactly optimized, but it'll definitely help you learn.
> set knock_stage_1 { > type ipv4_addr > flags timeout > timeout 10s > } > > set knock_stage_2 { > type ipv4_addr > flags timeout > timeout 10s > } > > set knock_success { > type ipv4_addr > flags timeout > timeout 10s > } >Please give me the new script as a whole. I find this nftable script interesting, as i don't need to install a knockd daemon. Less is always more in terms of system administration.
Fixed config:
Hmmm, port knocker with totp derivation
Knocker, so place that ruleset. And connect ssh port 22 like usually?
You'd connect to whatever you have in
ssh_nat_port. In this particular example, you'd connect to SSH on TCP/18766 instead.I won't get knocked out right? How many connection can establish for single ip sources. My bot often sshpassing servers we had like 1-30 concurrent at time
One connection per knock attempt, so once you "use up" a successful knock sequence and connect, you just have to do it again before you connect a second time. Turn this:
Into this:
And now it should allow unlimited connections to that port from the successfully-knocking IP for 10 seconds, rather than invalidating it as soon as a successful connection is made.