SSH 端口敲门实战:knockd 隐藏服务配置

适用场景

SSH 端口敲门(Port Knocking)是一套”默认关闭、按序敲门才开门”的服务隐藏方案。适用的典型场景有三类:

  • 服务器 22 端口直接暴露在公网,日志里每天有成千上万条爆破尝试,希望在不引入 VPN 的前提下把 SSH 从扫描器的视野里”藏起来”。
  • 运维入口机、跳板机、数据库跳板等只允许少数固定运维人员登录,来源 IP 不固定(家庭宽带、4G、漫游),无法用 IP 白名单解决。
  • 已有堡垒机或 WireGuard 隧道,但希望再加一层无成本的前置过滤,降低被 0day 或口令爆破打穿的窗口。

需要明确的是:端口敲门是一种隐匿手段,不是认证手段。它降低了被扫描发现的概率,但真正守住 SSH 的仍然是密钥认证、2FA 与 fail2ban 这类强认证与限流机制。二者应叠加使用,而不是互相替代。

前置条件

  • Linux 服务器(Debian/Ubuntu/CentOS/RHEL 均可),具备 root 权限。
  • 已安装 knockd 服务端与 knock/nc 客户端工具。
  • 服务器使用 iptables 管理过滤规则(若使用 firewalld 或 nftables,需先确认后端是 iptables 还是 nftables,参见第 6 节常见问题)。
  • 服务器上 SSH 服务当前可正常连接,且已准备好一个独立的备用登录通道(如云控制台 VNC),避免配置失误导致彻底锁死。
  • 已明确三到四个用于”敲门”的 TCP 端口号,例如 7000、8000、9000,这些端口不需要任何服务监听。

原理说明

端口敲门的核心机制是”按顺序连接一组本不提供服务的端口”,服务端通过抓包识别出正确的端口序列,再动态向防火墙插入一条临时放行规则。

knockd 的工作方式与普通守护进程不同:它通过 libpcap 在网卡上直接抓取数据包,因此即便 iptables 的 INPUT 链已经 DROP 掉这些包,knockd 依然能看到敲门序列。这一点决定了它不会因为防火墙拦截而失效,也决定了它无法穿透上层 NAT 或负载均衡——如果服务器在云厂商 NAT 网关之后,敲门包必须先被转发到主机,否则 knockd 收不到。

敲门序列的匹配由三个条件共同约束:

  • 顺序:必须严格按配置的端口顺序发起连接。
  • 时间窗seq_timeout 指定整个序列必须在多少秒内完成,防止慢速逐端口探测。
  • 标志位tcpflags = syn 只接受全新连接请求,避免被 ESTABLISHED 流量误触发,也避免某些扫描器用 FIN 扫描碰巧命中序列。

相对更安全的替代方案是 单包授权(SPA),代表实现是 fwknop。SPA 只发送一个加密的 UDP 包,不产生可被旁路监听的端口序列,也不暴露”哪些端口是暗号”这一信息。如果安全要求高、且能接受额外部署成本,建议优先考虑 SPA;如果只是想让自动化扫描器扫不到 SSH,knockd 的成本收益比更好。

操作步骤

1. 安装 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 时会询问是否保存当前规则,选择 Yes。这一步很关键:后面的防火墙规则需要在重启后依然生效。

2. 编写 knockd 配置文件

编辑 /etc/knockd.conf。下面是一份带自动关门的完整配置,敲门成功后放行 SSH,30 秒后自动收回权限:

