把 SSH 端口彻底藏起来:端口敲门到底解决了什么
只要服务器暴露在公网,22 端口(或你改过的任意 SSH 端口)每天都会收到成百上千次暴力破解尝试。fail2ban 能封 IP,改端口能躲过自动化扫描,但本质上端口始终在"应答"——扫描器一探就知道那里有个 SSH 在监听。真正的思路是把 SSH 端口做成"平时完全不存在":防火墙默认丢弃所有到 22 的连接,只有当你按特定顺序"敲"过一串端口后,防火墙才临时为你的 IP 开一个小口,让你连上,随后自动关闭。
这套机制叫端口敲门(port knocking),实现工具是 knockd。它不替代 SSH 密钥和 fail2ban,而是在它们之前多设一道"隐形门":对扫描器而言,你的服务器上根本没有开放任何管理端口,连指纹都抓不到。
本文把 knockd 从安装、服务端配置、客户端敲门脚本,到 systemd 加固与常见坑位走一遍,并顺带讲清楚它和单包授权(SPA,如 fwknop)的取舍。
原理:为什么"顺序"比"密码"更难被猜
端口敲门的安全性来自两个特性。第一是它不响应:默认策略是 DROP 而不是 REJECT,扫描器发来的 SYN 得不到任何回包,超时判断会让它认为端口被过滤而非开放,从而放弃。第二是敲门序列是自定义的端口号序列,比如 7000,8000,9000,敲对了才开门。这个序列本身可以看作一道"密码",而且是不在网络上传输任何明文的密码——每次敲门只是一个 TCP SYN 到某个端口,中间人看到的最多是一串奇怪的连接尝试,无法直接还原出"序列"的语义。
不过要客观说清楚:单纯依赖固定序列的端口敲门,安全性属于"隐晦式防护",序列一旦被日志记录或被流量分析还原,就会失效。因此它必须和 SSH 公钥认证、fail2ban 叠加使用,把 SSH 本身加固到即使门被打开也难以攻破。它是纵深防御的一层,不是唯一防线。
第一步:安装 knockd 并准备内核防火墙后端
knockd 通过监听网卡上的包头来工作,需要 libpcap。它本身不接管防火墙,而是敲对序列后调用 iptables 命令来临时放行。Modern Debian/Ubuntu 上 iptables 已经是 nftables 的兼容前端,敲门的规则最终会被翻译成 nft 规则,这没有问题,但要注意如果用 ufw 管理规则,两者可能互相覆盖,建议对敲门的放行规则直接用 iptables/nft 命令写,并在 ufw 规则之后执行。
# Debian/Ubuntu
apt update && apt install -y knockd
# 确认 iptables 版本与后端(nft 或 legacy 都可,knockd 调用的是命令)
iptables --version
# 典型输出:iptables v1.8.7 (nf_tables)
# 检查 knockd 是否随系统启动
systemctl status knockd
第二步:写 /etc/knockd.conf
配置文件分为 [options] 和若干个命名序列段。每个段里定义 sequence(敲门端口顺序)、seq_timeout(多久内敲完算数)、tcpflags(必须带 SYN)、以及 start_command/stop_command(开门与关门动作)。
[options]
# 记录敲门日志,便于排查与审计
LogFile = /var/log/knockd.log
# 监听哪块网卡,单网卡机器填 eth0,云主机常是 ens3
Interface = eth0
[openSSH]
sequence = 7000,8000,9000
seq_timeout = 10
tcpflags = syn
# 敲对后放行 SSH,并打上一条注释便于识别;60 秒后自动锁回
start_command = /usr/sbin/iptables -A INPUT -s %IP% -p tcp --dport 22 -j ACCEPT -m comment --comment knockd-ssh
stop_command = /usr/sbin/iptables -D INPUT -s %IP% -p tcp --dport 22 -j ACCEPT -m comment --comment knockd-ssh
cmd_timeout = 60
[closeSSH]
sequence = 9000,8000,7000
seq_timeout = 10
tcpflags = syn
command = /usr/sbin/iptables -D INPUT -s %IP% -p tcp --dport 22 -j ACCEPT -m comment --comment knockd-ssh
几个关键点解释一下。%IP% 会被替换成敲门来源 IP,所以只有敲门的那台机器能连上,别人即使知道序列也连不上(除非也用同样 IP)。cmd_timeout = 60 让开门命令执行 60 秒后自动执行 stop_command 关回,等于给了一个"限时窗口",比永久放行安全得多。tcpflags = syn 要求敲门包必须是 SYN,避免某些情况下普通数据包误触发。
序列端口号建议选在高端口、且不要用有服务的端口(如 80/443/22),避免敲门被真实服务抢先响应。选 7000,8000,9000 这类空闲端口最稳妥。
第三步:接管 SSH 的默认拒绝策略(保命顺序)
这是整个配置里最危险的一步:你要先把 22 端口对所有来源 DROP 掉,才能体现"隐形"效果;但如果你此刻正通过 SSH 连着服务器,DROP 生效的一瞬间你自己就会被切断。正确做法是先为自己当前 IP 放行一条"保命规则",再配置默认拒绝。
# 1. 先给自己留一条保命规则(当前登录 IP),务必先执行
iptables -I INPUT 1 -s $(echo $SSH_CLIENT | awk '{print $1}') -p tcp --dport 22 -j ACCEPT
# 2. 再把 SSH 的默认入口设为 DROP(放在保命规则之后)
iptables -A INPUT -p tcp --dport 22 -j DROP
# 3. 确认规则顺序:保命规则必须在 DROP 之前
iptables -L INPUT -n --line-numbers
注意顺序:iptables 从上到下匹配,第一条命中即生效。你把敲门放行规则通过 -A 追加在 DROP 之后是不行的——追加的规则永远排在 DROP 后面,包在 DROP 处就被丢了。所以 knockd 的 start_command 里务必用 -I(插入到最前面)而不是 -A。上面示例里为了可读性写了 -A,实战中应改为 -I INPUT 1。这是无数人被卡住的坑:敲对了门,但规则沉在 DROP 后面,照样连不上。
规则持久化在 Debian 系用 iptables-persistent 保存到 /etc/iptables/rules.v4,否则重启后全部丢失,服务器会变回"裸奔"状态。
第四步:客户端怎么敲门
敲门客户端就是按顺序向那几个端口发起连接。用 knockd 自带的 knock 命令最方便:
# 客户端安装(本地机器上)
apt install knockd # 或 brew install knock
# 按序列敲门(三个端口按顺序轻触,不发数据)
knock -v your-server.com 7000 8000 9000
# 敲完立即连接(在 60 秒窗口内)
ssh user@your-server.com
如果本地没有 knock 命令,用几行 shell 也能模拟,本质就是按顺序 telnet/curl 到那几个端口:
# 纯 bash 敲门(不依赖 knock 命令)
for p in 7000 8000 9000; do
timeout 1 bash -c "echo > /dev/tcp/your-server.com/$p" 2>/dev/null
sleep 0.3
done
echo "敲门完成,10 秒内连 SSH"
ssh user@your-server.com
也可以把敲门和连接合并成一条别名,写进本地 ~/.bashrc:
alias sshbox='knock -v your-server.com 7000 8000 9000 && ssh user@your-server.com'
第五步:用 systemd 加固 knockd 服务
knockd 以 root 运行且监听原始数据包,理应给它最小权限。用 systemd 的 drop-in 覆盖默认 service,限制它能碰的东西,即使 knockd 本身被攻破也跑不出沙箱。
# 创建 drop-in 覆盖
mkdir -p /etc/systemd/system/knockd.service.d
cat > /etc/systemd/system/knockd.service.d/harden.conf << 'EOF'
[Service]
# 独立用户运行(若 knockd 需要 root 调 iptables,可保留 root 但用能力限制)
User=root
# 去掉一切不必要的能力,仅留下发 iptables 所需
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW
# 只读挂载大多数目录
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
NoNewPrivileges=yes
Restart=on-failure
EOF
systemctl daemon-reload
systemctl restart knockd
systemctl status knockd
注意:这一节有个现实约束——knockd 需要通过执行外部 iptables 命令来改防火墙,如果把它限制得太死(比如去掉 CAP_NET_ADMIN 或禁止 fork 子进程),开门动作会静默失败。所以加固配置里保留了 NET_ADMIN/NET_RAW,其他能砍的都砍掉。改完必须实测敲门是否仍能开门。
第六步:验证与故障排查
配置完不要靠"应该没问题"就收工,按下面顺序实测一遍,每一步都能定位到具体环节。
# 1. 确认 knockd 在监听(应看到进程和日志)
ps aux | grep knockd
tail -f /var/log/knockd.log
# 2. 从外部 IP 尝试直连 22,应当是超时(而非 refused)
# 超时=端口被 DROP,符合预期;refused=端口在监听但没有规则拦,配置没生效
nc -zv -w 3 your-server.com 22
# 3. 敲门后再试,应当连上
knock -v your-server.com 7000 8000 9000
nc -zv -w 3 your-server.com 22
# 4. 看敲门是否被记录、iptables 是否插入了放行规则
tail -5 /var/log/knockd.log
iptables -L INPUT -n --line-numbers | head
常见问题:敲了门还是连不上——九成是 iptables 规则用 -A 追加到了 DROP 后面,改成 -I INPUT 1;或 Interface 填错网卡名,ip a 确认。自己把自己锁在外面——DROP 生效前没留保命规则,只能通过 VPS 控制台的 VNC 进救援模式修复。敲门日志有记录但不放行——tcpflags 不匹配或端口被其他服务占用导致序列被打断。
knockd 单包授权(fwknop)怎么选
knockd 的固定序列有一个短板:序列本身是静态的,理论上可被长期流量分析还原,且它不验证敲门者身份——任何知道序列的人都能开门。更严格的方案是 fwknop 的单包授权(Single Packet Authorization):客户端发一个加密签名的 UDP 包,服务端解密验签通过后才放行,序列不再是明文可观察的,且集成了 HMAC 防重放。
取舍建议:个人站长日常维护,knockd 已经能把 99% 的扫描器挡在门外,配置简单、心智负担低,性价比最高。如果服务器承载敏感数据、或你所在环境担心旁路监听,再上 fwknop。两者都能和 SSH 公钥、fail2ban 叠加,不冲突。
FAQ
Q:端口敲门会不会影响正常的 SSH 自动任务?
A:会。任何从新 IP 发起的自动化 SSH(CI 部署、定时同步)都要先敲门,否则连不上。建议给固定的自动化源 IP 直接在防火墙里长期放行,不参与敲门。
Q:敲门序列多久需要换一次?
A:没有硬性要求,但它是"密码"性质,建议换管理员时一并更换,并避免使用 1、2、3 这类连续端口。
Q:敲门后 60 秒窗口太短怎么办?
A:调大 cmd_timeout,但别调成 0 或永久,否则就失去了限时窗口的意义。也可以配置 stop_command 由客户端主动"反向敲门"关闭。