Skip to content
Menu

SSH: SPA (fwknop) + nftables + WireGuard — Two-Part Series for Server & Client

Two-Part Tutorial: Part A (Server): nftables + fwknop (SPA). Part B (Client): SPA Variants + WireGuard Fallback for Mobile Clients. Includes Port Forwarding for Fritz!Box &…

Articles are translated automatically from German and can be corrected by the editor. Read the German blog

Introduction (Short)

Port knocks are like an acoustic signal that opens a secret door: only those who know the correct sequence of tones may enter. SPA (Single Packet Authorization) with fwknop makes this encrypted and reliable. For mobile work (changing IPs), we combine SPA with WireGuard as a fallback — or use SPA variants that do not bind to a fixed client IP (e.g., --resolve-url or GPG-SPA). nftables is the firewall engine with which we manage access rules cleanly and performantly.

This tutorial is divided into two parts:

  • Part A Server: nftables base structure, fwknop server setup (with nft integration), WireGuard server setup, Backup & Recovery, Router port forwarding.
  • Part B Client: SPA (resolve-url) variant, GPG-SPA variant, WireGuard client config, tips for mobile work.

Part A: Server: nftables + fwknop (SPA) + WireGuard (Base)

Goal: Set up the server so that SSH is hidden by default (only reachable via SPA) and additionally run WireGuard so clients can reliably connect even with a changing IP.

Before Starting: Read the Emergency Plan!

  • You need physical/KVM access or an admin IP that you keep free for now.

  • Definitely create a backup of nftables:

    1
    
    sudo nft list ruleset > ~/nftables-before-fwknop.conf
    
  • If possible, work initially in a VM to test.


1) Install Packages

Unfortunately, we have to start by installing the things we need:

1
2
3
4
# Server (Debian/LMDE/Ubuntu)
sudo apt update
sudo apt install fwknop-server fwknop-client nftables wireguard -y
sudo systemctl enable --now nftables

Check:

1
2
nft --version
sudo systemctl status fwknop-server

2) nftables Base Setup (Table, Set, Chains)

We create a set allowed_ssh — fwknop adds client IPs there (with timeout). This keeps the firewall clean.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Table anlegen (falls noch nicht vorhanden)
sudo nft add table inet filter

# Set für erlaubte SSH-IPs (IPv4) mit timeout-Flag
sudo nft 'add set inet filter allowed_ssh { type ipv4_addr\; flags timeout\; }'

# (Optional) IPv6 Set
sudo nft 'add set inet filter allowed_ssh_v6 { type ipv6_addr\; flags timeout\; }'

# INPUT Chain mit policy drop
sudo nft 'add chain inet filter input { type filter hook input priority 0 \; policy drop\; }'

# Basisregeln
sudo nft 'add rule inet filter input iif "lo" accept'
sudo nft 'add rule inet filter input ct state established,related accept'

# Erlaube SSH für IPs, die im Set sind
sudo nft 'add rule inet filter input ip saddr @allowed_ssh tcp dport 22 accept'

# IPv6 Variante (optional)
sudo nft 'add rule inet filter input ip6 saddr @allowed_ssh_v6 tcp dport 22 accept'

Save (Boot Persistence):

1
sudo nft list ruleset > /etc/nftables.conf

3) Configure fwknop (Server) — nft integration

  1. Find the path to nft on your system:
1
2
which nft
# z.B. /usr/sbin/nft
  1. fwknopd Set the interface (fwknop sniffs via pcap): Edit /etc/fwknop/fwknopd.conf (as root) and set:
1
2
PCAP_INTF enp3s0      # ersetze enp3s0 durch dein externes Interface
UseSyslog Y           # optional, damit SPA-Versuche in syslog landen
  1. Create access.conf (fwknop) — nft commands as FW_CMD Create the /etc/fwknop/access.conf with the following template (replace the Base64 keys with those generated later by the client):

sudo nano /etc/fwknop/access.conf

Content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
SOURCE: ANY;
OPEN_PORTS: tcp/22;
FW_ACCESS_TIMEOUT: 60;
DATA_COLLECT_MODE: PCAP;

KEY_BASE64: <DEIN_RIJNDAEL_KEY_BASE64>;
HMAC_KEY_BASE64: <DEIN_HMAC_KEY_BASE64>;

