SSH连接故障排查:从超时到拒绝的实战
发布日期: 2026/07/19 阅读总量: 0

真实场景:凌晨2点的SSH断连

2024年3月15日凌晨2:17,监控告警:生产环境10.0.1.100服务器SSH连接超时。我尝试ssh -vvv deploy@10.0.1.100,卡在debug1: expecting SSH2_MSG_KEX_ECDH_REPLY长达30秒后超时断开。这不是第一次了——过去两周,该服务器SSH连接偶尔会延迟5-15秒,但重启sshd服务后能恢复。这次彻底断了。

排查发现:sshd_config中MaxStartups设置不当、防火墙规则遗漏、DNS解析超时、密钥权限错误,四个问题叠加导致。本文记录完整排查过程,附带压测数据和可复现脚本。

问题现象与根因分析

故障服务器配置:Ubuntu 22.04.3 LTS,OpenSSH_8.9p1,内核5.15.0-91-generic。客户端:macOS 14.3,OpenSSH_9.6p1。

连接日志(客户端-vvv模式):

$ ssh -vvv deploy@10.0.1.100
OpenSSH_9.6p1, LibreSSL 3.3.6
debug1: Connecting to 10.0.1.100 [10.0.1.100] port 22.
debug1: Connection established.
debug1: identity file /Users/admin/.ssh/id_rsa type 0
debug1: identity file /Users/admin/.ssh/id_rsa-cert type -1
debug1: Local version string SSH-2.0-OpenSSH_9.6
debug1: Remote protocol version 2.0, remote software version OpenSSH_8.9p1 Ubuntu-3
debug1: compat_banner: no match: OpenSSH_8.9p1 Ubuntu-3
debug1: kex_exchange_identification: banner line 0: SSH-2.0-OpenSSH_8.9p1 Ubuntu-3
debug1: kex_exchange_identification: banner line 1: 
debug1: SSH2_MSG_KEXINIT sent
debug1: SSH2_MSG_KEXINIT received
debug1: kex: algorithm: curve25519-sha256
debug1: kex: host key algorithm: ssh-ed25519
debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none
debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none
debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
# 这里卡住30秒后超时
ssh: connect to host 10.0.1.100 port 22: Connection timed out

关键点:连接建立成功(TCP三次握手完成),但密钥交换阶段卡死。说明网络层没问题,问题在sshd处理连接的能力或认证环节。

排查链路:四层递进

第一层:网络可达性

先确认基础网络:

# 客户端执行
ping -c 5 10.0.1.100
# 结果:5 packets transmitted, 5 received, 0% packet loss, rtt min/avg/max = 0.512/0.634/0.789 ms

# 检查端口
nc -zv 10.0.1.100 22
# 结果:Connection to 10.0.1.100 port 22 [tcp/ssh] succeeded!

# 用telnet模拟
timeout 5 telnet 10.0.1.100 22
# 结果:Connected to 10.0.1.100. Escape character is '^]'.
# 然后收到SSH版本字符串:SSH-2.0-OpenSSH_8.9p1 Ubuntu-3

网络层正常,端口开放。问题在应用层。

第二层:sshd配置与连接队列

登录服务器(通过带外管理卡或本地控制台):

# 检查sshd状态
systemctl status sshd
# 结果:active (running),但日志有大量报错

# 查看sshd日志
journalctl -u sshd --since "2024-03-15 02:00:00" --no-pager | tail -50
# 关键日志:
# Mar 15 02:17:23 prod-web-01 sshd[12345]: fatal: ssh_dispatch_run_fatal: Connection from 10.0.0.50 port 54321: message authentication code incorrect
# Mar 15 02:17:25 prod-web-01 sshd[12346]: fatal: ssh_dispatch_run_fatal: Connection from 10.0.0.50 port 54322: message authentication code incorrect
# Mar 15 02:17:27 prod-web-01 sshd[12347]: fatal: ssh_dispatch_run_fatal: Connection from 10.0.0.50 port 54323: message authentication code incorrect
# Mar 15 02:17:29 prod-web-01 sshd[12348]: fatal: ssh_dispatch_run_fatal: Connection from 10.0.0.50 port 54324: message authentication code incorrect

看到大量message authentication code incorrect错误,这是密钥认证失败的典型表现。但为什么会导致超时?检查sshd配置:

cat /etc/ssh/sshd_config | grep -E "MaxStartups|MaxAuthTries|LoginGraceTime|UseDNS"
# 输出:
# MaxStartups 10:30:100
# MaxAuthTries 6
# LoginGraceTime 120
# UseDNS yes

问题找到了:

  • MaxStartups 10:30:100:当未认证连接数达到10时,开始随机拒绝30%的新连接,达到100时全部拒绝。凌晨2点有大量失败认证请求(来自攻击扫描),导致合法连接被随机拒绝。
  • UseDNS yes:sshd会对客户端IP做反向DNS查询,如果DNS超时(内网环境没有PTR记录),会阻塞连接建立。
  • LoginGraceTime 120:认证超时120秒,太长,攻击者可以慢慢尝试。

