Making fwknopd work with fw4

Introduction

The Firewall Knock Daemon, fwknopd, does not work out of the box with openwrt-24.10.

This is because fwknopd does not support nftables, which is the core technology of fw4 and the modern way to manage network packets, datagrams and frames. Fortunately fwknopd provides the means to run custom open and close commands which we can use to manage nftables-based firewall rules.

Overview

This post describes how to set up port-forwarding using fwknopd. It uses luci (the web interface) for most of the configuration, meaning that you get to manage your port-forwarding using standard OpenWrt techniques. This means you can easily see and modify your firewall setup using the web interface. Traditionally, port-forwards managed by fwknopd were defined solely in the fwknopd configuration and did not appear in the firewall web interface.

Our approach is to use IP Sets with built-in timeouts to allow port-forwarding on a conditional basis.

When fwknopd authenticates a user, that user’s IP address and the port to which they require access is added to the IP Set. Our port-forwarding rules check incoming packets against the IP Set, and only allow those from the appropriate IP address that are requesting the appropriate port. And since entries in the IP Set automatically time-out after a short period no further action is required to subsequently disable further access.

Step 1 is to create IP Sets, which we will use for conditional port-forwarding. Step 2 is to set up the conditional port-forwarding based on those IP Sets. Finally, Step 3 is to set up fwknopd. The fwknopd configuration will be done outside of luci and uci, which is to say, we will configure it manually using its standard configuration files.

Note that simply opening local ports using fwknopd should be doable in a similar way but I haven't tried it.

Step 1 - Define IP Sets

We will create two IP Sets, one for tcp and one for udp. Note that you may not actually need both. Note: these two IP Sets can be used for multiple port forwarding rules.

In Network->Firewall select the IP Sets tab.

Press Add. Fill in the pop-up form as follows for tcp connections:

Name: fwknop_allow_6
Comment: Allow tcp connections via fwknop
Family: IPv4
Packet Field Match: src_ip: Source IP
dest_port: Destination Port
Timeout: 30

Press Save

Press Add again. Fill in the pop-up form as follows for udp connections:

Name: fwknop_allow_17
Comment: Allow udp connections via fwknop
Family: IPv4
Packet Field Match: src_ip: Source IP
dest_port: Destination Port
Timeout: 30

Press Save

Press Save & Apply

This will have created 2 IP Sets. These are used to record the source ip address and destination port for connections approved by fwknopd. These records will expire after the timeout period (30 seconds as defined above). Note that the names of the IP Sets include the protocol number: 6 for tcp and 17 for udp. This is necessary for fwknopd to be able to separately identify and allow tcp and udp access.

Step 2 - Set Up Port Forwarding

We will now set up a conditional port forwarding rule, in this case for ssh. This will allow connections from outside the LAN to establish an ssh session to a server within the LAN.

In Network->Firewall select the Port Forwards tab.

Press Add. Fill in the popup form as follows:

Name: Enter something that identifies what the rule is doing.
Something like: SPA_ssh_to_mysshserver.
Restrict to address family: IPV4 only
Protocol: tcp only (deselect udp)
Source Zone: wan (this is where the traffic will be arriving from)
External port: 22 (or whatever works for you)
Internal IP address: The internal ip address of your ssh server.  You
will need to ensure that this address is static within your network.
Internal port: 22 (or whatever port your ssh server uses)

Now select the Advanced Settings tab and fill out the following:

Use ipset: select fwknop_allow_6

Press Save.

You will now be back in the Firewall - Port Forwards page.

Press Save & Apply.

Testing the Port Fowarding Without fwknop

This step is optional for the sole purpose of providing you with a warm fuzzy feeling. You don’t need to do this but it will allow you to establish that your port forwarding rules and IP Sets are working as they should.

For this, you will need to be able to run a terminal session on your router, and have a machine outside your LAN from which you can attempt an ssh session. We will call this the remote machine. You will also have to have enabled the ssh service on your local ssh server.
Replace myhost.com below with your hostname or address.

On the remote machine ensure ssh is not accessible:

me:~$ ssh myhost.com
ssh: connect to host myhost.com port 22: Connection refused
me:~$

On the remote machine, identify your public facing ip address:

me:~$ curl ifconfig.me
123.456.125.77me:~$

Here, our public facing ip address is claimed to be 123.456.125.77

On the router, as root:

root:~# nft add element inet fw4 fwknop_allow_6 \{ 123.456.125.77 . 22 \}
root:~# nft list sets inet 
table inet fw4 {
        set fwknop_allow_6 {
                type ipv4_addr . inet_service
                timeout 30s
                comment "Allow tcp connections via fwknopd"
                elements = { 123.456.125.77 . 22 expires 28s880ms }
        }
        set fwknop_allow_17 {
                type ipv4_addr . inet_service
                timeout 30s
                comment "Allow udp connections via fwknopd"
        }
}
root:~# 

You now have 28.88 seconds to attempt to connect via ssh. So, on the
remote machine:

me:~$ ssh myhost.com
Linux myhost x.y.z-w-amd64 #1 SMP Debian N.M.P-Q (YYYY-MM-DD) x86_64

The programs included with the Devuan GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Devuan GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
You have no mail.
Last login: Thu Mar 26 11:21:31 2026 from 192.168.2.1
me:~$

And we are successfully connected.

Now disconnect, wait 30 seconds and attempt to reconnect. We will
be, again, locked out:

me:~$ ssh myhost.com
ssh: connect to host myhost.com port 22: Connection refused
me:~$

This shows that adding an appropriate element to the IP Setfwknop_allow_6 will temporarily allow connections through the firewall.