# nft add element mit timeout (FW_CMD führt das aus)
# %IP% = Platzhalter, den fwknop zur Laufzeit ersetzt
FW_CMD: /usr/sbin/nft add element inet filter allowed_ssh { %IP% timeout 60s }
FW_CMD_DEL: /usr/sbin/nft delete element inet filter allowed_ssh { %IP% }

Set access rights:

1
2
sudo chown root:root /etc/fwknop/access.conf
sudo chmod 600 /etc/fwknop/access.conf
  1. Start / restart fwknop:
1
2
3
sudo systemctl restart fwknop-server 2>/dev/null || sudo systemctl restart fwknopd
sudo systemctl enable --now fwknop-server
sudo journalctl -u fwknop-server -f

Note: If your nft version does not support timeout for set elements, use FW_CMD without timeout and FW_CMD_DEL as cleanup (fwknop can call FW_CMD_DEL), or use a small timer script.

4) WireGuard (Server) — Base Setup

WireGuard is the stable solution for mobile clients. If WireGuard is running, you can reach SSH via the tunnel, completely independent of the public client IP.

4.1 Server Keys & Configuration

1
2
3
4
5
6
# Verzeichnisse
sudo mkdir -p /etc/wireguard
sudo chmod 700 /etc/wireguard

# Schlüssel (als root)
wg genkey | sudo tee /etc/wireguard/server_private.key | wg pubkey | sudo tee /etc/wireguard/server_public.key

Create /etc/wireguard/wg0.conf (Example):

1
2
3
4
5
6
7
[Interface]
Address = 10.10.10.1/24
ListenPort = 51820
PrivateKey = <INHALT VON /etc/wireguard/server_private.key>
# Optional: bring up nftables rules beim Start (PostUp/PostDown) oder verwalte nftables manuell
PostUp = /usr/sbin/nft add element inet filter allowed_ssh { 10.10.10.2 } || true
PostDown = /usr/sbin/nft delete element inet filter allowed_ssh { 10.10.10.2 } || true

Note: In PostUp/PostDown you can allow certain VPN peers to directly use SSH (e.g., 10.10.10.2). Alternatively, manage access purely via allowed_ssh set entries for public IPs — VPN-tunneled clients do not need this.

Now activate WireGuard:

1
2
sudo systemctl enable --now wg-quick@wg0
sudo wg show

4.2 nftables + WireGuard

If SSH is to run over the tunnel, ensure that the INPUT chain allows VPN traffic (e.g., accept for iifname wg0 or for the VPN subnet):

1
2
sudo nft 'add rule inet filter input iif "wg0" accept'
# oder gezielt: accept tcp dport 22 iif "wg0"

5) Port Forwarding on Router (what needs to be forwarded?)

  • WireGuard: UDP port (default 51820) → Server LAN IP (e.g., 192.168.178.10).
  • SPA / fwknop: SPA packets are sent to a target port (depending on the client call). If you use a fixed port for SPA (e.g., UDP 62201), you must also forward this port to the server. fwknop itself “listens” via pcap, but NAT/Router must let packets through/forward them to the server IP.

We treat router configuration below (Fritz!Box & Speedport).


6) Backup / Rollback (nftables + fwknop)

Backup nftables (important):

1
sudo nft list ruleset > ~/nftables-before-fwknop.conf

Rollback (if locked out):

1
sudo nft -f ~/nftables-before-fwknop.conf

Rollback script (optional):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
cat > ~/fwknop-rollback-nft.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
BACKUP="$HOME/nftables-before-fwknop.conf"
if [ ! -f "$BACKUP" ]; then
  echo "Backup $BACKUP fehlt"
  exit 1
fi
echo "Restore nftables from $BACKUP"
sudo nft -f "$BACKUP"
EOF
chmod +x ~/fwknop-rollback-nft.sh

7) Quick Checks (before tests)

  • sudo nft list ruleset — can you see Table / Set / Rules?
  • which nft — check path and adjust in access.conf if necessary.
  • ip addr — check interface (PCAP_INTF).
  • sudo journalctl -u fwknop-server -f — observe logs.

Part B: Client: SPA Variants & WireGuard Client

Goal: two ways to reliably get access from your laptop/smartphone:

  • SPA with resolve-url (practical on the go), or
  • GPG-SPA (stronger, IP-independent), or
  • WireGuard (recommended for many users / stable connection).

A) SPA with --resolve-url (mobile, simple)

