“Connection reset by xxx.xxx.xxx.xxx port 22,fatal: Could not read from remote repository.”——这句话我这两年已经看过太多次了。凡是长期用 Git 管理代码、经常要往远端仓库推送拉取的人,早晚都会撞上这个报错。第一次遇到时我以为是仓库地址写错了,反复检查 URL 好几遍,结果问题根本不在这里。后来陆陆续续帮同事排查过几个同类问题,也踩过各种奇奇怪怪的坑,才把这类“SSH 连不上仓库”的问题彻底摸清。
这个报错的核心含义其实很明确:你本机的 Git 客户端尝试通过 SSH 协议连接远程服务器的 22 端口,但连接在握手阶段就被远端直接重置了。也就是说,数据包可能已经发到了对方机器,或者中途某个网络设备把它拦下并返还了 RST 标志,总之 SSH 隧道根本没建起来。这篇内容里我会拆解报错背后的网络机制、快速排查顺序、常见场景的应对方法,以及我真实排错过程中的完整链路,希望能帮你在几分钟内定位问题,而不是病急乱投医地去重新生成密钥。
1. 先别慌:读懂“Connection reset”背后的网络逻辑
1.1 TCP 握手和“重置”到底是什么关系
要理解这个报错,得先回到最底层的网络模型。Git 通过 SSH 连接远端仓库时,本质上是在做一次 TCP 连接,并在这个连接上完成 SSH 协议协商。
TCP 连接建立时有一个经典的三次握手过程:
- 你的机器向服务器发送 SYN 包,表示“我想和你建立连接”。
- 服务器收到后回复 SYN+ACK 包,表示“我收到了,我也同意”。
- 你的机器再回复 ACK 包,双方进入已连接状态。
而“Connection reset”意味着握手过程中某个环节收到的是 RST 包。RST 是 TCP 协议里的重置标志,收到它的一方必须立刻终止这个连接。和“超时”不同,RST 不是“等不到响应”,而是明确收到了“别再继续了”的信号——这和敲门没人回应完全是两码事。
打个比方:你把简历递给公司前台,前台直接把简历揉成一团扔回到你脚边。这就是 RST。如果前台压根没开门,你站在门口等了一个小时毫无回应,那叫“timeout”。如果前台开门告诉你“我们这栋楼不允许投递简历”,那是“refused”。三种失败模式的语义完全不同,排查方向也大相径庭。
1.2 SSH 连接在 22 端口上做了什么
SSH 协议是建立在 TCP 之上的。Git 在使用 SSH 传输时,会从本机发起一次到目标主机 22 端口的 TCP 连接,连接建立成功后,双方交换 SSH 协议版本字符串,然后进入密钥交换、认证等流程。
这里有一个容易被忽略的点:Git 报错里的“Connection reset”和“Could not read from remote repository”其实是两层消息。第一句是底层网络层的失败原因,第二句是 Git 基于这个根本原因给出的业务提示。Git 本身并不关心底层是 TCP 重置还是超时,它只知道“我没能从远端仓库读到任何数据”,于是生成 fatal。所以只看最后一句 fatal 是没办法定位问题的,必须抓住第一句里的网络层信息。
常见的几种失败信息形态和它们的指向差异如下:
| 报错特征 | 可能的含义 | 优先排查方向 |
|---|---|---|
| Connection reset by ... port 22 | 连接被对方或链路设备强制中断 | 防火墙、安全组、服务器负载保护 |
| Connection timed out | 数据包有去无回 | 网络不通、目标地址不可达、防火墙静默丢弃 |
| Connection refused | 端口上没有服务监听 | SSH 服务未启动、端口不对、监听地址错误 |
| Permission denied (publickey) | TCP 连接成功,但密钥认证失败 | 公钥没配置、私钥不对、认证方式受限 |
| Too many authentication failures | 客户端提交了太多不匹配的密钥 | 多密钥配置混乱、ssh-agent 没清理 |
了解这些差异之后,你可以直接在第一步就把排查方向框定,不需要绕圈子。
2. 最不该做的事:拿到报错就重新生成 SSH 密钥
2.1 一个会浪费大量时间的思维误区
遇到“Could not read from remote repository”时,很多人的第一反应是“是不是我密钥过期了?重新生成一个吧”。我一开始也是这么干的,白白浪费了一个下午。
但仔细想一下:如果真的是密钥认证失败,报错通常会显示 “Permission denied (publickey)”,而不是 “Connection reset”。Connection reset 说明你的 TCP 连接根本没有完成,SSH 协议协商还没走到密钥认证那一步。换句话说,你就算换一把全新的、绝对正确的密钥,它也没有机会被检查。
所以第一步不是打开终端去生成新密钥,而是先确认网络链路本身是否可用。这就像你要打电话投诉某个公司,但号码拨出去直接被对方系统挂断,这时候再怎么重新准备投诉稿子都没用,你得先确认电话能不能打通。
2.2 正确的快速排查顺序
我整理了一条固定排查链路,每次遇到同类问题都沿着它走:
- 确认远端地址和端口没写错。检查 git remote -v 输出的 URL,以及是不是所有端口都写成了 22。
- 测试 TCP 连通性。使用 nc 或 telnet 连接目标主机的 22 端口,观察是否立即收到连接关闭。
- 测试 SSH 协议层连通性。用 ssh -T 连接目标主机,看有没有输出 SSH 版本字符。
- 查看 Git 实际使用的 SSH 命令。如果本机有多个 SSH 客户端或者配置了别名,确认 Git 调用的到底是哪个。
- 结合网络环境判断。是在公司网络、校园网络还是自己的家庭宽带?不同环境下 22 端口的可达性可能完全不同。
这里专门说一下第二步和第三步的做法。在命令行执行:
nc -vz 远端主机IP 22或者更宽松一点:
timeout 5 bash -c 'echo > /dev/tcp/远端主机IP/22' && echo "port open"如果端口是通的,输出会显示连接成功;如果立即返回“Connection reset”或者“Connection refused”,那问题大概率出在网络上。如果端口是通的但走 SSH 协议时仍然重置,那就是 SSH 层面的配置问题。
ssh -T -p 22 git@远端主机IP正常情况下会看到类似 “Hi xxx! You've successfully authenticated” 或者至少进入 SSH 版本协商阶段。如果这里直接报 “Connection reset”,基本可以确认是链路上某个环节把 SSH 流量重置了。
2.3 网络链路上的三种拦截位置
22 端口被重置,通常发生在以下三个位置:
第一个是本地电脑的防火墙。一些安全软件、系统防火墙对出站连接做了限制,或者把 22 端口识别成高风险端口。Windows 上常见,Mac 下极少见,但在企业环境中很普遍。
第二个是内部网络出口设备。公司网络、学校网络的出口路由器或防火墙,可能出于安全策略禁止内网主机直接对外建立 22 端口连接。这种限制通常对所有人生效,换电脑、换密钥都没用。
第三个是远端服务器所在的机房或云平台安全组。云服务商的安全组规则、机房防火墙、甚至服务器本地的 iptables/nftables 规则,都可能主动向不匹配来源 IP 的 SSH 连接发送 RST。
怎么区分这三种位置?很简单:换网络环境测试。在同一台机器上用手机热点连一次,如果热点下正常、公司网络下报错,那就是公司网络出口的问题。如果所有网络环境都报错,再看是不是只有这一个远端主机有问题。如果只有这一个主机有问题,那大概率是远端防火墙或安全组的问题。
3. 五大高频场景逐个击破:从公司网络到服务器端限流
3.1 公司或校园网络封锁 22 端口
这是我在实际工作中遇到最多的一种场景,尤其是在接入企业 Wi-Fi 或使用固定办公网络时。IT 部门往往只开放 80、443 等常见端口,22 出站会被策略性丢弃或重置。
你可能会觉得奇怪:HTTPS 能正常访问网页,为什么 Git 连不上?因为 Git 默认走的是 SSH 协议 22 端口,HTTPS 走 443 端口,两者是完全独立的 TCP 连接。网站能打开不代表 22 端口可达。
这种情况下解决方案很多,不需要跟网络策略硬刚:
- 改走 HTTPS 克隆。这是最简单可靠的办法。把远程地址里的 git@...: 换成 https://...,用账号密码或 token 认证。很多托管平台对 HTTPS 克隆支持得非常好,传输速度也不差。
- 使用 HTTP/HTTPS 代理。如果你的公司网络只允许 80/443 出站,配置 SSHD 通过代理连接是可行的。关键是要用支持 CONNECT 方法的 HTTP 代理。
- 向 IT 申请开放 22 端口。如果你确实需要 SSH 方式连接仓库,可以走正规流程申请例外。
我个人在这个场景下的推荐顺序是:先用 HTTPS 保证工作不中断,然后走流程申请 22 端口放开。不要为了图省事私自搭东西绕开公司防火墙,合规和效率要同时保住。
3.2 本机 SSH 公钥权限过宽导致的奇怪失败
有时候 TCP 连接是通的,SSH 版本协商也通过了,但服务端出于安全策略,认为你的密钥文件权限过宽,直接拒收。
OpenSSH 服务端有一个配置项严格检查用户目录和密钥文件的权限:如果 ~/.ssh/id_rsa 或者 ~/.ssh 目录的权限是 777、755 这类“世界可读”的状态,服务端会直接忽略这把密钥,相当于你什么都没配。
这种场景下报错偶尔会显示为 “Connection reset”,因为服务端在传递公钥之前就把连接断掉了。排查方法很简单:
ls -la ~/.ssh/ chmod 700 ~/.ssh chmod 600 ~/.ssh/id_rsa ~/.ssh/id_ed25519把目录设为 700,私钥文件设为 600。修改完再试一次连接,大概率就恢复了。这个问题在 Windows 上更常见,尤其是从老版本迁移过用户目录时,权限继承非常容易出错。
3.3 多个 SSH 密钥同时存在引发的“Too many authentication failures”
这个其实和 Connection reset 不一定同名报错,但经常混在一起出现。当你本机的 ssh-agent 里同时塞了十几把密钥,每次连接时客户端会按照顺序挨个提交,服务端一旦发现失败次数超过上限,就直接断开连接。
在 Git 的报错里,这种情况可能表现为:
Received disconnect from 远端主机IP port 22:2: Too many authentication failures fatal: Could not read from remote repository.这个场景最容易发生在开发者维护多平台账号的时候:同一台电脑上既有公司的 Git 仓库密钥,又有个人项目的密钥,还有各种历史遗留密钥。ssh-agent 把所有密钥都加载进去了,连接远端仓库时客户端不指定使用哪一把,就挨个试。
解决方法是给每个远端主机单独指定私钥,并强制只使用这一把。在 ~/.ssh/config 中添加:
Host 别名 HostName 远端主机IP User git Port 22 IdentityFile ~/.ssh/id_ed25519 IdentitiesOnly yes关键在这一行 IdentitiesOnly yes。它的作用是告诉 SSH 客户端:不要管 ssh-agent 里还有什么其他密钥,只使用我指定的这一把。否则即使你写对了 IdentityFile,客户端可能还是会先去查 agent 里的其他密钥,然后触发认证次数上限。
配置完之后执行:
ssh -T 别名确认能正常认证,再把 Git 远程地址里的主机名改成这个别名。
3.4 远端服务器修改过 SSH 端口,但 Git 配置还停留在 22
有些团队为了安全会故意把 SSH 监听端口从 22 改成其他高位端口,比如 2222、8022。如果你的远端配置变了,但本机 Git 的远程地址里没有带端口号,Git 会默认走 22,结果自然是被重置。
在 Git 命令里为自定义端口指定远程地址,格式是:
git remote set-url origin ssh://git@远端主机IP:2222/user/repo.git注意,如果你之前用的是 scp 语法(git@host:path/to/repo.git),它默认使用 22 端口且无法直接在 URL 中指定端口。要带端口就必须改成 ssh:// 的完整格式。修改完之后记得验证一下:
git remote -v git fetch --all我在实际排错里遇到过好几起“明明服务器端口已经改了,Git 配置也改了,但依然报错”的案例,最后发现是 ~/.ssh/config 里给这个主机名配置了一个旧端口,压过了 URL 里的端口设置。所以检查的时候,必须同时看两个地方:Git URL 和 ssh config。
3.5 服务器端防爆破工具或负载保护触发的重置
这个场景比较隐蔽,但遇到一次就知道疼。远端服务器如果装了 Fail2ban 之类的防爆破工具,在检测到短时间内大量失败 SSH 登录请求后,会临时封禁来源 IP。当你的 IP 被临时封禁时,服务器对后续所有来自该 IP 的连接请求直接发送 RST,表现就是 “Connection reset by ... port 22”。
还有一种常见情况:服务器上 22 端口的连接数达到上限,或者系统负载过高,导致 sshd 没有能力继续接受新连接。服务端的 TCP 栈可能会拒绝新连接。这种时候远端运维日志里会有线索,但普通用户基本看不到,只能尝试换个时段再连,或者联系管理员确认有没有封禁操作。
| 场景 | 核心特征 | 解决办法 |
|---|---|---|
| 本地/公司网络拦截 | 换网络环境一切正常 | 改用 HTTPS、申请开放端口 |
| 密钥权限过宽 | 认证时被服务端忽略 | 重置 ~/.ssh 目录和文件权限 |
| 多密钥认证过多 | 报错 Authentication failures | ssh config 指定 IdentityFile + IdentitiesOnly |
| 端口不匹配 | 服务器监听非 22 端口 | 改 git URL 和 ssh config 的 Port |
| 服务端封禁/限流 | 其他来源 IP 正常 | 联系运维确认封禁规则,换网络源 |
4. 深入下半场:把 SSH 连接的调试过程完整打开
4.1 使用 verbose 模式看清每个阶段
如果你已经确认网络链路没问题,网络环境也正常,但报错还是出现,那就需要把 SSH 连接的整个协商过程打开来看。
SSH 客户端支持 -v、-vv、-vvv 三个级别的调试输出。级别越高,打印的信息越详细。我通常直接用 -vvv,一次性把所有信息抓到。
ssh -vvv -p 22 git@远端主机IP输出会像流水账一样告诉你每一步发生了什么:TCP 连接是否建立、SSH 版本字符串是否匹配、支持的加密算法有哪些、密钥交换进行到哪一步、尝试了哪些认证方法。通过观察输出的断点位置,就能精准定位:
- 如果输出停在 “Connection established” 之后立刻显示 “Connection reset”,说明连接在 TCP 层面被掐断,大概率是中间设备或服务端防火墙问题。
- 如果输出进入 “SSH2_MSG_KEXINIT received” 之后才断,说明不是端口封锁问题,而是加密算法不匹配或中间设备深度包检测拦截。
- 如果输出显示 “Authentications that can continue: publickey” 但随后没有拿到认证结果,问题出在密钥配置上。
Git 本身也支持透传 SSH 调试参数,不需要手动去跑 ssh 命令。设置环境变量:
GIT_SSH_COMMAND="ssh -vvv" git pull这样在 git pull 的过程中,所有 SSH 握手日志都会直接打印到终端。这是排查 Git 和 SSH 组合问题最有力的武器,强烈推荐。
4.2 known_hosts 的“本机记忆”也可能成为障碍
OpenSSH 会把曾经连接过的主机公钥记录在 ~/.ssh/known_hosts 里。当你第一次连接某个主机时,会看到一句类似 “Are you sure you want to continue connecting?” 的提示。后续连接时,客户端会校验服务端公钥是否和 known_hosts 里的记录一致。
如果远端服务器的系统盘重新安装过,或者 SSH 服务的 host key 被重新生成过,而本机 known_hosts 里还保留着旧的 host key,客户端就会拒绝继续连接。某些严格配置下,这种拒绝也可能表现为连接过程中被直接中断。
判断是不是 known_hosts 问题很简单:看 verbose 输出里有没有 “REMOTE HOST IDENTIFICATION HAS CHANGED” 或者 “Host key verification failed” 字样。如果有,删除 known_hosts 中对应的旧条目即可:
ssh-keygen -R 远端主机IP执行完成后重新连接,客户端会重新询问是否信任新的 host key,回答 yes 就行。这里要特别提醒:删除旧 host key 前最好先确认服务器确实变更过 key。如果服务器没变更过,那可能是你连错了机器,或者 DNS 解析到了一个错误的地址。
4.3 客户端版本与服务端算法的兼容冲突
SSH 连接建立后,双方要协商出一套彼此都支持的加密算法。如果客户端版本太老,不认识服务端提供的所有算法,连接会失败或者被服务端直接重置。
这个问题在比较旧的公司内网服务器上概率更大。比如某些服务器只支持 ssh-rsa 加密算法,而新版本的 OpenSSH 默认禁用了 ssh-rsa,只接受更安全的 ssh-ed25519 和 ecdsa。两边谈不拢,连接就直接断了。
解决思路有两个方向:
- 升级本机 SSH 客户端,让客户端算法列表覆盖服务端算法。
- 在服务端开启对旧算法的兼容支持。如果你有服务器权限,可以在 sshd_config 里添加旧的 HostKeyAlgorithms 配置。这属于兼容性取舍,得根据服务器的安全要求评估。
临时验证客户端是否支持某算法,可以在 ssh 命令里指定算法:
ssh -o HostKeyAlgorithms=+ssh-rsa -p 22 git@远端主机IP如果能连通而默认连不通,基本可以确定是算法协商问题。但这种方式不建议长期使用,最好升级两端到统一的现代标准。
4.4 操作系统的 SSH 实现差异
Windows 上自带的 OpenSSH 和 Linux/macOS 自带的 OpenSSH 在默认配置上有一些细微差异,特别是在密钥文件的路径、代理支持、权限校验等方面。
Windows 环境里,用户的 SSH 目录位置是 C:\Users\用户名.ssh,和 Linux 是一致的,但权限模型的实现方式不同。Windows 上的文件 ACL 如果和 OpenSSH 的“期望权限”不一致,可能直接导致私钥文件被忽略,或者认证过程异常。
Windows 用户还可以检查一下是否启用了 Windows 防火墙对 OpenSSH 的出站拦截。在比较严格的企业环境里,Windows Defender 防火墙可能会阻止 ssh.exe 作为客户端出站。这时候报错特征就是:浏览器能上网,git 也正常,但所有 SSH 操作全部被重置。
遇到这种情况,可以把防火墙规则临时放行,或者通过系统设置允许 OpenSSH 客户端通过防火墙。不过我更建议先切换到 HTTPS 方式快速恢复工作,再慢慢调防火墙规则。
5. 一次完整排错实战:从报错出现到恢复只花了四步
5.1 现场情况
前阵子有个同事 A 找到我,说他在给公司的代码仓库推送分支时,终端里连续弹出这个报错:
Connection reset by 远端IP port 22 fatal: Could not read from remote repository.他说自己已经把 SSH 密钥重新生成过一遍,也重新添加到了托管平台上,问题依旧。我坐下来开始按链路排查。
第一步:查看 git remote -v,确认远程地址是否正常。结果地址里的主机名正确,协议是 SSH,端口没有特殊标注,默认走 22。
第二步:用 nc 测试端口连通性。执行:
nc -vz 远端IP 22输出显示连接成功,但紧接着又被重置。这说明该主机 22 端口在 TCP 层可以被触达,但有某种机制在连接后立刻断掉。结合“立刻断掉”这个特征,我怀疑问题不在 DNS、不在地址不可达,而在更深一层的防火墙或安全策略。
第三步:换了一个不同网段的环境测试。让 A 断开公司 Wi-Fi,用手机热点连接。在热点环境下执行同样命令,连接成功,SSH 认证也通过。到这里问题范围立刻缩小:远端没问题,密钥没问题,重心锁定在公司网络出口对 22 端口的连接策略上。
第四步:由于公司网络对 22 端口出站有限制,临时解决方式是让 A 把远程地址切换为 HTTPS 协议,用访问令牌代替密钥认证,不影响当前开发工作。后续再通过正规流程申请放开 22 端口权限。
5.2 这个案例带给我的三个经验
第一个经验:换网络环境测试是所有网络问题排查里效率最高的一步,它能在两分钟内告诉你问题出在你这一侧还是远端那一侧。
第二个经验:不要在第一轮排查里就去碰密钥。密钥问题几乎不会造成 “Connection reset”,它更多表现为认证失败。你重新生成密钥,等于把正确的工具换成了另一把正确的工具,但对连接重置毫无作用。
第三个经验:公司网络环境下,SSH 22 端口“时好时坏”非常正常。有的出口设备只对部分目的 IP 封 22,有的只对 SSH 协议特征做干扰,有的在负载高时才启动丢弃策略。所以同一个报错在不同时段出现与否,并不代表问题不存在,只是触发条件不同。
5.3 什么时候需要切换 SSH 之外的方案
有一种声音认为“用 Git 就应该走 SSH”,这是对 Git 协议的误解。Git 官方支持多种传输协议,HTTPS 和 SSH 都是生产环境广泛使用的方式。
如果遇到以下情况,完全可以考虑长期使用 HTTPS 而不是硬蹭 SSH:
- 你在公司网络里,22 端口被长期封禁,申请流程迟迟走不下来。
- 你使用代码托管平台的频率不高,偶尔拉取公共代码,HTTPS 的匿名访问足够。
- 团队统一使用账号体系管理访问权限,HTTPS 用 token 认证能更好匹配审计需求。
当然,如果团队大量使用 SSH 密钥做自动化部署、多机协作,那 22 端口还是值得坚持的。我见过不少团队将 SSH 端口改成高位端口后,在公司网络里反而畅通无阻,因为出口防火墙只封了 22,对高位端口没有特殊策略。不过修改服务器 SSH 端口属于基础设施变更,需要走运维审批,不建议个人擅自操作。
6. 停止踩坑之后:我每天都会检查的几个项目
这类问题排查多了之后,我养成了一个习惯:定期检查 SSH 和 Git 的配置文件、密钥权限、网络可达性,把故障消灭在萌芽阶段。具体来说有下面几项。
第一项是检查密钥权限。每过一段时间,系统更新、用户目录迁移都可能导致权限变化。在 Linux/macOS 上我会跑一遍:
ls -la ~/.ssh确保目录是 700、私钥是 600、公钥是 644。如果不对,立即修改。
第二项是维护一份 ~/.ssh/config 的清单。每增加一个新的代码托管平台或者新服务器,我都会在这里写清楚主机别名、端口、私钥路径和 IdentitiesOnly 选项。久而久之,这台机器连接的所有远端主机都有了清晰的配置,再也不会出现“客户端随机拿一把密钥去试”的窘境。
第三项是测试连通性。我写了一个简单的脚本,定时检查常用托管平台的 22 端口连通性:
for host in 托管平台1 托管平台2; do nc -zvw3 "$host" 22 && echo "$host ok" || echo "$host fail" done一旦发现某个主机连不通,立刻排查是网络策略变化还是远端故障,而不是等到和同事协作时才尴尬发现。
第四项是关注远程地址的形态。每当我们决定把协议从 SSH 切到 HTTPS,或者反过来切换到 SSH,都必须在团队文档里同步最终地址格式。因为一个人改了自己的本地配置,不代表所有人的本地配置都改了。团队协作中,远程仓库地址的统一管理非常关键,避免出现“我的机器能拉,他的机器一拉就报 Connection reset”的混乱局面。
最后再说一个我自己的操作习惯:遇到任何 Git 报错,先完整记录报错原文,再动手改任何配置。
“Connection reset by xxx.xxx.xxx.xxx port 22”这句话已经包含了足够多的信息:协议方向是 SSH、远端端口是 22、失败阶段是连接层。顺着这条链路往下查,网络环境、防火墙策略、端口配置、密钥文件、known_hosts,一个个排除,大概率能在半小时内解决问题。最怕的就是不看报错具体内容,上来就重新生成密钥、删除仓库重新克隆,那样不但浪费大把时间,还可能把本来正常的配置改坏,让问题雪上加霜。