Wrinkles

There is a small wrinkle with this setup, which is that once the port forwarding has been defined in fw4, local connections (ie from within the LAN) are able to connect using the domain name without usingfwknop. This appears to be standard policy within openwrt. I find this somewhat surprising but, for me at least, not problematic.

Step 3 - Set Up fwknopd

Now we will set up fwknopd in the simplest way possible for a single accessor. If you need a more sophisticated setup what follows should provide a good starting point. Read the online fwkknopd documentation for more.

Start by installing fwknopd on the router. There is no need to install either luci-app-fwknopd or fwknop.

On the remote machine, as the user that will use ssh, we will generate our keys. The server name and ip address in the following command can be used verbatim. We will rewrite the config file later.

You will need to install fwknop on this machine. Then...

me:~$ fwknop -A tcp/22 -a 1.1.1.1 -D myhost.com --key-gen --use-hmac --save-rc-stanza
[+] Wrote Rijndael and HMAC keys to rc file: /home/mbr/.fwknoprc
me:~$ 

This creates our initial set of keys in .fwknoprc. Securely copy this file, or at least its key definitions, to the router.

Log in to the router using a terminal interface and become root:

root:~# cd /etc/fwknop
root:fwknop# mv access.conf access.conf.orig
root:fwknop# mv fwknopd.conf fwknopd.conf.orig
root:fwknop# 

This creates backup copies of the installed config files. You may want to refer to them later as they contain lots of information in comments.

Now we recreate those config files, to contain the following.

access.conf:

# Access from "remote"
SOURCE                  ANY
REQUIRE_SOURCE_ADDRESS  Y
KEY_BASE64              <COPY THIS FROM THE .fwknoprc FILE CREATED ABOVE>
HMAC_KEY_BASE64         <COPY THIS FROM THE .fwknoprc FILE CREATED ABOVE>
CMD_CYCLE_OPEN          nft add element inet fw4 fwknop_allow_$PROTO { $SRC . $PORT timeout 30s }
CMD_CYCLE_CLOSE         NONE

fwknopd.conf:

PCAP_INTF wan

Reboot the router. Then connect using ssh and become root again.

root:~# ps ww | grep fwkn
 2779 root      3684 S    /usr/sbin/fwknopd --foreground --syslog-enable -i wan
 5742 root      1336 S    grep fwkn
root:~# 

This shows that the fwknopd daemon was successfully started.

If you don't see something like this, take a look at the system logs from the router's web interface: Status->System Log. Look for lines containing fwknopd. This should give you clues about what is going on.

Now switch back to using the "remote" machine.

Edit the .fwknoprc file to look like this:

[myhost.com]
SPA_SERVER                  myhost.com
KEY_BASE64                  xxxxxxxxxxxxxxxxx
HMAC_KEY_BASE64             xxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Check that the key fields match those defined in/etc/fwknop/access.conf on the router.

Now we can attempt ssh connections. We'll start be making sure that connecting without SPA fails:

me:~$ ssh myhost.com
ssh: connect to host myhost.com port 22: Connection refused
me:~$

Now, we'll run fwknop and try again:

me:~$ fwknop -R -n myhost.com -A tcp/22
me:~$ ssh myhost.com
<fanfare sounds...>

Conclusion

Using fwknopdwith OpenWrt provides access to services behind your firewall without exposing the ports for those services to the wider Internet. For ssh in particular, it can greatly reduce the number of brute-force attack attempts seen by your server, and reduce the consequent clutter in your logs.

Note that fwknopd should be used in addition to the usual best practices for securing such services and not as an alternative.

1 Like

Why do this when you can just set up a wireguard VPN? The setup would definitely be simpler and probably much more flexible.

It’s down to use-cases and, possibly, ignorance.

I’ve been using ssh for many decades and it’s my tool of choice. Adding a secure port-knock to ssh makes for an easy, for me, user interface while granting minimal access. I do not want access to my whole network from outside. I just, occasionally, need command-line access to a specific server.

My limited experience of VPNs is that once established, they open up a whole lot of access and while I’m sure they can be locked down, I prefer to start from a state of minimalism. That could, I admit, be simply down to inexperience and ignorance.

Having just now looked at the Wireguard website I’ll admit it looks good, like a VPN done properly, but I don’t have a need for anything more than ssh.

If you put your VPN interface into its own firewall zone (rather than just throwing it into the lan zone), you have fine grained firewall control over everything die VPN client can- or cannot to.

Or maybe "just" leave it as it is. Install fail2ban and dont think about it to hard :person_shrugging:

There are lots of solutions. This is just one of them.

Doing nothing was not an option as my server was being probed by distributed brute-force attacks and that was tedious. These would find the ssh port even if I changed the port number.

I haven’t tried fail2ban but it looks much like portsentry which used to occasionally lock me out, which was tedious. Philosophically, I don’t like this approach as it relies on failed attacks for blocking. Given that the brute force attacks I was seeing came from many distributed sources, fail2ban and its siblings leave a lot of attack surface available along with associated noise in the logs.

A VPN solution would be fine, but it doesn’t look like less work if all I want to do is open 1 port. Also, given that I would still be using ssh, the extra encryption and security of the VPN seems a little superfluous, though I’ll admit it does appeal to the tin-foil hatter in me.

Ssh is all I need. This solution hides the port which reduces the attack surface and keeps my logs clean. It works for me and this post is just to document it for anyone else who may want to try the same thing.

Regardless of opinions, all of which are valid and welcome, I appreciate you taking the time to document this.