Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!


Shells Virtual Desktop
BMail.ag - Secure Email Service
Server.net
CPLicense.net
VPS Server
Buy VPN
Vultr
VMs for AI
HostDare
HostDare
ReliableSite White-Label Dedicated Hosting for Resellers
25% Recurring Discount on NVMe VPS
InterServer VPS
BMail.ag - Secure Email Service
Best VPN
High-Performance Bare Metal Server Solutions
Karvl.com
Server Mania Cloud Hosting
DataWagon Hosting
AlphaVPS Hosting
Evoxt.com
Clouvider
VPS Hosting with NVMe
Residential IPs in the US & 4G Mobile Proxies in EU & US with Unlimited Bandwidth
ReliableSite White-Label Dedicated Hosting for Resellers
Rabisu - Hosting Solutions
CloudLinux
Shells Virtual Desktop
New on LowEndTalk? Please Register and read our Community Rules.

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)

forestforest Member
edited 3:18AM in Tutorials

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

  • buggedoutbuggedout Member

    Nice.... will learn nft soon. I have always used just ufw.

    Thanked by 2oloke forest
  • forestforest Member
    edited 2:54AM

    @buggedout said:
    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.conf with the contents:

    flush ruleset
    
    chain input {
            type filter hook input priority filter; policy drop;
            iif lo accept
            ct state { established, related } accept
            tcp dport { ssh, http, https } accept # or whatever services you want
    }
    
    chain output {
            type filter hook output priority filter; policy accept;
    }
    
    chain forward {
            type filter hook forward priority filter; policy drop;
    }
    

    And that's all. Now you allow SSH, HTTP, and HTTPS traffic in. No port knocking in that example, though.

    Thanked by 1buggedout
  • rpqurpqu Member

    Hmm, what if replay?

  • forestforest Member
    edited 3:08AM

    @rpqu said:
    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 .profile that automatically randomizes the ports in /etc/nftables.conf and 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.

    Thanked by 2rpqu buggedout
  • Nice tutorial. What sshd config settings should I be aware of that might cause self-lockout?

  • forestforest Member

    @Motion3549 said:
    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 accept to the input chain. That defeats the purpose of port knocking, but it'll let you make sure it works before you remove that line.

  • forestforest Member
    edited 6:07AM

    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 { to set knock_stage_2 { and change the second set knock_stage_3 { to set 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.

  • JohnnySacJohnnySac Member

    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.

    Thanked by 1forest
  • nghialelenghialele Member

    Nice tuto man!

    I will just change ssh port to 65432 and call it a day.

  • forestforest Member

    @nghialele said: 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.

    Thanked by 1nghialele
  • nghialelenghialele Member

    I’m joking here man, my port number is low. šŸ˜­šŸ˜‚

    Thanked by 1forest
  • mans_xdmans_xd Member

    @nghialele said:
    I’m joking here man, my port number is low. šŸ˜­šŸ˜‚

    is that a 67

  • @forest said:

    @Motion3549 said:
    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 accept to the input chain. That defeats the purpose of port knocking, but it'll let you make sure it works before you remove that line.

    Thanks, I'll implement this for my OCI idler.

  • RiedeRiede Member

    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.

  • forestforest Member

    @Riede said:
    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.

  • dbadudedbadude Member

    @forest said:
    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 { to set knock_stage_2 { and change the second set knock_stage_3 { to set 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.

    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.

  • forestforest Member

    @dbadude said: Please give me the new script as a whole.

    Fixed config:

    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_2 {
                    type ipv4_addr
                    flags timeout
                    timeout 10s
            }
    
            set knock_success {
                    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;
            }
    }
    
    Thanked by 3atkl malikshi dbadude
  • rpqurpqu Member

    Hmmm, port knocker with totp derivation

  • malikshimalikshi Member

    Knocker, so place that ruleset. And connect ssh port 22 like usually?

  • forestforest Member

    @malikshi said:
    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.

  • malikshimalikshi Member

    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

  • forestforest Member
    edited 8:55AM

    @malikshi said:
    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:

    tcp dport ssh ct status dnat ip saddr @knock_success delete @knock_success { ip saddr } accept
    

    Into this:

    tcp dport ssh ct status dnat ip saddr @knock_success accept
    

    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.

Sign In or Register to comment.