SSH 与 VPN 入站规则:允许清单究竟控制什么
沿一条连接从客户端走到主机:它跨过哪个钩子、为什么挂起和拒绝代表不同规则,以及为什么 reject 从来不是策略。
改动任何规则之前先连接一次,记下客户端是挂起还是被立即拒绝。这一处差别就能告诉你哪条决策丢掉了数据包,并省下一小时读策略文件的时间。其余过程很短:数据包穿过钩子、匹配规则,然后要么到达监听者,要么不到。允许清单不过是围绕这三件事的记账。
跟踪一次 SSH 尝试走到 input 钩子
客户端向主机地址的 22 端口发送 SYN,内核判断该包是否属于本地服务。若属于,input 路径执行,规则集要么接受、要么丢弃。沉默意味着 SYN 在监听者看到它之前就死了,而快速拒绝意味着有东西用重置做了回应。这是不同规则给出的不同判定,两者都是合理结果。
接着确认监听者,因为不存在的服务没有任何规则能救。运行 ss -ltnp,检查是否有东西绑定在预期的端口和地址族上,再在客户端读 ssh -v,看它停在哪里。
转发是另一个决策
在 VPN 服务器上更有意思的钩子是 forward,因为客户端流量是穿过这台机器,而不是交给它。只运行自家 VPN 客户端的主机,需要的规则集远小于替别人转发数据包的主机。把两种设计混在一台机器上,产出的规则看着对,却什么都放不过。
把内部策略写成独立的问题:哪些客户端地址可以经隧道到达哪些目的地。把管理通道(例如 SSH)留在 input 路径上,并给它自己的来源限制。测试期间加的宽松 forward 规则,就是那种比测试活得更久的经典缺陷,所以改动之后要复查整个规则集,而不是你改过的那一行。打开隧道监听者,并不等于允许每个已连接的客户端管理主机。
drop 与 reject 是动作,不是策略
基础链的策略只有接受或丢弃两种,选择清单到此为止,正如nftables 参考文档所述。reject 是带指定响应的规则动作,所以把它写成策略在 nftables 和 iptables 上都行不通。实践中 drop 不给客户端任何信息,而 reject 给它一个可以据以处理的错误。
table inet admin {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
ip saddr 203.0.113.0/28 tcp dport 22 accept
udp dport 51820 accept
icmp type { destination-unreachable, time-exceeded } accept
}
}
把这张草图读成一种决策的形态,而不是一段可粘贴的文件,因为其中的地址范围只是示例。在 SSH 端口上显式 reject,会让未授权的客户端一秒内失败,而不是等超时,这缩短你自己的排查时间。沉默会劝退随手扫描,却不会告诉已授权用户任何事。按服务逐个选择并写下理由,否则下一个人会把它改回去。
留下真正需要的控制流量
屏蔽所有 ICMP 是个流行默认值,它会破坏路径 MTU 发现,症状是连接建立后在较大的包上卡住。IPv6 的邻居发现依赖它自己的 ICMPv6 类型,丢掉这些会在任何 TCP 规则被查询之前就断掉链路。两者都该以具名依赖的形式进入允许清单,而不是一刀切的放行。悄悄破坏发现的策略,比允许一点诊断流量的策略更糟。
对你自己发起的连接放行 established 与 related 状态,让有状态引擎处理回应。然后确认回程路径在外侧没有被挡住,因为上游只限制出站流量,会产生你正要归咎于主机的同一个超时。
先证明可恢复,再停用旧路径
开着控制台会话,应用改动,并从另一个终端打开一条全新的管理连接。现有会话什么都证明不了,因为它可能熬过一条阻断所有新登录的规则,这使它成为防火墙工作里最有说服力的假阳性。若监控系统会检查主机,要在改动前而不是第一条告警后加上它的源地址。然后用一个大包和一个小包各测一次,证明发现功能仍然有效。
对 VPN 服务,要从未连接过的客户端重来一遍,因为恢复的会话可能复用新会话必须重新协商的状态。把通过的规则集原样记录下来,附上校验和或副本,这样以后的修改是可以比对而不是猜测。与掌握规则顺序的人协调,因为更早的规则会悄悄压过你新加的那条。