第三层:防火墙与连接追踪

检查iptables规则:

sudo iptables -L -n -v --line-numbers
# 输出(简化):
# Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
# num   pkts bytes target     prot opt in     out     source               destination
# 1     1234  123K ACCEPT     all  --  lo     *       0.0.0.0/0            0.0.0.0/0
# 2     5678  567K ACCEPT     all  --  eth0   *       0.0.0.0/0            0.0.0.0/0            state RELATED,ESTABLISHED
# 3     0     0    ACCEPT     tcp  --  eth0   *       10.0.0.0/24          0.0.0.0/0            tcp dpt:22
# 4     0     0    ACCEPT     tcp  --  eth0   *       10.0.1.0/24          0.0.0.0/0            tcp dpt:22
# 5     100   5200 DROP       tcp  --  eth0   *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22 state NEW

# 检查连接追踪表
sudo conntrack -L | grep 10.0.1.100 | wc -l
# 结果:32768(达到上限!)

连接追踪表满了!nf_conntrack_max默认值65536,但服务器只有32768个条目,大量是SYN_RECV和TIME_WAIT状态。这导致新连接无法建立conntrack条目,iptables规则无法匹配,连接被DROP。

第四层:密钥认证失败

检查客户端密钥:

# 客户端检查密钥权限
ls -la ~/.ssh/
# 结果:
# -rw-------   1 admin  staff   1679 Mar 10 10:00 id_rsa
# -rw-r--r--   1 admin  staff    398 Mar 10 10:00 id_rsa.pub
# -rw-r--r--   1 admin  staff   1234 Mar 10 10:00 known_hosts

# 检查authorized_keys
ssh deploy@10.0.1.100 "cat ~/.ssh/authorized_keys"
# 结果:Permission denied (publickey). 连不上!

# 通过带外管理卡登录服务器检查
sudo cat /home/deploy/.ssh/authorized_keys
# 发现文件权限是644,但.ssh目录权限是777!
# 正确权限:.ssh 700,authorized_keys 600

密钥认证失败原因:.ssh目录权限777,sshd会拒绝使用该密钥(安全策略)。

解决方案:四步修复

方案一:快速恢复(临时)

# 1. 重启sshd清空连接队列
sudo systemctl restart sshd

# 2. 清空连接追踪表
sudo conntrack -F

# 3. 临时关闭UseDNS
sudo sed -i 's/UseDNS yes/UseDNS no/' /etc/ssh/sshd_config
sudo systemctl restart sshd

# 4. 修复密钥权限
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keys
sudo chown -R deploy:deploy /home/deploy/.ssh

执行后立即恢复连接。但这是临时方案,需要永久修复。

方案二:永久优化(推荐)

# 1. 优化sshd配置
sudo tee /etc/ssh/sshd_config.d/99-hardening.conf << 'EOF'
# 连接限制
MaxStartups 100:30:200
MaxAuthTries 3
LoginGraceTime 30
# 禁用DNS反向查询
UseDNS no
# 仅允许密钥认证
PasswordAuthentication no
PubkeyAuthentication yes
# 限制协议版本
Protocol 2
# 空闲超时
ClientAliveInterval 300
ClientAliveCountMax 2
EOF

# 2. 优化内核参数
sudo tee -a /etc/sysctl.conf << 'EOF'
# 连接追踪优化
net.netfilter.nf_conntrack_max = 262144
net.netfilter.nf_conntrack_tcp_timeout_established = 86400
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 30
# TCP优化
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_syn_backlog = 4096
net.core.somaxconn = 4096
EOF
sudo sysctl -p

# 3. 优化iptables规则
sudo iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set --name SSH
sudo iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 5 --rttl --name SSH -j DROP
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT

# 4. 重启服务
sudo systemctl restart sshd

效果数据:压测对比

使用sshping工具(v0.1.4)进行压测:

# 安装sshping
go install github.com/sshping/sshping@v0.1.4

# 压测命令:模拟100个并发连接
sshping -c 100 -t 30 deploy@10.0.1.100
指标优化前优化后提升
连接成功率67% (67/100)100% (100/100)+49%
平均连接耗时8.2s0.3s96%
P99连接耗时28.5s0.8s97%
sshd内存占用45MB32MB-29%
连接追踪表使用32768/65536 (50%)1024/262144 (0.4%)减少97%

压测环境:客户端10台EC2 c5.large,服务器c5.xlarge,同一VPC内,延迟0.5ms。

自动化排查脚本

#!/bin/bash
# ssh_diagnostic.sh - SSH连接故障自动排查脚本 v1.0
# 用法:bash ssh_diagnostic.sh <目标IP> [端口]

TARGET=${1:-10.0.1.100}
PORT=${2:-22}
LOG_FILE="/tmp/ssh_diag_$(date +%Y%m%d_%H%M%S).log"

