一台小型 Linux 服务器为它托管的应用程序使用出站 VPN,而管理员仍想从隔壁工位通过 SSH 远程连接它。这是合理需求,通常既不需要端口转发,也不需要服务商地址,更不需要在隧道上开洞。真正需要的是一条刻意设计的管理路径,而不是被动继承 VPN 客户端对路由表所做的改动。

下面的步骤假设管理员与服务器位于同一 LAN。远程管理是另一个问题,也需要另一套答案。

把 SSH 绑定到管理员使用的地址

从监听端开始。ss -ltnp | grep :22显示 sshd 绑定在什么地址上,而绑定在 0.0.0.0 上的监听会接受来自所有接口的连接,包括隧道。绑定到 LAN 地址范围更窄,也能让意图更明确:ListenAddress 192.168.10.20放在sshd_config中,然后做配置检查并重新加载。这个地址里打错字比通配符更容易暴露,因为地址不存在时 sshd 会拒绝启动。

主机密钥校验和按用户认证要原样保留。“这只是本地网络”不是认证方式;因为路径短就跳过密钥校验的管理通道,是一条等着被滥用的通道。DHCP 保留或静态地址能让目标可预期,同时又不把它变成服务器的公开身份。

让 LAN 路由留在隧道之外

地址位于 192.168.10.0/24 的主机对该前缀有一条直连路由,而在同一张表中,更具体的路由会胜过 VPN 的默认路由。这就是隧道升起后本地 SSH 往往仍能自行工作的原因。要验证而不是假设:ip route get 192.168.10.50会给出内核将要使用的接口和源地址,答案应当是 LAN 设备而不是隧道。也要从管理员的机器上重复检查,因为最终是它自己的表决定数据包能否到达。同一根网线上的两台主机,可能对哪个方向可达存在分歧。

这个假设会在两处失效。基于策略的路由规则可能在查主表之前,就把流量送往默认指向隧道的另一张表。带有锁定或 kill switch 功能的 VPN 客户端,也可能拦下不进入隧道的流量,无论路由表怎么说。

被路由的管理 VLAN 是另一种情况。如果管理员位于本地路由器之后的 192.168.50.0/24,服务器就需要一条经 192.168.10.1 指向该前缀的显式路由,而且这条路由必须在重连后仍然存在。例外要只针对管理前缀;对私有网段的宽泛例外会抵消隧道的目的。

防火墙例外要和规则一样窄

如果主机运行防火墙,只允许来自管理源的 SSH,并且只在 LAN 接口上放行。允许从任何地方访问 22 端口的规则比需求更宽,也更难在评审中解释清楚。在 nftables 主机上,规则应放在匹配接口和源的输入链中;在 iptables 主机上,等价做法是两条分别匹配-i和-s的规则。在规则上注释它服务于哪个管理员组,这样下一次审计就不必猜 22 端口为什么是开着的。

也要检查 VPN 客户端自带的过滤器。有些客户端会按设计丢弃本地流量,而在客户端层面丢弃之上再加一条正确的防火墙规则并不会改变任何结果。修改主机防火墙之前先读客户端配置,因为两道串联的过滤器会产生一种看起来都不像的故障。

分别测试两种状态

测试跑两次:一次在隧道连接时,一次在断开时。每种状态下都确认监听、路由,以及从管理员机器发起一次真实的 SSH 登录。只在隧道升起时才能走通的路径不是本地管理路径,而是巧合。

然后在 SSH 会话保持打开的情况下重启隧道。会话存活说明路由稳定;会话中断说明客户端在重连时重写了路由或过滤器,这一点值得在事故之前而不是事故当中知道。

记录你测试时使用的源地址,因为为 192.168.10.50 写的规则,在管理员笔记本换到另一个子网后会静默失效。

把恢复路径写进 runbook

现在就决定:如果出站 VPN 破坏了本地路径该怎么办。控制台访问、第二个管理地址,或一套有文档的停止客户端流程,都是正当答案,其中任何一种都比在必须登录的那一刻才发现问题要好。如果服务器在远端,本地路径本来就不存在,正确的设计是单独批准的远程访问路径或堡垒机,而不是暴露在公网的端口。

在文档里把两种角色分开:面向近处管理员的本地 SSH,以及面向应用流量的出站隧道。把这些角色写下来之后,将来对任何一方的改动都有明确的检查位置,下一个人也不必重新发现为什么一台带 VPN 的服务器仍然会响应它的 LAN 地址。

相关文章