资讯动态

用 nftables 集合与 timeout 实现 SSH 端口敲门:让公网扫描器无门可敲

发布时间:2026/9/25 3:54:20 来源:尧图企业网站定制
SSH端口暴露在公网上每天被各类扫描器来回捶打日志里全是暴力尝试记录这是很多运维心里的痛。“端口隐藏”这件事本质上不是把服务藏起来而是改变攻击面对外表现为“端口不存在”或“拒绝连接”只有掌握敲门序列的调用方才能临时获得访问通道。这篇文章我会用 nftables 自带的集合set与超时timeout机制实现一套不需要额外守护进程的端口敲门方案让 SSH 或其他管理端口在公网扫描下快速“隐身”。这套方案很适合被扫描问题困扰的运维、对安全感兴趣的开发者以及想把手头防火墙规则升级为现代 nftables 方案的朋友。不需要太复杂的前置知识只要能看懂基本的 nftables 规则就能照着落地。1. 端口隐藏到底在解决什么问题1.1 为什么只依赖改端口不够很多朋友第一反应是把 SSH 从 22 改成 2222以为这样可以躲开扫描。但全端口扫描工具根本不挑口几十秒就能把全部 65535 个端口扫一遍2222 同样会暴露在结果里。现在不少扫描器会优先扫描高频端口段但哪怕你的端口不在高频段里有心人拿着常见的“常见端口清单”扫完也能很快覆盖掉。改端口的真实成本很低对应的防护收益也有限。相比之下端口隐藏做得更彻底探测者看到的不只是一个“高端口”而是这个端口根本不存在、完全拒绝服务。敲门技术就是实现这种“不存在感”的一种手段——服务端只信任那些能够证明自己“按顺序敲过门”的地址。所以这篇博文的核心目标很直接把 22 端口从“暴露”变成“需要验证后才暴露”。1.2 nftables 为什么适合承载这套逻辑从内核层面来说nftables 是当前推荐的 netfilter 前端继承了 iptables 的钩子能力但把规则、集合、映射的抽象做得更清晰。尤其是集合set支持超时timeout特性这几乎就是为敲门状态机量身定做的我们可以把敲过第一道门的来源 IP 放进集合 A 并标记 10 秒存活敲过第二道门的放进集合 B依次往下最终完成整个序列的 IP 才被放入放行集合。整个过程不需要写一堆 echo iptables 命令拼成的剧本也不需要一个常驻进程去 tail 日志规则的更新由 nftables 内核自身完成。这个优势在流量大的服务器上非常明显。iptables-legacy 规则多时是线性遍历而 nftables 的 set 是可以走哈希查找的更关键的是flags timeout让元素自动过期IP 白名单不会越积越多。列个简单对比维度iptables-legacynftables查找性能规则多时线性遍历明显集合可哈希查找更快集合超时回收需借助 ipset 和外部定时清理原生支持 timeout 自动过期配置可读性传统写法混乱参数难记类似脚本语言层次清晰规则更新原子性批量替换有加载窗口nft -f 整体加载原子生效我在很早以前就用过 iptables 加 ipset 的“动态放行”方案维护成本实在不低要定时清空 ipset 残留条目还要写 crontab 配合。nftables 直接把这些能力做进了语法里这才是现代防火墙该有的样子。1.3 攻击面治理的价值观端口隐藏属于“攻击面最小化”的一环但我不建议把它当成银弹。它解决的是批量扫描、预探测、自动化攻击成本极低的场景面对有人盯着你的流量来玩普通敲门的序列可能被观察并被重放。所以我的观点很明确端口隐藏是纵深防御的第一道伪装不是最后一道锁。SSH 本身必须用密钥认证账号要禁用密码登录fail2ban 作为补充再加上这套敲门才是一个合理的组合。这篇博文的核心是讲清楚 nftables 敲门怎么做但它真正值钱的地方在于你对攻击面模型的认知——每一步减少暴露都是在给攻击者增加成本。2. 环境准备与基础规则骨架2.1 系统与版本要求我测试的环境是 Debian 12 / Ubuntu 22.04nftables 1.0Linux 内核 5.15。新一点的系统基本都自带 nftables但为了保险可以这样装apt update apt install -y nftables systemctl enable nftables先确认版本nft --version如果输出类似nftables v1.0.2 (Lester Gooch #4)那就正常。还需要确认内核模块已经加载lsmod | grep nf_tables没有输出就手动加载modprobe nf_tables另外做这套实验时需要理解 TCP 状态跟踪所以nf_conntrack内核模块也建议确认存在。一般来说发行版默认都会加载不用太操心。 注意不要一上来就用生产服务器做实验先在测试机器或云主机上把流程跑通再把规则改造成自己生产环境的样子。2.2 一台“有底线的”服务器骨架实验前先给自己留一条后路。在本地物理控制台、带外管理面板或者 screen/tmux 里保持一个 root 会话防止规则一上就把自己锁在外面。绝不要先用policy drop然后测试到一半断线。我自己的习惯是先写一个临时保底命令at now 5 minutes nft flush ruleset如果 5 分钟内没出事我再把这个计划任务删除。这个方法虽然土但比被锁在门外干瞪眼强多了。初始化一张干净的骨架规则集。先清空再加载nft flush ruleset nft -f /etc/nftables.conf基础配置文件可以写成这样#!/usr/sbin/nft -f flush ruleset table inet filter { chain input { type filter hook input priority filter; policy drop; ct state established,related accept iif lo accept ct state invalid drop } chain forward { type filter hook forward priority filter; policy drop } chain output { type filter hook output priority filter; policy accept } }这里解释几个关键点。inet表同时覆盖 IPv4 和 IPv6所以不需要建两张表ct state established,related accept的作用是保证已经建立的连接不会被新的默认策略掐断这对 SSH 掉线问题有很大帮助ct state invalid drop会把畸形包直接丢掉减少无意义的告警。至于forward链默认 drop 是给这台机器当路由器场景兜底如果你只是普通服务器也可以暂时不定义 forward 链。2.3 检查规则是否生效加载后立刻开一个新的 SSH 窗口确认新连接能连上。能连上再执行nft list ruleset看到三条链和policy drop说明基本框架起来了。此时外部的新连接会被丢弃但已经建立的 SSH 不会断因为这条连接命中了established规则。在排查和调整期间我强烈建议背下来这条临时候补命令nft add rule inet filter input tcp dport 22 accept万一规则写错了、自己又被挡在门外只要还有带外通道或 tmux 里的 root 会话就可以执行这条命令把 SSH 放行出来。3. 敲门技术原理与方案选型3.1 敲门流程拆解敲门技术的模型像一个机关门门上有三个按钮必须按照 7000 → 8000 → 9000 的顺序各按一次且间隔不能太久门才会开按错顺序或超时一切从头再来。放到网络里这个“按钮”就是 TCP 连接尝试。攻击者扫到 7000 端口他看到的只是一个拒绝服务但服务端的 nftables 其实已经暗地里把他的 IP 标记为“已按过第一下”如果他在很短时间内又去碰 8000就被标记为“已按过第二下”当第三次碰到 9000 时SSH 端口才对他临时开放。整个过程涉及三个关键设计序列唯一性门铃端口不要用连续端口也尽量别用容易被猜到的数字窗口期相邻两次敲门的时间差必须落在规定窗口内否则状态作废放行后的过期时间白名单不是永久生效到期自动回收避免长期挂着一个放行状态。3.2 三种主流实现路线对比先看第一梯队knockd。这是老牌敲门工具配置在/etc/knockd.conf里指定sequence 7000,8000,9000动作可以调用nft命令添加规则。优点是简单可靠网上资料也多缺点是引入一个额外的守护进程它通过 libpcap 抓取数据包在高并发流量下有一定开销而且它跟 nftables 之间是外部命令联动状态不透明。第二条路线日志加脚本。从 sshd 日志或 nftables 日志里看到特定端口访问再用 awk/sed 提取 IP执行 nft 命令加入白名单。这条方案因为简单常出现在老博客中但它有两个天然问题日志洪泛可以拖垮解析脚本从日志到添加规则有几秒延迟敲门体验很差。我自己早期试过后来放弃了。第三条路线纯 nftables 集合状态机也就是本文主推方案。本质是让 netfilter 在匹配到敲门包时直接对指定集合做add动作并附带一个超时时间。这个机制不需要额外进程、不依赖日志、操作是内核原子性的性能好、可靠性高。三种方案对照方案依赖维护成本性能安全性knockd额外守护进程中高并发时一般基于源 IP序列可被观察日志脚本crontab/sed/nft高低依赖日志解析延迟明显nftables 内置集合仅内核 nftables低高与 knockd 类似可扩展加密3.3 集合 timeout 机制就是临时通行证nftables 里的flags timeout集合会在每个元素创建时绑定一个存活时间到期后自动删除不需要外部定时器。这个机制被我用作“按过门的临时状态”第一次敲门产生的标记在 10 秒后消失所以你没在窗口期内继续敲第二个端口就重新开始。用生活类比解释最直观保安手里有一叠“临时通行券”他看到你按了第一道门铃就往你手上贴一张 10 秒有效的票你没在 10 秒内按第二道门铃票自动作废。你每过一道门票会在上一个的基础上换成一张新票最后一次换到的票让你能进 SSH 房间而且这张票的有效期是 1 小时。这种由内核自动回收的设计不会留下越来越多的无用 IP 条目也大大减少白名单维护工作量。4. 核心实现nftables 端口敲门完整配置4.1 定义三段式状态机规则集直接给一份可以放到/etc/nftables.conf的配置。门铃序列我用 7000、8000、9000 演示SSH 端口是 22各位根据实际环境替换。窗口期 10 秒最终放行时间为 1 小时。#!/usr/sbin/nft -f flush ruleset table inet knock_demo { # 第一道门的临时通行证 set knock_stage1 { type ipv4_addr flags timeout } # 第二道门的临时通行证 set knock_stage2 { type ipv4_addr flags timeout } # 最终白名单 set ssh_allowlist { type ipv4_addr flags timeout } chain input { type filter hook input priority filter; policy drop; ct state established,related accept iif lo accept ct state invalid drop # 白名单内 IP 直接放行 SSH tcp dport 22 ip saddr ssh_allowlist accept # 第一击碰 7000 → 把来源 IP 放进 stage1 tcp dport 7000 add knock_stage1 { ip saddr timeout 10s } reject with tcp reset # 第二击仅在 stage1 里有记录的 IP 碰 8000 → 移入 stage2 tcp dport 8000 ip saddr knock_stage1 add knock_stage2 { ip saddr timeout 10s } reject with tcp reset # 第三击仅在 stage2 里有记录的 IP 碰 9000 → 进入白名单有效期 1 小时 tcp dport 9000 ip saddr knock_stage2 add ssh_allowlist { ip saddr timeout 3600s } reject with tcp reset # 其余一律丢弃 counter drop } }关键点逐条解释。首先三个集合用type ipv4_addr。如果你的服务器有 IPv6 公网地址不能漏掉这个维度后面我会单独讲双栈改法。其次放行 SSH 那条规则放在敲门规则之前没有问题因为ssh_allowlist默认是空集外部没有敲过门的 IP 不会匹配它会继续往下走最终走到counter drop。第三所有敲门动作最后都以reject with tcp reset结束客户端会立刻收到 RST甚至会觉得“端口关闭”但实际上服务端已经偷偷记下状态。4.2 为什么用 reject 而不是 drop 或 accept这里值得细说。如果使用drop对端 TCP 会一直等待连接超时客户端看到的是“连接超时”敲一次门可能要等两三秒敲三次门就要十几秒对本地体验影响不小。如果使用accept对端会完成 TCP 握手扫描器会认为 7000 端口“开放”这显然和“端口隐藏”的初衷矛盾。使用reject with tcp reset则模拟了一个真实关闭的端口既不暴露又能让敲门工具快速结束。这是一个很小但影响体验的细节很多教程照抄 drop实际操作时明显感觉到卡顿。4.3 加载与验证把配置写进/etc/nftables.conf后执行nft -f /etc/nftables.conf然后立刻开一个新 SSH 窗口验证原来的 SSH 是不是被保护住了。如果新窗口连不进去没关系这正是预期效果。接着查看规则集nft list ruleset再用一台客户端模拟敲门for port in 7000 8000 9000; do nc -z -w1 your_server_ip $port done ssh your_server_ip理想情况下前三次 nc 都返回“拒绝连接”但第三次之后 ssh 已经能连上。为了确认集合内的状态可以同时开一个终端执行nft list set inet knock_demo ssh_allowlist你会看到 SSH 放行集合里出现了客户端的 IP并且带有expires时间。更精确的检查命令是nft get element inet knock_demo ssh_allowlist { 客户端IP }它会告诉你这个条目还剩多少秒有效。4.4 客户端敲门脚本怎么写才顺手上面用 nc 手动敲三次虽然直观但日常使用不方便。我会写一个小脚本放到本地任何可执行路径比如/usr/local/bin/knock-ssh#!/usr/bin/env bash set -euo pipefail SERVER${1:?usage: knock-ssh server} SEQUENCE(7000 8000 9000) for port in ${SEQUENCE[]}; do nc -z -w1 $SERVER $port || true done exec ssh $SERVER这样每次连服务器前先执行knock-ssh my-server几秒后直接进入 SSH。如果想在 Windows 上也能用用nmap -Pn -p 7000,8000,9000或者配合/dev/tcp都能实现原理都一样只要确实发出 TCP SYN 即可不要求握手成功。4.5 如果服务器本身是 NAT/云主机怎么办公网云主机通常直接配置就够了。但如果这台服务器还承担 NAT 网关角色或者你在内网环境里还需要在forward链写对应规则。比如要保护的内网机器是 192.168.1.10那敲门规则涉及的不是input链而是forward链集合记录来源 IP最终放行规则变成tcp dport 22 ip daddr 192.168.1.10 ip saddr ssh_allowlist accept。思路完全一致只是链换成forward。建议先拿最简单的直连场景跑通再往 NAT 迁移。5. 常见问题与排查技巧实录5.1 为什么敲完门仍然连不上 SSH最常见的几个原因按概率排第一客户端 IP 和集合记录的 IP 不一致——如果你是经过 NAT 网关或代理访问服务器“你看到的源 IP”可能不是真正送达服务器的 IP要确认nft get element inet knock_demo ssh_allowlist里存的地址是什么第二SSH 连接被另一层防火墙拦截比如云平台安全组、fail2ban、SELinux第三规则顺序错了比如放行规则被写到了其它 drop 规则之后。排查时我习惯先在服务器上用tcpdump -i any port 22看报文有没有到达内核再逐条检查nft list ruleset定位是在哪一环断掉。记住一个大原则先分清是“没到 Linux”还是“到了但被 nftables 拒了”这能帮你少绕半天弯。5.2 敲门窗口期到底设多少合适窗口期太短手动敲门很容易失败太长又相当于给了攻击者更多时间利用被劫持的标记。我的经验值第一道门 10 秒左右第二道门可以稍长一点比如 15 秒最终放行 2 小时。不追求极低的窗口值因为这套机制主要防批量扫描而不是防定向攻击。如果全部用脚本自动敲门整个过程在 1 秒内完成10 秒窗口绰绰有余。但如果偶尔需要手动敲建议把 stage1 和 stage2 的 timeout 都调到 20 秒把最终白名单调到 8 小时然后定期用脚本清理。不要为了追求“看起来安全”把窗口压得太死硬件上的体验会严重恶化。5.3 有 IPv6 地址该怎么办如果你只用type ipv4_addr写规则IPv6 报文完全不会命中这些敲门规则。这样一来白名单放行规则不响应 IPv6 地址但默认 drop 策略依然会拦下未放行的 IPv6 流量。真正的问题在于如果你实际想通过 IPv6 使用 SSH那 IPv6 的敲门通道就不存在等于自己把自己的路堵死。一个实用的双栈改法是把三个集合类型换成ipv6_addr规则里用meta nfproto ipv6限定table inet knock_demo { set knock_stage1 { type ipv6_addr; flags timeout; } set knock_stage2 { type ipv6_addr; flags timeout; } set ssh_allowlist { type ipv6_addr; flags timeout; } chain input { type filter hook input priority filter; policy drop; ct state established,related accept iif lo accept tcp dport 22 meta nfproto ipv6 ip6 saddr ssh_allowlist accept tcp dport 7000 meta nfproto ipv6 ip6 saddr knock_stage1 accept tcp dport 8000 meta nfproto ipv6 ip6 saddr knock_stage1 add knock_stage2 { ip6 saddr timeout 10s } reject with tcp reset tcp dport 9000 meta nfproto ipv6 ip6 saddr knock_stage2 add ssh_allowlist { ip6 saddr timeout 3600s } reject with tcp reset } }如果你只需要 IPv4那就保持原样但要确认服务器上的 IPv6 服务已经关闭或没分配公网地址免得留一个不设防的逻辑出口。5.4 开机后规则丢了怎么办规则丢了通常有两个原因一是你在命令行加载了但没写入配置文件二是 nftables.service 没有开启。我建议直接把规则写进/etc/nftables.conf然后systemctl enable nftables systemctl restart nftables用 systemd 管理时加载失败会在 journal 里留下错误。排查命令journalctl -u nftables -n 50另外要特别注意云平台安全组这个独立于操作系统的控制层。哪怕内核规则写错只要安全组没放行 22你依然会被拦住反过来安全组放行了 22内核里的policy drop一样会拒绝。排查时要先分清访问到底是在哪一层被掐断的否则容易原地打转。5.5 敲门时偶尔失败重试就好了吗TCP 的 SYN 重传可能导致同一个包被重复处理但集合add是幂等的重复添加不会产生副作用放心重试即可。唯一值得注意的是如果你在脚本里每次用nc -z等结果中间间隔超过窗口期就可能失败。加上-w1这种超时限制后三次敲门通常在 2 秒内完成正常不会超时。如果客户端和服务器之间的网络延迟较高比如跨洋访问10 秒窗口也可能不够稳定。此时可以把 timeout 提升到 20 秒或者改用 hping3 从应用层直接发 SYN 包可靠性更高。我实测不同网络环境下10 秒窗口在国内常规线路够用但从海外回连时建议放宽到 20 秒。5.6 自检清单上线前一定过一遍新窗口 SSH 还能连上吗规则刚加载完立刻验证nft list ruleset里的表名、链名与后续操作一致吗客户端敲门时源 IP 是不是服务器上看到的那一个重启机器或重载 nftables 后配置是否还在IPv6 是否留了一个不设防的口子6. 从敲门到更安全的玩法6.1 给规则加计数器和日志调试和上线观察时可以在每条敲门规则上加counter和log这样就能清楚看到谁在敲门。例如tcp dport 7000 add knock_stage1 { ip saddr timeout 10s } log prefix knock1: counter reject with tcp reset但生产环境要小心日志量。有人敲错门不算攻击可如果高频出现可能是扫描器正在探测你的敲门端口规律。建议只在调试验证阶段开启 log上线后保留 counter 即可。counter可以帮你统计每个敲门端口被访问的次数对判断攻击模式很有帮助。6.2 敲门序列的口令化硬编码一组序列有一个明显风险一旦序列泄露比如出现在配置备份、shell 历史或被他人抓包看到保护就形同虚设。因此我强烈建议把序列放进密码管理器的自定义字段每部署一台服务器就用不同的一组端口不要所有机器都设置同一个门铃。如果需要更高安全等级可以把“无口令敲门”升级为“加密单包授权”。思路是把序列用对称密钥加密后放进一个 UDP 包服务端用同一密钥解密、校验时间戳后放行这样抓包也无法重放。这个方向有现成的 fwknop 实现感兴趣可以继续深入。6.3 我踩过的一些坑第一千万别在敲门配置里顺手把 SSH 端口改成非标准端口再叠加这套规则。这样做会增加很多排查复杂度实际收益有限。第二reject with tcp reset的语法在高版本 nftables 上是reject with tcp reset不要在旧版本里直接照抄先nft --version确认。第三如果和 fail2ban 共用你要知道 fail2ban 默认写入的是 iptables-nft 规则两个工具同时管理同一张表会让 nftables 规则集变得混乱。我的建议是新的敲门方案里不要再用 fail2ban 去管理 SSH或者把 fail2ban 的 ban 动作改成写 nftables 集合避免双管理冲突。第四从实际运维出发所有脚本都要考虑“不可达”的场景。敲门脚本本身失败了不要立刻 panic检查集合里的元素状态比看 nc 输出更可靠。最后把门铃端口和 SSH 端口分开写进文档别把两者都猜成 22。我就是因为端口写错愣是排查了一个晚上真的很耽误事。这套方案跑通后你会发现日常的扫描噪音明显减少运维安全感也提升不少。如果你的场景比我这套更复杂比如有多个管理端口需要隐藏完全可以照着同样的思路在多几个 stage 集合上继续延伸。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