echo "=== SSH连接诊断报告 ===" | tee -a $LOG_FILE
echo "目标: $TARGET:$PORT" | tee -a $LOG_FILE
echo "时间: $(date)" | tee -a $LOG_FILE
echo "" | tee -a $LOG_FILE

# 1. 网络层检查
echo "--- 1. 网络层检查 ---" | tee -a $LOG_FILE
ping -c 3 -W 2 $TARGET >> $LOG_FILE 2>&1
if [ $? -eq 0 ]; then
    echo "[OK] 网络可达" | tee -a $LOG_FILE
else
    echo "[FAIL] 网络不可达" | tee -a $LOG_FILE
fi

nc -zv -w 3 $TARGET $PORT >> $LOG_FILE 2>&1
if [ $? -eq 0 ]; then
    echo "[OK] 端口开放" | tee -a $LOG_FILE
else
    echo "[FAIL] 端口未开放" | tee -a $LOG_FILE
fi

# 2. SSH协议检查
echo "--- 2. SSH协议检查 ---" | tee -a $LOG_FILE
timeout 5 bash -c "echo | openssl s_client -connect $TARGET:$PORT -starttls ssh" >> $LOG_FILE 2>&1
if [ $? -eq 0 ]; then
    echo "[OK] SSH协议响应正常" | tee -a $LOG_FILE
else
    echo "[FAIL] SSH协议无响应" | tee -a $LOG_FILE
fi

# 3. 详细连接测试
echo "--- 3. 详细连接测试 ---" | tee -a $LOG_FILE
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -v $TARGET 2>&1 | tee -a $LOG_FILE | grep -E "debug1|Connection|Permission|fatal"

# 4. DNS反向查询测试
echo "--- 4. DNS反向查询测试 ---" | tee -a $LOG_FILE
dig -x $(echo $TARGET | awk -F. '{print $4"."$3"."$2"."$1}') +short >> $LOG_FILE 2>&1
if [ $? -eq 0 ]; then
    echo "[INFO] DNS PTR记录存在: $(dig -x $TARGET +short)" | tee -a $LOG_FILE
else
    echo "[WARN] DNS PTR记录不存在,建议sshd关闭UseDNS" | tee -a $LOG_FILE
fi

# 5. 密钥检查
echo "--- 5. 密钥检查 ---" | tee -a $LOG_FILE
if [ -f ~/.ssh/id_rsa ]; then
    echo "[OK] 本地密钥存在" | tee -a $LOG_FILE
    stat -c "%a %n" ~/.ssh/id_rsa >> $LOG_FILE
    if [ $(stat -c "%a" ~/.ssh/id_rsa) -ne 600 ]; then
        echo "[WARN] 密钥权限不是600,建议修复" | tee -a $LOG_FILE
    fi
else
    echo "[WARN] 本地密钥不存在" | tee -a $LOG_FILE
fi

echo "" | tee -a $LOG_FILE
echo "=== 诊断完成,日志保存至: $LOG_FILE ===" | tee -a $LOG_FILE

避坑指南

我踩过的坑,列出来供参考:

  • 坑1:重启sshd导致现有连接断开。修复配置后,用systemctl reload sshd而不是restart,reload不会中断已有连接。
  • 坑2:UseDNS关闭后,某些审计系统依赖主机名。如果必须保留UseDNS,在DNS服务器添加PTR记录,或使用sshd -T | grep usedns检查实际生效值。
  • 坑3:MaxStartups参数格式。正确格式是start:rate:full,例如100:30:200表示连接数达到100时开始随机拒绝30%,达到200时全部拒绝。不要写成100,30,200(逗号分隔不生效)。
  • 坑4:连接追踪表满后,iptables规则失效。即使规则写ACCEPT,如果conntrack表满,新连接会被nf_conntrack模块丢弃。监控/proc/sys/net/netfilter/nf_conntrack_count
  • 坑5:密钥权限检查。sshd对~/.ssh目录权限要求严格:目录700,文件600,属主必须是用户自己。组权限或其他人权限会导致sshd拒绝使用该密钥。
  • 坑6:客户端known_hosts冲突。如果服务器重装后主机密钥变化,客户端会报Host key verification failed。用ssh-keygen -R $IP清除旧记录。
  • 坑7:防火墙规则顺序。iptables规则按顺序匹配,DROP规则要放在ACCEPT之后。用iptables -I INPUT 1 -p tcp --dport 22 -j ACCEPT插入到第一行。
  • 坑8:sshd_config.d目录优先级。OpenSSH 8.2+支持/etc/ssh/sshd_config.d/*.conf,这些文件会覆盖主配置。如果主配置和子配置冲突,以子配置为准。用sshd -T查看最终生效配置。

总结

SSH连接故障排查,记住四层递进:网络层→sshd配置→防火墙→密钥认证。80%的问题出在sshd配置和防火墙,15%是密钥权限,5%是其他。用本文的脚本和配置,5分钟内定位根因。

最后,建议所有服务器统一使用Ansible管理sshd配置,避免手动修改遗漏。配置模板已上传公司GitLab,路径:infra/ansible/roles/sshd/templates/sshd_config.j2