SSH Hardening
    Single Packet Auth

    Hide SSH with fwknop Single Packet Authorization on a VPS

    Conceal SSH on a RamNode VPS using fwknop SPA — AES + HMAC authenticated packets, default-drop firewall, and a temporary per-IP ACCEPT rule.

    Putting a service behind a firewall is good practice, but it does not make the service invisible. An attacker scanning your RamNode VPS with nmap can still see that SSH is listening, and that visibility is enough to start probing for weak credentials or a zero day. Single Packet Authorization (SPA) closes that gap. With fwknop deployed, your firewall sits in a default drop stance and SSH appears closed to the entire internet. Only after the daemon passively sniffs a single, encrypted, authenticated packet from an authorized client does it briefly open the port to that one source IP.

    This guide sets up fwknop to conceal SSH on a RamNode VPS running Ubuntu 24.04 LTS, using AES encryption with HMAC authentication.

    How SPA works

    fwknop stands for FireWall KNock OPerator. It is the modern successor to port knocking, developed by Michael Rash. The flow is:

    1. Your firewall drops all inbound traffic to the protected port by default.
    2. The fwknopd daemon listens passively with libpcap. There is no open socket to connect to, so there is nothing for a scanner to find.
    3. An fwknop client builds one encrypted UDP packet (default destination UDP/62201) containing the requested access, encrypts it, and signs it with an HMAC.
    4. The daemon verifies the HMAC first, then decrypts, then checks for replay using a SHA 256 digest cache.
    5. On success it inserts a temporary ACCEPT rule into a dedicated firewall chain for your source IP, and removes it after a timeout (30 seconds by default, which is plenty of time to establish the SSH connection).

    Because the HMAC is checked before decryption, the heavier crypto code is never even reached by an unauthenticated packet.

    A critical warning before you start

    If you misconfigure fwknop and also apply a default drop policy on SSH, you can lock yourself out of the VPS permanently. Work through this guide with a second access path available. RamNode provides serial console or VNC access through the control panel, so confirm you can reach that console before you switch the firewall to default drop. Test every step before closing the door.

    Prerequisites

    • A RamNode VPS running Ubuntu 24.04 LTS. SPA is extremely lightweight, so the smallest plan is fine.
    • Root or sudo access, plus confirmed out of band console access through the RamNode panel.
    • A client machine (your laptop or another server) where you will install the fwknop client.
    • NTP running on the server, because SPA packets carry a timestamp that must fall within roughly 120 seconds of the server clock.

    Step 1: Install fwknop

    On the server:

    shell
    sudo apt update
    sudo apt install -y fwknop-server

    On your client machine (also Ubuntu or Debian):

    shell
    sudo apt install -y fwknop-client

    The Android app and macOS Homebrew package work as clients too.

    Step 2: Generate keys

    On the client, generate a matched AES and HMAC key pair:

    shell
    fwknop --key-gen

    This prints two values:

    shell
    KEY_BASE64: <long base64 string>
    HMAC_KEY_BASE64: <long base64 string>

    Copy both. You will paste them into the server access file and reuse the same values in the client config. Treat them like SSH private keys.

    Step 3: Configure the server access file

    Edit /etc/fwknop/access.conf. Comment out or remove any example stanza, then add:

    shell
    SOURCE                  ANY
    OPEN_PORTS              tcp/22
    KEY_BASE64              <paste KEY_BASE64 here>
    HMAC_KEY_BASE64         <paste HMAC_KEY_BASE64 here>
    FW_ACCESS_TIMEOUT       30
    REQUIRE_SOURCE_ADDRESS  Y

    REQUIRE_SOURCE_ADDRESS Y forces the client to embed its real source IP in the packet rather than allowing 0.0.0.0, which tightens the grant to exactly the requesting host.

    Step 4: Configure the daemon

    Edit /etc/fwknop/fwknopd.conf. The defaults are sane for a single interface VPS, but set the sniffing interface explicitly. Find PCAP_INTF and set it to your public interface (often eth0; check with ip addr):

    shell
    PCAP_INTF               eth0;

    On modern Ubuntu the firewall backend is handled through iptables compatibility, and fwknopd manages its own chain (FWKNOP_INPUT) so its rules stay separate from the rest of your policy.

    Step 5: Build the default drop firewall, carefully

    First, allow your current established connection so you are not dropped mid session:

    shell
    sudo iptables -A INPUT -i eth0 -p tcp --dport 22 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

    Now test fwknopd in the foreground before adding the DROP rule. Run the daemon so you can watch it:

    shell
    sudo fwknopd -f

    In another terminal, leave that running. On your client, create ~/.fwknoprc (or pass everything on the command line). A config stanza looks like:

    shell
    [default]
    ACCESS                  tcp/22
    SPA_SERVER              your.vps.ip.address
    KEY_BASE64              <paste KEY_BASE64 here>
    HMAC_KEY_BASE64         <paste HMAC_KEY_BASE64 here>
    USE_HMAC                Y

    Send an SPA packet and request access:

    shell
    fwknop -n default -R

    The -R flag resolves your client's public IP automatically and embeds it. Watch the server's foreground output. You should see a line confirming a valid SPA packet and an added rule, something like:

    shell
    received valid Rijndael encrypted packet ... add FWKNOP_INPUT <your IP> -> 0.0.0.0/0(tcp/22) ACCEPT rule 30 sec

    If that works, you are ready to close the port. Stop the foreground daemon with Ctrl+C, then add the DROP rule:

    shell
    sudo iptables -A INPUT -i eth0 -p tcp --dport 22 -j DROP

    SSH is now invisible. Confirm from an outside host that nmap -p 22 your.vps.ip shows the port as filtered, not open.

    Step 6: Run fwknopd as a service

    Enable and start the daemon so it survives reboots:

    shell
    sudo systemctl enable --now fwknop-server
    sudo systemctl status fwknop-server

    Step 7: Persist the firewall rules

    iptables rules do not survive a reboot on their own. Install the persistence package:

    shell
    sudo apt install -y iptables-persistent
    sudo netfilter-persistent save

    Save only your baseline rules (the ESTABLISHED allow and the DROP). The dynamic FWKNOP rules are transient and should not be persisted, so save before you have triggered any SPA grant, or flush the fwknop chain first with sudo fwknopd --fw-flush.

    Daily use

    From an authorized client, knock then connect:

    shell
    fwknop -n default -R && ssh user@your.vps.ip

    Wrap it in a shell function or SSH ProxyCommand so the knock happens automatically before each connection.

    Hardening notes

    • Use HMAC always. Never disable it. The HMAC is what shields the decryption routines from unauthenticated input.
    • Randomize or change the SPA port. You can move off UDP/62201 with ENABLE_UDP_SERVER and UDPSERV_PORT in fwknopd.conf, though passive sniffing already means there is no port to find. Keeping the default is fine.
    • Protect other services too. fwknop is not tied to SSH. You can conceal any TCP service the same way by adding its port to OPEN_PORTS.
    • Keep clocks synced. Confirm timedatectl shows NTP active on the server.

    Troubleshooting

    • No rule added, no log line. The packet is not reaching the daemon. Check that your client network allows outbound UDP/62201 and that PCAP_INTF matches your real interface.
    • Timestamp errors in the log. Server and client clocks differ by more than two minutes. Fix NTP on both ends.
    • Locked out. Use the RamNode serial console or VNC to flush the rules: sudo iptables -F INPUT or sudo fwknopd --fw-flush, then reconnect and fix the config.

    Wrap up

    Your RamNode VPS now presents a completely closed SSH port to the internet, opening only for a cryptographically authenticated single packet from a client you control. This removes SSH from the attack surface that scanners and automated brute force tools can even see, while adding almost no operational friction once the knock is wrapped into your connection workflow.