Concept: Client determines its public IP via a web URL (e.g., https://icanhazip.com or better: your own small resolver URL) and packs this IP into the SPA packet. Server opens exactly this IP.

Client Command (simple):

1
2
# Beispiel: verwende deine Server-Domain im -D Feld
fwknop -A tcp/22 -D server.example.com --use-hmac --resolve-url https://icanhazip.com --verbose

Advantages: works with changing IP; minimal configuration effort. Disadvantages: Privacy (you query external services for your IP). I recommend hosting your own small resolver URL if possible (e.g., a small CGI with echo $_SERVER['REMOTE_ADDR']).

Server: no change needed, use access.conf as in Part A — fwknop sets %IP% from the SPA.


B) GPG SPA (No binding to Source IP)

Concept: Client signs/encrypts SPA with private GPG key. Server uses public key to check packets. Server trusts packet content, not source address → mobile and secure.

Steps (Short):

  1. Client (Create GPG Key):
1
2
gpg --full-generate-key
# Notiere KeyID (z.B. ABCDEF12)
  1. Server (Import Client Public Key): Transfer gpg --export --armor CLIENT_KEYID and import on the server in /root/.gnupg or in a dedicated gnupg-home for fwknop.

  2. access.conf (GPG Config):

1
2
3
GPG_HOME_DIR: /root/.gnupg
GPG_REMOTE_ID: <CLIENT_KEYID>
GPG_DECRYPT_ID: <SERVER_KEYID>    # optional, wenn Entschlüsselung nötig
  1. Client creates SPA with GPG options: see fwknop manpage (options such as --use-gpg / --gpg-key — the flags vary with version).

Advantages: very secure, no source-IP issues. Disadvantages: GPG key management required (passphrase, optionally smartcard).

1) Recommendation (mobile & simple) — resolve-url

The client determines its public IP via a web URL (e.g., https://icanhazip.com or a custom resolver URL) and sends an SPA to the server. Simple, robust with changing IPs.

1
2
# Auf dem Client ausführen (ersetzen!)
fwknop -A tcp/22 -D server.example.com --use-hmac --resolve-url https://icanhazip.com --verbose

Explanation / Placeholder

  • -A tcp/22 → Access to SSH (port 22).
  • -D server.example.com → Hostname or IP of your server (replace!).
  • --use-hmac → Use HMAC (for symmetric keys).
  • --resolve-url https://icanhazip.com → Service for determining the public IP; recommend a custom resolver URL for privacy.
  • --verbose → Output for debugging.

Quick check (after execution):

1
2
# prüfe, ob ~/.fwknoprc Schlüssel enthält
grep -E '^(KEY_BASE64|HMAC_KEY_BASE64):' ~/.fwknoprc

If keys are available, you can build from them /etc/fwknop/access.conf (server-side), e.g., with the AWK snippet in the tutorial.


2) Recommendation (IP-independent & cryptographic) — GPG-SPA

If you want to access from anywhere without IP binding and use GPG, sign/encrypt the SPA with your GPG key. (Flag names may vary slightly depending on fwknop version — check man fwknop if necessary.)

Example (conceptual — replace CLIENT_KEYID and server.example.com):

1
2
# Beispiel (prüfe deine fwknop-Version / manpage für genaue gpg-Flags)
fwknop -A tcp/22 -D server.example.com --use-gpg --gpg-identity CLIENT_KEYID --verbose

Important notes

  • --use-gpg / --gpg-identity are exemplary flags — the exact option may vary slightly by fwknop version, e.g., –gpg-key / –gpg-id. Check man fwknop or fwknop –help.
  • On the server, you must import the corresponding public key (e.g., into /root/.gnupg) and configure access.conf so that GPG_REMOTE_ID / GPG_HOME_DIR is set.

Quick check on client (GPG):

1
2
gpg --list-keys
# notiere die KeyID, die du dann in fwknop verwendest

3) (Optional) One-liner to pre-build a access.conf after key generation

If you have the --key-gen and the ~/.fwknoprc , you can quickly create a access.conf locally (on the client) and then copy it to the server via scp:

1
2
3
awk '/^KEY_BASE64:/ {k=$2} /^HMAC_KEY_BASE64:/ {h=$2} END { if(!k||!h){print "Fehler: Keys nicht gefunden" > "/dev/stderr"; exit 2} \
printf "SOURCE: ANY;\nOPEN_PORTS: tcp/22;\nFW_ACCESS_TIMEOUT: 60;\nDATA_COLLECT_MODE: PCAP;\n\nKEY_BASE64: \ 
%s\nHMAC_KEY_BASE64: %s\n", k, h }' ~/.fwknoprc > /tmp/fwknop_access.conf

