← News
/PLAYBOOK · 2026-09-12

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:

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

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:

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:

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:

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:

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:

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:

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.