真实场景:凌晨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.2s | 0.3s | 96% |
| P99连接耗时 | 28.5s | 0.8s | 97% |
| sshd内存占用 | 45MB | 32MB | -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。