SSH Port Knocking in Practice: knockd Hidden-Service Config
SSH port knocking with knockd and iptables: default-deny, open on sequence. 7000/8000/9000 knock sequence, seq_timeout window, cmd_timeout auto-revoke. firewalld/nftables ports, four cannot-connect after knock triages, and sequence-sniff risk.
Scope
SSH port knocking is default-closed, open-only-after-sequence service hiding. Three typical cases:
- Port 22 is on the public net. Logs show thousands of brute-force tries a day. Hide SSH from scanners without standing up a VPN.
- Ops ingress, jump host, DB jump: few operators, source IPs not fixed (home broadband, 4G, roaming). IP allowlists do not work.
- Bastion or WireGuard already exists. Want a zero-cost extra filter to shrink the 0day / password-spray window.
Be clear: knocking is concealment, not authentication. It lowers scan discovery. What actually holds SSH is key auth, 2FA, and fail2ban. Stack them. Do not substitute.
Prerequisites
- Linux server (Debian/Ubuntu/CentOS/RHEL), root.
- `knockd` server plus `knock`/`nc` client installed.
- Host uses iptables for filter rules. If firewalld or nftables, confirm the backend first (see FAQ 6 / FAQ 2).
- SSH currently connects. Independent backup login ready (cloud console VNC). A bad config must not lock you out.
- Three or four TCP ports reserved for knocking, e.g. 7000, 8000, 9000. Nothing needs to listen on them.
Mechanism
Core: connect a set of otherwise-dead ports in order. Server sniffs the sequence, then inserts a temporary firewall allow.
`knockd` is not a normal daemon. It pcap-sniffs on the NIC via libpcap. Even if iptables INPUT already DROPs those packets, knockd still sees the sequence. That is why a DROP policy does not disable it, and why it cannot traverse upstream NAT or LB — if the host sits behind a cloud NAT gateway, knock packets must be forwarded to the host or knockd never receives them.
Match is three constraints:
- Order: ports must be hit in configured order.
- Window: `seq_timeout` is the max seconds for the whole sequence. Stops slow sequential probes.
- Flags: `tcpflags = syn` accepts only new SYNs. Avoids ESTABLISHED false fires and FIN-scan accidental hits.
Safer alternative: Single Packet Authorization (SPA). Canonical impl is fwknop. One encrypted UDP packet. No sniffable port sequence. Does not leak which ports are the secret. High bar and extra deploy cost: prefer SPA. Goal is only to vanish from automated scanners: knockd has a better cost/benefit.
Procedure
1. Install knockd
`# Debian / Ubuntu
apt update && apt install -y knockd iptables-persistent
# CentOS / RHEL / Rocky
dnf install -y epel-release
dnf install -y knockd iptables-services`
`iptables-persistent` will ask to save current rules. Choose Yes. Firewall rules must survive reboot.
2. Write knockd config
Edit `/etc/knockd.conf`. Full config with auto-close: successful knock allows SSH, privilege revoked after 30s:
`[options]
logfile = /var/log/knockd.log
interface = eth0
[openssh]
sequence = 7000,8000,9000
seq_timeout = 10
tcpflags = syn
start_command = /usr/sbin/iptables -I INPUT -s %IP% -p tcp --dport 22 -j ACCEPT
cmd_timeout = 30
stop_command = /usr/sbin/iptables -D INPUT -s %IP% -p tcp --dport 22 -j ACCEPT`
Meaning:
- interface: NIC that actually receives public traffic. Confirm with `ip -br addr`. Wrong name = knock is dead silent.
- sequence: knock ports. None of them need a listener.
- seq_timeout: whole sequence in 10s. Longer weakens the control.
- %IP%: knockd substitutes the knocker source IP. It is the TCP-layer source. Behind CDN or a reverse proxy this breaks.
- cmd_timeout: allow-window length. 30s means SSH handshake and login must finish in 30s. Established sessions survive timeout; new connects are denied.
Want a longer window (e.g. 300s): raise `cmd_timeout`. Want manual close: add a reverse sequence `9000,8000,7000` `[closessh]` block and knock it from the client.
3. Default-deny SSH
Before knockd is live, default-deny 22. Allowlist model:
`# Do not interrupt already-established connections
iptables -I INPUT 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Default-drop SSH
iptables -A INPUT -p tcp --dport 22 -j DROP
# Save rules
netfilter-persistent save`
Order: ESTABLISHED first, then DROP SSH. DROP first and the next packet of a live SSH session dies. Check with `iptables -L INPUT -n --line-numbers`.
4. Enable and start on boot
`systemctl enable --now knockd
systemctl status knockd --no-pager`
Start fail: `journalctl -u knockd -n 50 --no-pager`. Usual causes: wrong NIC name, or Tab indent in `[options]` (knockd wants spaces).
5. Client knock script
Wrap the knock so you are not counting ports by hand:
`#!/bin/bash
# Save as ~/knock-ssh.sh and chmod +x
HOST="${1:-ssh.example.com}"
for port in 7000 8000 9000; do
nc -z -w1 "$HOST" "$port" >/dev/null 2>&1
sleep 0.2
done
echo "[*] Knock complete, connecting SSH..."
ssh -o ConnectTimeout=5 "root@${HOST}"`
-z: probe only, no payload. -w1: 1s per try. `sleep 0.2` between ports so local concurrency does not scramble order.
Validation
Four steps:
`# 1) Confirm knockd is capturing packets
systemctl is-active knockd
ss -lunp | grep knockd
# 2) Before knock: SSH should time out with no response
nc -z -w2 ssh.example.com 22; echo "exit=$?"
# 3) Run the knock and immediately watch the log
tail -f /var/log/knockd.log
# Example of a normal run:
# 2026-09-12 10:30:01 ssh.example.com: openSSH: Stage 1
# 2026-09-12 10:30:01 ssh.example.com: openSSH: Stage 2
# 2026-09-12 10:30:01 ssh.example.com: openSSH: Stage 3
# 2026-09-12 10:30:01 ssh.example.com: openSSH: OPEN SESAME
# 4) Confirm the allow rule is inserted with the source IP
iptables -L INPUT -n --line-numbers | grep 22`
Expect: 22 probe times out before knock. After knock, a rule `ACCEPT tcp -- 203.0.113.9 0.0.0.0/0 tcp dpt:22`. Rule gone after 30s.
FAQ
FAQ 1: Log says OPEN SESAME, SSH still will not connect
Triage in order:
- Window too short: SSH handshake started after `cmd_timeout`; rule already gone. Temporarily set `cmd_timeout` to 120s to confirm.
- fail2ban already banned the IP: knockd inserts on iptables INPUT; fail2ban inserts DROP on its own chain. Priority is chain-hook order. `iptables-save | grep -i f2b`. Unban on fail2ban if needed.
- Earlier DROP: cloud security group or a DROP higher on the host fires before knockd ACCEPT. `iptables -L INPUT -n --line-numbers` — new rule must sit before DROP. `-I INPUT` inserts at chain head; usually safe.
- Local network gear: some home routers RST un-listened ports instead of silent drop, so `nc` sends one packet. Use `-w1`, confirm the client actually sent, or on server `tcpdump -i eth0 'tcp[tcpflags] & tcp-syn != 0 and port 7000'`.
FAQ 2: Host uses firewalld or nftables; knockd iptables commands do nothing
knockd is not bound to a firewall backend. It runs whatever you put in `start_command`. Two ports:
- firewalld: replace with `/usr/bin/firewall-cmd --add-rich-rule='rule family="ipv4" source address="%IP%" port port="22" protocol="tcp" accept'`. Close with `--remove-rich-rule`. Do not mix firewalld and iptables rules; they overwrite each other.
- nftables: named set, add/delete elements. `nft add element inet filter ssh_allow { %IP% }` and `nft delete element inet filter ssh_allow { %IP% }`. Rule references `ip saddr @ssh_allow accept`. Cleaner than inserting/deleting rules; easier to audit.
Pitfall: `firewalld` and `iptables-services` both installed. Two rule sets fight over netfilter. Keep one.
FAQ 3: Can the knock sequence be sniffed off-path?
Yes. Sequence is plaintext TCP SYN. Anyone on the same broadcast domain or link can tcpdump it and replay. Therefore:
- Do not knock on public Wi-Fi or shared office nets.
- Pick ports that look random and unrelated to well-known services. Avoid 1,2,3 or 111,222,333.
- Server can enable `one_time_sequences` in `[options]` (needs a sequence file in `knockd.conf`). Sequence changes each use; replay dies.
- Higher bar: fwknop SPA. One encrypted UDP packet, no sniffable sequence, replay counter.
knockd matches TCP SYN. UDP knocking support is limited. UDP knocks require the path not to SYN-proxy or conntrack-optimize, or the sequence merges/drops.
FAQ 4: After a successful knock, why is SSH still in scan results?
Some online scanners reuse historical results for a while. The 30s allow window can still be hit. Reduce that:
- Keep `cmd_timeout` at or under 30s. Enough to log in.
- Bind SSH to loopback or an internal address. Externally only the knockd-inserted rule plus a forward. Port does not exist from outside.
- After login, knock the reverse sequence immediately. Do not wait for timeout.
Summary
Port knocking solves service visibility, not identity. This config stacks three controls: SSH default DROP; order + window + SYN triple match; cmd_timeout auto-revoke. Cheaper than a jump host. Better payoff than just changing the SSH port.
Priority: confirm backup login works, then change the default firewall policy, then enable and verify knockd. Never default-deny SSH when it is the only login path. Higher concealment requirement: evaluate fwknop SPA, stacked with SSH keys, 2FA, and fail2ban.