[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

参数含义:

  • interface:必须填写服务器实际接收公网流量的网卡名,可用 ip -br addr 确认,填错会导致敲门完全无响应。
  • sequence:关门端口序列,三个端口均无需监听。
  • seq_timeout:10 秒内必须敲完,过长会削弱防护效果。
  • %IP%:knockd 自动替换为敲门来源 IP,注意它取的是 TCP 层源地址,若服务器在 CDN 或反向代理之后会失效。
  • cmd_timeout:放行窗口时长。设为 30 秒意味着必须在 30 秒内完成 SSH 握手并登录,超时后已建立的连接不受影响,但新连接会被拒绝。

如果希望放行更持久(例如 300 秒),把 cmd_timeout 调大即可;如果希望手工关门,可另写一个反向序列 9000,8000,7000[closessh] 段并在客户端主动敲门关闭。

3. 配置 SSH 默认拒绝

在 knockd 生效前,先把 22 端口设为默认拒绝,形成”白名单式”访问模型:

# 保证已建立连接不被中断
iptables -I INPUT 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# SSH 默认丢弃
iptables -A INPUT -p tcp --dport 22 -j DROP

# 保存规则
netfilter-persistent save

注意顺序:先放行 ESTABLISHED,再 DROP SSH。如果先 DROP 再放行,SSH 会话会在下次数据包到达时被切断。执行完可用 iptables -L INPUT -n --line-numbers 检查两条规则的先后位置。

4. 启动并设置开机自启

systemctl enable --now knockd
systemctl status knockd --no-pager

若启动失败,先看 journalctl -u knockd -n 50 --no-pager,最常见的原因是网卡名写错或配置文件中 [options] 段缩进使用了 Tab(knockd 要求使用空格)。

5. 客户端敲门脚本

把敲门动作封装成脚本,避免每次手数端口:

#!/bin/bash
# 保存为 ~/knock-ssh.sh 并 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 "[*] 敲门完成,正在连接 SSH..."
ssh -o ConnectTimeout=5 "root@${HOST}"

参数 -z 表示只探测端口不发数据,-w1 限制单次超时 1 秒。两个端口之间 sleep 0.2 是为了避免本地并发导致顺序错乱。

配置验证

按以下四步验证链路是否打通:

# 1) 确认 knockd 已在抓包
systemctl is-active knockd
ss -lunp | grep knockd

# 2) 敲门前:SSH 应超时无响应
nc -z -w2 ssh.example.com 22; echo "exit=$?"

# 3) 执行敲门并立即查看日志
tail -f /var/log/knockd.log
# 正常输出示例:
# 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) 确认放行规则已插入且带来源 IP
iptables -L INPUT -n --line-numbers | grep 22

预期结果是敲门前 22 端口探测超时,敲门后出现一条 ACCEPT tcp -- 203.0.113.9 0.0.0.0/0 tcp dpt:22 规则,30 秒后该规则自动消失。

常见问题

FAQ 1:敲门日志显示 OPEN SESAME,但 SSH 依然连不上

按以下顺序排查:

  • 时间窗太短:从敲门完成到发起 SSH 握手超过 cmd_timeout,规则已被收回。先临时把 cmd_timeout 调到 120 秒验证。
  • fail2ban 已封禁该 IP:knockd 放行的是 iptables INPUT,但 fail2ban 会往自己的链里插入 DROP 规则,二者的优先级取决于链的挂载顺序。执行 iptables-save | grep -i f2b 确认,必要时先在 fail2ban 侧 unban。
  • 前置 DROP 规则:云厂商安全组、主机上更靠前的 DROP 规则会先于 knockd 的 ACCEPT 生效。用 iptables -L INPUT -n --line-numbers 确认新规则是否排在 DROP 之前——-I INPUT 插入的是链首,通常是安全的。
  • 本地网络设备干扰:部分家用路由器对未监听端口返回 RST 而非静默丢弃,会导致 nc 只发出一个包。改用 -w1 并确认客户端发包,或在服务端用 tcpdump -i eth0 'tcp[tcpflags] & tcp-syn != 0 and port 7000' 直接看是否收到。

FAQ 2:服务器使用 firewalld 或 nftables,knockd 的 iptables 命令不生效

knockd 本身不绑定任何防火墙后端,它只是执行你写在 start_command 里的命令。因此有两种处理方式:

  • 改用 firewalld:把命令替换为 /usr/bin/firewall-cmd --add-rich-rule='rule family="ipv4" source address="%IP%" port port="22" protocol="tcp" accept',关门命令用 --remove-rich-rule。注意 firewalld 不可与 iptables 规则混用,否则会出现规则互相覆盖。
  • 改用 nftables:使用命名集合动态增删元素,例如 nft add element inet filter ssh_allow { %IP% }nft delete element inet filter ssh_allow { %IP% },规则里用 ip saddr @ssh_allow accept 引用集合。这种方式比分次增删规则更清晰,也便于审计。

另一个容易踩的坑是:若系统同时装了 firewalldiptables-services,两套规则会争夺 netfilter,建议只保留一套。

FAQ 3:敲门端口序列会不会被旁路嗅探出来

会。端口序列是明文 TCP SYN 包,处于同一广播域或同一链路上的人可以用 tcpdump 完整记录序列并重放。因此:

  • 不要在公共 Wi-Fi、共享办公网等不可信网络中执行敲门。
  • 把序列端口设置成看似随机、且与常用端口无关联的一组值,避免使用 1、2、3 或 111、222、333 这类易猜测组合。
  • 服务端可开启 [options] 里的 one_time_sequences(需要配合 knockd.conf 的序列文件),让每次使用的序列不同,重放即失效。
  • 安全要求更高时改用 fwknop 的 SPA 方案:单个加密 UDP 包,无序列可嗅探,且具备防重放计数。

还需注意:knockd 只针对 TCP SYN 做判断,对 UDP 敲门支持有限。如果从 UDP 开始敲门,必须保证中间链路不做 SYN 代理或连接跟踪优化,否则序列会被合并或丢失。

FAQ 4:敲门成功后 SSH 为何仍暴露在扫描结果里

若干在线端口扫描服务会在一段时间内复用历史探测结果,敲门后放行的 30 秒窗口有概率被扫描器命中。降低该风险的做法:

  • cmd_timeout 控制在 30 秒以内,够登录即可。
  • 让 SSH 只监听回环或内网地址,对外仅通过 knockd 放行的规则配合端口转发,实现”端口本身不存在”的效果。
  • 登录成功后立刻执行反向序列主动关门,不依赖超时自动收回。

总结

端口敲门解决的是”服务可见性”问题,而不是”身份可信”问题。本文给出的配置组合了三个关键控制点:SSH 默认 DROP顺序 + 时间窗 + SYN 标志三重校验cmd_timeout 自动收权,落地成本低于一台跳板机,防护收益高于单纯更换 SSH 端口。

实施时优先级建议为:先确认备用登录通道可用,再改防火墙默认策略,最后启用并验证 knockd;任何时候都不要在只有一个登录入口的前提下把 SSH 改成默认拒绝。若环境对隐匿性要求更高,应直接评估 fwknop 等单包授权方案,并配合 SSH 密钥认证、2FA 与 fail2ban 形成分层防御。