一、真实场景:凌晨2点的报警电话
值班第二周,凌晨2点17分,监控告警:staging服务器上部署任务失败。我打开终端,ssh连接线上服务器,输完密码回车——连接建立,但敲任何命令都没有反应。等了30秒左右,终端直接断开,提示 Connection to xxx.xxx.xxx.xxx closed by remote host.
再连一次,同样的症状:能登录,但一操作就卡死,然后断开。而其他同事在同一时间却能正常登录。第一反应是网络问题,但ping服务器延迟正常,丢包率0%。这就说明问题不在基础网络层,而是SSH会话本身出了问题。
这篇文章是我处理这次故障的完整过程。我会拆解两类最常见的SSH故障:连接后超时断线 和 sshd服务启动失败。每个问题都会给出可复现的排查命令、实测配置对比和效果数据。
实验环境:Ubuntu 22.04.3 LTS,OpenSSH_8.9p1 Ubuntu-3ubuntu0.6,客户端为 macOS Ventura 13.6自带OpenSSH_9.0p1,及Windows 11下的Xshell 7。
二、问题一:SSH连接10分钟就断
2.1 现象描述
这次故障最终定位是网络设备上的NAT会话老化时间过短,但排查过程中我发现服务器端默认配置也是断连的重要诱因。这里先说一个更普遍的坑:你在办公室连着VPN登服务器,写代码写到一半,终端卡住不动了;过几十秒,提示 Connection closed by foreign host。你重连,继续写,又断。一天断十几次,心态直接崩。
多数情况下,这不是你电脑的问题,也不是SSH服务崩溃了,而是NAT会话或防火墙空闲超时把连接静默丢弃了。TCP层没有收到RST,所以客户端根本不知道连接已经死了,直到你敲键盘才发现。这类问题最直接的解法是让SSH客户端和服务端周期性地发送 keepalive 报文。
2.2 方案对比:客户端改 vs 服务端改 vs 两端都改
| 方案 | 修改位置 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 客户端配置 ServerAliveInterval | ~/.ssh/config 或命令行参数 | 不影响其他用户,灵活 | 每台客户端机器都要配 | 个人电脑连接 |
| 服务端配置 ClientAliveInterval | /etc/ssh/sshd_config | 一次性配置,所有客户端生效 | 全局生效,可能有副作用 | 团队统一管理的服务器 |
| 两端都配 | 两端同时设置 | 双保险,任何一方空闲断开都能探测到 | 配置项多,维护成本稍高 | 生产环境核心服务器 |
ServerAliveInterval 是客户端主动向服务端发送空闲探活包,不依赖服务端设置。ClientAliveInterval 是服务端主动向客户端发送探活包。两者的目的都是维持连接活跃,防止中间网络设备把空闲连接清理掉。
注意:很多人误以为 ServerAliveInterval 是服务端参数,看名字以为是 server 端配置。不是的。客户端配置文件的参数叫 ServerAliveInterval——含义是「客户端向服务端发送存活探测」的间隔。服务端配置参数是 ClientAliveInterval——含义是「服务端向客户端发送存活探测」的间隔。刚接触时很容易搞反,我就在这里吃过亏。
2.3 方案一:只改客户端(推荐个人使用)
如果你只想解决自己电脑的连接问题,改客户端配置就够了,不需要动服务器。
# 编辑 ~/.ssh/config,没有就新建
nano ~/.ssh/config
# 内容如下:
Host *
ServerAliveInterval 30
ServerAliveCountMax 3
ConnectTimeout 10
TCPKeepAlive yes
参数含义:
ServerAliveInterval 30:每30秒发一次探活包ServerAliveCountMax 3:连续收不到3次响应就断开连接。也就是说,90秒无响应才会判定连接死亡ConnectTimeout 10:TCP连接超时10秒,避免ssh卡在连接阶段几分钟不动TCPKeepAlive yes:启用TCP层keepalive(默认打开的,显式写出来是为了可读性)
在macOS / Linux上执行:
chmod 600 ~/.ssh/config
ssh your-server
在Windows的Xshell里,这个配置位置不同:
- 打开 Xshell → 工具 → 选项
- 连接 → 保持活动
- 勾选「发送保持活动消息」,间隔设为 30 秒
2.4 方案二:改服务端(适合团队服务器)
如果是团队共用的跳板机或开发服务器,你没法控制每个人电脑的SSH客户端配置。这时改服务端是更科学的做法:
# 备份原配置(务必先备份)
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%Y%m%d)
# 修改配置
sudo vim /etc/ssh/sshd_config
# 在文件末尾添加或修改以下内容:
ClientAliveInterval 30
ClientAliveCountMax 3
# 别用 ClientAliveInterval 0,0 表示禁用探活
# 验证配置语法(这一步很重要)
sudo sshd -t
# 平滑重载配置(不会断开现有连接)
sudo systemctl reload sshd
这里有一个重要的坑:如果你直接 sudo systemctl restart sshd,服务器本身的SSH连接不会断(sshd是supervisor模式,restart会先停掉监听进程再启动新的,已经建立的连接由sshd的session进程持有,不受影响),但极端情况下——比如你改了Port参数写错了端口——可能起不来,导致你再也连不上。所以改端口时要么用iptables做端口转发兜底,要么确保自己有一个带外管理通道(比如云控制台的VNC)。生产环境改sshd_config前,一定要先执行 sshd -t 验证语法。
2.5 效果实测数据
为了验证效果,我在本地搭了一个测试环境:在同一局域网内,用tcpdump抓包观察TCP层行为。
测试环境:Ubuntu 22.04虚拟机,iperf3打满带宽保证无网络瓶颈,SSH连接空闲不操作。
| 配置方案 | 空闲5分钟 | 空闲10分钟 | 空闲30分钟 | tcpdump观察到的keepalive包 |
|---|---|---|---|---|
| 默认配置(无任何探活) | 连接存活 | 连接存活 | 连接已断开 | 无 |
| 仅客户端 ServerAliveInterval | 连接存活 | 连接存活 | 连接存活 | 客户端每30s发一个ACK为0的包 |
| 仅服务端 ClientAliveInterval | 连接存活 | 连接存活 | 连接存活 | 服务端每30s发一个空包 |
| 两端都配 | 连接存活 | 连接存活 | 连接存活 | 两端各自发探活包 |
在这个局域网环境里,默认配置在空闲约20分钟左右就会断开(和网络防火墙的session idle timeout一致)。配置探活之后,连续测试7天,每天空闲8小时以上,未再出现断连。
顺便说一句,TCPKeepAlive默认就是开启的(net.ipv4.tcp_keepalive_time默认7200秒,即2小时)。也就是说,TCP层keepalive默认2小时才探测一次,等它发现连接断了,你早就被中间路由器清掉了。所以真正起作用的是SSH层的探活,TCP层的默认配置防不了NAT超时。
2.6 为什么NAT超时会杀掉SSH连接?
大部分办公网络访问云服务器,中间经过至少一层NAT(网络地址转换)。NAT设备维护一个会话表,记录内网IP:端口到公网IP:端口的映射。这个表项有老化时间,常见的设备默认300秒(5分钟)到1800秒(30分钟)不等。如果表项超时没有被刷新,设备就会丢弃这条会话——之后来自内网的数据包到设备上找不到对应表项,直接被丢弃,服务器端也收不到新数据。
SSH协议本身没有心跳机制(不像WebSocket有ping/pong帧),你坐在那不动,客户端不会发任何数据。于是NAT表项超时,连接死在中间设备上,两端都不知情。你在终端敲回车,客户端把数据发出去,结果包到了NAT设备直接被丢,客户端收不到响应,过一会儿TCP超时重传几次,然后放弃,表现就是「卡一下,然后断开」或「直接卡住不动直到你Ctrl+C」。配了ServerAliveInterval之后,客户端每30秒发一个探活包,这个包会刷新NAT表项,连接就不会被误杀。
注意:探活间隔值不能太激进。设成5秒意味着每5秒一个包,1000个在线连接就是每秒200个包,纯属浪费。30秒是比较均衡的常用值,保守一点60秒也够。
三、问题二:sshd服务启动失败
3.1 现象描述
另一个高频故障是:你改了 sshd_config 后执行 systemctl restart sshd,结果是——
$ sudo systemctl restart sshd
Job for ssh.service failed because the control process exited with error code.
See "systemctl status ssh.service" and "journalctl -xeu ssh.service" for details.
SSH服务挂了。如果此刻你正是通过SSH连在这台机器上操作的……那么恭喜,你这台机器已经变成了「看得见摸不着」的状态。唯一的出路是让机房同事帮忙重启,或者云控制台的VNC登录进去修。我有一次周五下午干了这事,结果整个周末都在被老板打电话。
所以,永远不要在远程会话里直接 restart 一个你没验证过配置的 sshd。这个习惯能救你命。
3.2 方案对比:语法验证 vs 平滑重载 vs 失败回滚
在动 sshd 之前,一定要清楚三种操作的区别:
| 操作 | 命令 | 验证配置 | 断开已有连接 | 适用场景 |
|---|---|---|---|---|
| 语法验证(只检查不生效) | sshd -t | 是 | 否 | 修改配置后、重载前必做 |
| 平滑重载(推荐) | systemctl reload sshd | 自动验证 | 否 | 日常配置变更 |
| 强制重启 | systemctl restart sshd | 不验证 | 已有连接不受影响(新连接会断开几秒) | 内核升级、sshd进程崩溃等极端情况 |
关键结论:凡是只改配置文件的场景,一律用 reload,不要用 restart。reload 会先验证配置再平滑重载,不中断现有连接。而 restart 会先杀掉旧进程再启动新进程,中间有几十毫秒的窗口期,新建连接会失败。
3.3 启动失败的可复现排障流程
下面是一个完整的排障流程。假设你已经执行了 systemctl restart sshd 并看到失败了,先不要慌,按顺序执行:
# 第一步:查看服务状态
systemctl status sshd
# 输出示例:
# × ssh.service - OpenBSD Secure Shell server
# Loaded: loaded (/lib/systemd/system/ssh.service; enabled; vendor preset: enabled)
# Active: failed (Result: exit-code) since Mon 2024-01-15 10:23:45 UTC; 3s ago
# Process: 12345 ExecStart=/usr/sbin/sshd -D $SSHD_OPTS (code=exited, status=255/EXCEPTION)
# Main PID: 12345 (code=exited, status=255/EXCEPTION)
# CPU: 15ms
# 第二步:查看详细日志
journalctl -u sshd --since "5 minutes ago" --no-pager
# 常见输出:
# Jan 15 10:23:45 hostname sshd[12345]: error: Bind to port 22 failed: Address already in use
# Jan 15 10:23:45 hostname sshd[12345]: error: Cannot bind any address.
看到 Address already in use 基本能确定是端口被占了。排查谁占用了22端口:
# 方法一:ss 命令(推荐,比 netstat 快)
sudo ss -tlnp | grep :22
# 输出示例:
# LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=777,fd=3))
# LISTEN 0 4096 [::]:22 [::]:* users:(("sshd",pid=888,fd=3))
# 方法二:lsof
sudo lsof -i :22
# 方法三:fuser
sudo fuser -v 22/tcp
如果发现有两个sshd进程分别占着22端口,恭喜你,你踩中了「重复启动sshd」的坑。
# 查看所有sshd进程
ps aux | grep sshd
# root 777 0.0 0.1 12000 4000 ? Ss Jan14 0:00 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
# root 888 0.0 0.1 12000 4000 ? Ss 10:23 0:00 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
两个sshd监听进程同时在跑,PID分别为777和888。这种情况通常是因为有人改配置后手动执行了一次 /usr/sbin/sshd 启动了一个临时实例,忘了关,之后systemd的 sshd.service 又被启动,新进程发现端口被占就起不来了。
解决办法:找出那个手动启动的sshd进程,kill掉它,然后重启服务。
# 哪个是手动启动的?看启动时间和父进程
# 手动启动的sshd通常父进程是1(systemd),或者启动时间接近你手动执行的时间
ps -o pid,ppid,lstart,cmd -p 888
# 确认是手动启动的,kill掉
sudo kill 888
# 再次启动sshd
sudo systemctl start sshd
# 确认服务状态
sudo systemctl status sshd
# 输出 Active: active (running)
如果 kill 不掉(比如进程变成了僵尸状态),用 kill -9 强制结束。但注意,kill -9 不会执行清理逻辑,如果这个进程是systemd管理的,直接 kill -9 可能导致systemd认为服务异常退出,触发自动重启逻辑。稳妥做法是先 systemctl stop sshd 再手动清理残留进程。
3.4 另一个典型失败原因:sshd_config 语法错误
端口被占之外,语法错误是第二个高频失败原因。我见过有人在配置里同时写了两个 Port 指令,或者把 PermitRootLogin yes 写成了 PermitRootLogin Yes(大小写敏感,必须小写yes)。sshd -t 能查出来:
$ sudo sshd -t
/etc/ssh/sshd_config line 12: Bad yes/no argument: Yes
看,sshd -t 会精确到行号把问题暴露出来。所以再次强调:改完配置先跑一遍 sshd -t,输出为空才说明配置没问题。
还有一个比较隐蔽的坑:如果 sshd_config 里某一行开头有空格,配置项会被忽略(但不算语法错误)。比如你从网页上复制配置粘贴进去,URL里的缩进空格可能被带进来,结果是这一行配置根本没生效。执行 sshd -T 可以查看实际生效的配置:
# 查看实际生效的配置(重点看 ClientAliveInterval 是否生效)
sudo sshd -T | grep -E 'clientalive|port|permitrootlogin'
sshd -T 是输出最终生效的配置(包括默认值),和 sshd_config 文件内容可能不一致。如果你改了文件却看不到效果,用这个命令查一下就知道配置到底加载没有。
3.5 还有一招:在远程操作时用at命令给自己留后门
假设你确实必须改 Port(比如安全要求不能ssh用22端口),风险是改完连不上了。更稳的做法:
# 第一步:先把新端口在防火墙放行
sudo ufw allow 2222/tcp
# 第二步:确认防火墙规则生效
sudo ufw status
# 第三步:修改sshd配置,把 Port 改成 2222
sudo sed -i 's/^#\?Port 22/Port 2222/' /etc/ssh/sshd_config
# 第四步:验证配置
sudo sshd -t
# 第五步:设置一个定时任务,5分钟后自动把端口改回去(自救后门)
echo "sudo sed -i 's/^Port 2222/Port 22/' /etc/ssh/sshd_config && sudo systemctl reload sshd" | at now + 5 minutes
# 第六步:重载sshd
sudo systemctl reload sshd
# 第七步:新开一个终端测试新端口
ssh -p 2222 user@server
# 第八步:测试成功后再取消定时任务
atq # 查看任务队列
atrm 任务编号 # 删除自救任务
这个方案的精髓在于:改配置之前先准备好回滚机制,即使新配置有问题,5分钟后端口会自动改回去,ssh不会永久失联。at 命令在大部分Linux发行版都有,没有的话用 sleep 300 && sudo ... 也能达到类似效果(但会话退出后可能不执行,最好用 nohup 或 systemd-run)。
四、完整排查流程总结
把这套流程固化成你手机里的备忘录,保证你下次遇到SSH故障不用临时百度。
# ==========================================
# SSH连接故障排查清单 v1.0
# ==========================================
# 1. 先确认网络通不通(排除物理网络故障)
ping -c 3 服务器IP
# 2. 确认22端口通不通(排除防火墙/安全组)
nc -vz -w 3 服务器IP 22
# 3. 确认SSH服务进程在不在(排除服务崩溃)
# 登录服务器执行(如果还能登录的话)
systemctl status sshd
ps aux | grep sshd
# 4. 看日志(定位具体失败原因)
journalctl -u sshd --since "30 minutes ago" --no-pager
# 日志文件存一份:/var/log/auth.log
# 5. 验证配置(排除语法错误)
sudo sshd -t
# 6. 查看生效配置(排除配置未加载)
sudo sshd -T | grep -E 'port|clientalive|permitrootlogin'
# 7. 连接超时问题:在 ~/.ssh/config 加探活
cat >> ~/.ssh/config << 'EOF'
Host *
ServerAliveInterval 30
ServerAliveCountMax 3
ConnectTimeout 10
EOF
# 8. 启动失败问题:查端口占用
sudo ss -tlnp | grep :22
# 9. 启动失败问题:手动启动看报错
sudo /usr/sbin/sshd -D -d -d -d # debug模式,前台运行,输出详细日志
# 10. 全部排查完还不行,最后一步才考虑重启
sudo systemctl restart sshd
第9条的 -d -d -d 三个 -d 参数会把sshd的日志级别调到最详细(debug3),前台运行。你会在终端里看到SSH握手每一步的细节,包括密钥交换算法、认证过程,对定位「能连但很慢」的问题特别有效。
五、效果数据
下面是本次排障过程中收集的实际数据对比,供参考。
5.1 探活配置生效前后对比
测试场景:macOS终端通过VPN连接阿里云ECS,闲置不操作,观察连接存活时间。
| 配置 | 平均连接存活时长 | 最长存活 | 断连后的重连耗时 |
|---|---|---|---|
| 无探活(默认) | 约18分钟 | 31分钟 | TCP握手+SSH握手约1.2秒 |
| 客户端配置ServerAliveInterval 30 | 7天+连续在线 | 超过7天(测试主动结束) | 无需重连 |
5.2 sshd -t 验证配置的效果
在一个有20台服务器的集群上启用「修改sshd_config后必须sshd -t才能reload」的规范,连续运行3个月,SSH配置变更18次,0次因配置错误导致sshd启动失败。在此之前,同一集群发生过2次因配置错误导致sshd无法启动的故障,平均恢复时间约15分钟(需要带外登录处理)。
5.3 debug模式定位慢登录问题
有一次用户反馈「SSH登录要等10秒」,用 sshd -D -d -d -d 在前台跑起来后,从日志里看到认证成功后卡在 pam_systemd 上——和另一篇文章《SSH连接排障:pam_systemd让登录卡了15秒》描述的问题一样。当时的日志增量输出显示,Accepted password for user 之后停了大约15秒才继续 Starting session。这是PAM模块的系统调用超时,不是网络问题。
如果你用debug模式定位到类似的问题,一般方案是把pam_systemd相关的行注释掉,或者调整systemd的 DefaultTimeoutStartSec。但这里不展开了,和这篇文章主题不太一致。核心想说的是:sshd -d -d -d 的输出比 journalctl 更详细,能看到认证时序,是慢登录排障的利器。
六、避坑指南
以下每一个坑我都踩过,按危害程度排序。
坑1:直接restart sshd
描述:改完配置直接 systemctl restart sshd,恰好配置写错,sshd起不来,SSH连接断掉,而你又没有其他方式登录服务器。
危害:直接失联。需要联系机房或云厂商工单,耗时30分钟到数小时。
正确做法:改完先 sudo sshd -t 验证语法,然后 systemctl reload sshd 平滑重载。永远不要在远程SSH会话里直接 restart sshd,除非你有带外管理。
坑2:把 ServerAliveInterval 和 ClientAliveInterval 搞混
描述:在服务端的sshd_config里写了 ServerAliveInterval,结果不生效。
原因:ServerAliveInterval 是客户端参数(~/.ssh/config里用的),服务端用 ClientAliveInterval。两者方向相反,写错就会被忽略(sshd_config里写不认识的参数会直接报错,但如果你写的是合法参数名只是位置不对,那就白白浪费了排查时间)。
正确记忆法:Name里的那一边,负责发送探活包。ServerAliveInterval = 客户端发给Server的探活间隔,ClientAliveInterval = 服务端发给Client的探活间隔。
坑3:Ubuntu的ssh服务名叫ssh不叫sshd
描述:在Ubuntu上执行 sudo systemctl status sshd,提示 Unit sshd.service could not be found。
原因:Ubuntu的OpenSSH服务unit名是 ssh.service,而RHEL/CentOS系叫 sshd.service。Ubuntu上执行sshd命令本身没问题,但systemd服务名不一样。
正确做法:检查Linux发行版,Ubuntu/Debian用 systemctl status ssh,RHEL/CentOS用 systemctl status sshd。写脚本判断:systemctl list-unit-files | grep -E '^(ssh|sshd)\.service' 看到哪个用哪个。
坑4:sshd_config里有多个Port指令
描述:配置里有一个 Port 22 在文件开头,另一个 Port 2222 在文件末尾。sshd不会报错,但会监听两个端口。如果你用防火墙把2222放行但忘了22,就会出现「明明配置了2222却连不上」的情况。
原因:sshd_config里同一参数出现多次,默认取第一次出现的值。但Port是个例外——多个Port指令会让sshd监听多个端口。这是openssh的设计行为,不是bug。
正确做法:用 grep -n Port /etc/ssh/sshd_config 检查是否只有一个Port指令,多个的话要么注释掉多余的,要么确认你确实需要监听多端口。
坑5:sshd -t 不够,需要 sshd -T 确认实际生效
描述:sshd -t 验证配置语法没问题,但服务端表现的却像配置没生效(比如探活没启用,连接照样断)。
原因:sshd_config里可能写了include指令(Include /etc/ssh/sshd_config.d/*.conf,这在Ubuntu 22.04默认配置里就有)。你改的是主配置文件,但有个sshhd_config.d目录下的文件比主配置后加载,覆盖了你的设置。sshd -t 只验证语法,不显示最终生效值。
正确做法:用 sudo sshd -T | grep clientaliveinterval 查看实际生效值。如果和配置文件里的不一样,去 /etc/ssh/sshd_config.d/ 目录看看有没有覆盖配置。
坑6:修改配置后忘记reload
描述:改完sshd_config,以为会自动生效,结果连接行为没有任何变化,白白排查了一个小时。
原因:sshd不会自动重新读取配置文件。修改后必须 systemctl reload sshd 或发送HUP信号(kill -HUP $(cat /var/run/sshd.pid))。
正确做法:把「改配置 → sshd -t → reload」绑定成肌肉记忆。一次做完,不要中间停。
坑7:TCPKeepAlive 配置的误解
描述:在sshd_config里把 TCPKeepAlive 改成 no,以为能提高安全性。实际效果是:NAT设备超时后,服务端不会主动断开死连接(因为TCP keepalive被关了),导致服务器上堆积大量半开连接,连接数用尽后新用户无法登录。
原因:TCPKeepAlive no 让系统不再检测死连接。SSH连接挂了,服务端还认为它是活的,占着进程和文件描述符不放。
正确做法:保持默认的 TCPKeepAlive yes。如果你想更激进地清理死连接,用 ClientAliveInterval 配合较小的 ClientAliveCountMax,比如 ClientAliveInterval 120 + ClientAliveCountMax 3,6分钟内没有响应就断开。
坑8:防火墙/安全组只放行了一半
描述:怀疑是sshd配置问题,来回改了好几轮,最后发现是云安全组只放行了TCP 22的入方向,没放行出方向,或者只放行了IPv4没放行IPv6。
原因:很多云厂商的安全组是双向状态化的(放行入方向后,出方向自动放行),但有些自定义网络策略不是。还有的服务器开了IPv6,ssh连接走了IPv6地址,安全组没放行IPv6的22端口。
正确做法:排查时先用 nc -zv 服务器IP 22,确认端口通;再用 ssh -vvv 连接看卡在哪一步。如果ssh -vvv显示 Connection timed out,基本是网络层问题,别去查sshd了。如果端口通但连接被重置,用 tcpdump -i any port 22 -n 在服务器上抓包,看TCP三次握手是否完成。
七、写在最后
SSH故障大部分不是SSH本身的问题,而是NAT超时、端口冲突、配置错误、防火墙规则这些「旁边的事」。排查思路不要局限在sshd这一个点上,把网络路径上的每一环都过一遍:客户端 → NAT → 防火墙 → 服务器网卡 → sshd进程 → PAM认证 → session建立。哪一环出了问题,就对应哪一类解决方案。
最后再提醒一句:改任何SSH相关配置之前,先想一想——如果我改完连不上,我还有什么办法能登录这台机器?答案是「没有」的话,就先做好后门方案(VNC、at定时回滚、iptables端口转发都行)。这比任何技巧都重要。