I close SSH port 22 (and what I use instead)

speckx1 pts0 comments

Why I close SSH port 22 entirely (and what I use instead) · Michele Bologna<br>Most SSH hardening guides stop at key-only auth and fail2ban. That helps, but if your machine has a public IP, port 22 is still open to the internet. Every automated scanner can probe it and get a response: the SSH version string, the banner, proof that something is listening.<br>The log noise alone is annoying: even with key-only auth, failed attempts pile up every day. But the bigger problem is exposure. If a zero-day drops in OpenSSH, every server with port 22 open is a target before you have time to patch. That has happened before.<br>I wanted the SSH daemon itself to be unreachable: no banner, no version string, nothing for nmap to work with. Not just rate-limited or hidden behind a non-standard port, but genuinely not connectable unless you already hold the key.<br>The idea: port knocking, and why it is not enough on its own

Link to heading<br>Port knocking is the original approach to this problem. The idea is simple: the server watches for a specific sequence of connection attempts on closed ports. If a client connects to port 7000, then 8000, then 9000, the server recognizes the pattern and temporarily opens a real port, like 22.<br>It works, but it has a real weakness: the knock sequence travels in the clear. Anyone watching your traffic can capture the sequence and replay it. There is no authentication, only obscurity.<br>fwknop solves this with Single Packet Authorization (SPA). Instead of a knock sequence, you send a single UDP packet that is encrypted and cryptographically signed with an HMAC. The server only opens port 22 if it can verify the packet came from someone holding the right keys.<br>How Single Packet Authorization works

Link to heading<br>Before SSH is accessible at all, you send one encrypted UDP packet to the server. fwknopd validates the packet and temporarily inserts a firewall rule that opens port 22 for your source IP only, for a configurable time window (I use 120 seconds). After that window, the rule is removed automatically. If you are already connected, the session stays up because the connection was established before the rule disappeared.<br>From a scanner&rsquo;s perspective, port 22 never responds. nmap reports it as filtered, the same state as any silently dropped packet. No banner, no RST, no confirmation that SSH is even listening there.<br>Because SPA uses UDP, the packet can be lost in transit. If SSH hangs on connect, run fwknop -n server1 again and retry. It is stateless, so resending is safe.<br>Left: a port scanner gets no response. Right: a valid SPA packet opens port 22 for the client&rsquo;s IP for 120 seconds, then closes it again.<br>The SPA packet itself is HMAC-SHA512-authenticated and encrypted. Replaying a captured packet does not work: the packet contains a timestamp and a single-use sequence counter. fwknopd checks both, and drops the packet silently if either fails.<br>Generating keys

Link to heading<br>You need two keys: an encryption key and an HMAC key. The encryption key protects the contents of the SPA packet. The HMAC key authenticates it. They serve different purposes and should be separate; using the same key for both weakens the security properties of the scheme.<br>Key generation is a one-time step, and you need the output before configuring either side:<br>fwknop --key-gen

This outputs something like:<br>KEY_BASE64:<br>HMAC_KEY_BASE64:<br>Store both keys somewhere you can retrieve them later: you will need them on the server (via Ansible vault) and on every client machine. I keep them in Ansible vault as vault_fwknop_spa_key and vault_fwknop_hmac_key, and mirrored in Bitwarden as shared/fwknop_spa_key and shared/fwknop_hmac_key. The two stores serve different tools: Ansible reads from its own vault when deploying the server side, while chezmoi queries Bitwarden when populating ~/.fwknoprc on client machines. Same keys, two entry points.<br>One key pair covers all hosts. Each host runs the same fwknopd configuration pointing to the same shared keys. If you want per-host keys you can use separate stanzas in the access file, but one shared pair is simpler to rotate.<br>Deploying the server side with Ansible

Link to heading<br>My Ansible role for this is fwknop_server. It installs fwknop-server, deploys the server configuration and access file, and adjusts UFW rules.<br>The key part of the tasks file:<br>- name: Install fwknop-server<br>apt:<br>name: fwknop-server<br>state: present

- name: Deploy fwknopd.conf<br>template:<br>src: fwknopd.conf.j2<br>dest: /etc/fwknop/fwknopd.conf<br>owner: root<br>group: root<br>mode: "0600"

- name: Deploy access.conf<br>template:<br>src: access.conf.j2<br>dest: /etc/fwknop/access.conf<br>owner: root<br>group: root<br>mode: "0600"<br>no_log: true

- name: Remove public SSH UFW rule<br>ufw:<br>rule: limit<br>port: "{{ fwknop_ssh_port | default(22) }}"<br>proto: tcp<br>delete: yes

- name: Allow fwknop SPA port (UDP)<br>ufw:<br>rule: allow<br>port: "{{ fwknop_spa_port }}"<br>proto: udp

The public SSH UFW rule gets removed after fwknop is deployed. From that point, iptables...

port packet server fwknop keys rule

Related Articles