Then scp + sudo mv as described in the tutorial, or use the one-liner deploy from the article.


  1. Generate client key:
1
wg genkey | tee client_private.key | wg pubkey > client_public.key
  1. Client config (example wg0-client.conf):
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[Interface]
PrivateKey = <INHALT client_private.key>
Address = 10.10.10.2/24
DNS = 10.10.10.1

[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
Endpoint = your.dyndns.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0  
PersistentKeepalive = 25
  1. On the server add the peer (public key + AllowedIPs) to the wg0 configuration (or manage via wg set).

  2. Firewall: allow wg0 traffic (see Part A).

Advantages: stable, independent of client IP, performant. Disadvantages: initial setup effort, managing peers.


Router: Port Forwarding for Fritz!Box & Speedport (General Guide)

Note: Menu names / firmware variants differ slightly. If individual menus do not match exactly, search for terms like “Internet”,
“Sharing”, “Port Sharing”, “NAT”, or “Port Forwarding”. Read your model’s manual if in doubt.

1) Fritz!Box (AVM)

  • Open the web interface: http://fritz.box
  • Log in as Admin.
  • Menu: Internet → Sharing (or «Portfreigaben» / «Freigaben»).
  • “New Sharing” / “Add Device for Sharing” — select your server (LAN IP).
  • Create a port forwarding rule:
  • Application name: WireGuard (or fwknop-SPA)
  • Protocol: UDP
  • External port: 51820 (WireGuard) or e.g., 62201 (SPA), if you want to send SPA over UDP on a specific port
  • Internal port: same as external

Save. Test from outside (e.g., phone via mobile network) with nc / wg / fwknop call.

2) Speedport (Deutsche Telekom) — typical workflow

  • Open web interface: http://192.168.2.1 (or speedport.ip)
  • Log in as Admin.
  • Menu: Internet / Sharing / Port Forwarding (exact name varies).
  • “Create New Rule”: fill out as with FritzBox:
  • Target device / IP: server LAN IP
  • External / internal port: 51820 UDP (WireGuard) and optionally SPA port
  • Protocol: UDP

Save and test.

Test (from outside)

For WireGuard: try connecting from the mobile device using wg/wg-quick.

For SPA: test fwknop client command with --resolve-url from the mobile network.


Conclusion

To conclude, I will provide 3 possible configuration variants for the mobile connection.

Variant A — Resolve URL (practical, mobile-friendly)

Concept briefly: Client determines its public IP via HTTP(S) (e.g., https://icanhazip.com or better: your own small resolver URL) and packs this IP into the SPA. Server adds this IP to the nft set.

Server — access.conf (nft integration)

Add to /etc/fwknop/access.conf (replace Base64 keys):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
SOURCE: ANY;
OPEN_PORTS: tcp/22;
FW_ACCESS_TIMEOUT: 60;
DATA_COLLECT_MODE: PCAP;

KEY_BASE64: <DEIN_RIJNDAEL_KEY_BASE64>;
HMAC_KEY_BASE64: <DEIN_HMAC_KEY_BASE64>;

FW_CMD: /usr/sbin/nft add element inet filter allowed_ssh { %IP% timeout 60s }
FW_CMD_DEL: /usr/sbin/nft delete element inet filter allowed_ssh { %IP% }

Pay attention to the correct nft-path (which nft) and that allowed_ssh exists.

Client One-liner (mobile)

1
fwknop -A tcp/22 -D server.example.com --use-hmac --resolve-url https://icanhazip.com --verbose

(Recommendation: replace https://icanhazip.com with a custom resolver URL if privacy is important to you.)

Quick check

  • After execution: on the server sudo nft list set inet filter allowed_ssh → the IP should appear.
  • Then ssh -i ~/.ssh/id_rsa user@server.example.com.

Pros/Cons

  • Easy to set up, works with changing IPs.
  • − Dependence on an external IP service (or effort to host a mini-resolver yourself).
  • Security: encryption via SPA continues; short-lived access (timeout).

Variant B: GPG-SPA (IP-independent, cryptographically strong)

Concept briefly: Client signs/encrypts the SPA with its private GPG key; server verifies/decrypts with the corresponding public key. Server does not need to bind the client source IP (or can additionally use %IP%).