Troubleshooting

kex_exchange_identification: Connection reset by peer — why SSH drops you at once

SSH closes before the login prompt with 'Connection reset by peer'? Usually fail2ban, hosts.deny, MaxStartups or a broken sshd. Find which one.

Published ~11 min read

Short answer: kex_exchange_identification: read: Connection reset by peer means your client reached the SSH port and the TCP connection opened, but the server side hung up before sshd sent its first line. Something on the server, or directly in front of it, is turning you away. The usual suspects are a fail2ban ban, a rule in /etc/hosts.deny, sshd’s own limits (MaxStartups, and PerSourcePenalties on OpenSSH 9.8 and newer), or an sshd that cannot start a session because host keys are missing, the config is broken or the server is out of memory. Run ssh -vvv on your side, then read sshd’s log through your provider’s console.

what you see
$ ssh root@203.0.113.10
kex_exchange_identification: read: Connection reset by peer
Connection reset by 203.0.113.10 port 22

# the same failure, closed cleanly instead of reset
kex_exchange_identification: Connection closed by remote host
Connection closed by 203.0.113.10 port 22

# OpenSSH 7.9 and older clients name it differently
ssh_exchange_identification: read: Connection reset by peer
ssh_exchange_identification: Connection closed by remote host

Treat all four lines as one problem. “Reset” means a TCP RST arrived, “closed” means a normal FIN. Which one you get often comes down to timing: your client sends its own version line the moment it connects, and if the server closes the socket while that line is still unread, the kernel answers with a reset instead of a clean close.

What does kex_exchange_identification mean?

It is the very first step of an SSH session. Both sides send a one-line version string such as SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13, and kex_exchange_identification is the client function that waits for the server’s. The error says that line never came: the connection ended before the server said anything.

That rules out two whole classes of problem. The port is not closed (that would be Connection refused) and the packets are not silently dropped on the way (that would be a timeout). The server, or a device in front of it, accepted the connection and then chose to end it, so the answer is almost always in a log on that machine.

How do I find out which cause it is?

Start on your own machine. The verbose output already narrows it down:

on your computer
ssh -vvv root@203.0.113.10

debug1: Connecting to 203.0.113.10 [203.0.113.10] port 22.
debug1: Connection established.
...
debug1: Local version string SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.5
debug1: kex_exchange_identification: banner line 0: Exceeded MaxStartups
kex_exchange_identification: Connection closed by remote host

Connection established confirms the TCP handshake worked. Then look for a banner line 0: entry. When sshd drops a connection on purpose it sends a short reason first: Exceeded MaxStartups from OpenSSH 9.7 and older, Not allowed at this time from 9.8 and newer. If that line is there, skip straight to the MaxStartups and penalties sections below. If there is no banner line at all, the connection was cut by something that does not explain itself.

Next, look at the server. SSH is the thing that is broken, so use the web or VNC console in your provider’s panel:

on the server, via the console
# Debian, Ubuntu (the unit is called ssh)
sudo journalctl -u ssh -n 50 --no-pager

# Rocky, AlmaLinux, CentOS Stream (the unit is called sshd)
sudo journalctl -u sshd -n 50 --no-pager

sudo sshd -t                        # config check, silent when OK
sudo fail2ban-client status sshd    # if fail2ban is installed

Match what the log says against this list:

  • beginning MaxStartups throttling or drop connection #… past MaxStartups — too many half-open logins, see MaxStartups.
  • drop connection #… penalty: failed authentication — OpenSSH’s per-address penalties, see PerSourcePenalties.
  • refused connect from 198.51.100.7 — TCP wrappers, see hosts.deny.
  • Unable to load host key or sshd: no hostkeys available -- exiting. — see host keys.
  • fork: Cannot allocate memory — the server is out of resources, see memory and disk.
  • Nothing at all for your connection attempt — sshd never saw you. Check fail2ban and the firewall first, then your own network.

Is fail2ban rejecting you?

Very often, yes, and it is your own address. A script with an old key, a password manager trying a stale password or a few colleagues behind the same office IP are enough to reach maxretry. Depending on how the ban rejects packets and what sits in front of the server, a ban shows up as refused, timed out or reset, so check it whatever the exact wording:

check and lift a ban
sudo fail2ban-client status sshd
# |- Currently banned: 1
# `- Banned IP list:   198.51.100.7

sudo fail2ban-client set sshd unbanip 198.51.100.7

To stop it happening again, put your own static addresses on fail2ban’s ignore list. Run curl -4 https://ifconfig.me on your computer to see which address the server sees:

/etc/fail2ban/jail.local
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 198.51.100.7
apply
sudo systemctl restart fail2ban

Only whitelist addresses that are really yours. A dynamic home IP changes hands, and a whole provider range on the ignore list exempts every bot inside it. Other blockers work the same way: CrowdSec lists its bans with sudo cscli decisions list and lifts one with sudo cscli decisions delete --ip 198.51.100.7; sshguard and DenyHosts keep their own block lists. The SSH hardening guide has a sensible fail2ban jail to start from.

Is sshd itself penalising your address?

OpenSSH 9.8 added PerSourcePenalties, and it is on by default. sshd keeps a score per client address: by default each failed authentication costs 5 seconds, a connection that never tries to log in costs 1 second, and one that crashes the session costs 90. Once an address has collected more than 15 seconds, sshd refuses it until the penalty runs out, for up to 10 minutes. A handful of failed logins in quick succession is enough.

Debian 13 ships OpenSSH 10.0, so it has this. Debian 12 (9.2), Ubuntu 24.04 (9.6) and Rocky Linux 9 (8.7) do not. The log line is explicit:

sshd log
sshd[812]: drop connection #0 from [198.51.100.7]:50422 on [203.0.113.10]:22 penalty: failed authentication

Waiting a few minutes fixes it. To exempt addresses you trust, such as your office or a monitoring host, add them to the exempt list:

/etc/ssh/sshd_config.d/10-penalties.conf
PerSourcePenaltyExemptList 198.51.100.7,192.0.2.0/24
apply
sudo sshd -t && sudo systemctl reload ssh

Better still, fix whatever keeps failing to log in. A deploy job or backup script that authenticates wrongly every few minutes keeps the penalty topped up, and fail2ban will eventually ban the same address anyway.

Is /etc/hosts.deny blocking you?

Upstream OpenSSH dropped TCP wrappers years ago, but Debian and Ubuntu still build sshd with them, up to and including Debian 13. On those systems sshd reads /etc/hosts.allow and /etc/hosts.deny for every connection and closes it straight away if a deny rule matches. Rocky, AlmaLinux and RHEL 8 and newer removed TCP wrappers completely, so there the files do nothing.

show active rules
grep -Ev '^\s*(#|$)' /etc/hosts.allow /etc/hosts.deny

# typical culprits
/etc/hosts.deny:sshd: ALL
/etc/hosts.deny:ALL: ALL

Lines like these are left behind by hardening scripts, old control panels and DenyHosts. hosts.allow is checked first, so either delete the deny line or allow yourself explicitly with sshd: 198.51.100.7 in /etc/hosts.allow. The change applies to the next connection; nothing needs restarting.

Is sshd throttling new connections?

MaxStartups caps how many connections may sit in the login phase at once. The default, 10:30:100, means: once 10 connections are unauthenticated at the same time, sshd drops 30% of new ones at random, rising steadily to 100% at 100. Every bot that hammers port 22 occupies a slot until it gives up or LoginGraceTime (120 seconds by default) runs out. During a brute-force wave your own connection loses the lottery.

The signature: it fails, then works on the second or third try, and the log shows beginning MaxStartups throttling. Count the current connections on the port:

how busy is port 22?
sudo ss -tn state established '( sport = :22 )' | wc -l

A few dozen with only you logged in means bots are holding the door. Shorten the grace time and give sshd a little more room:

/etc/ssh/sshd_config.d/20-limits.conf
LoginGraceTime 30
MaxStartups 30:30:100
# OpenSSH 8.5 and newer
PerSourceMaxStartups 10
apply
sudo sshd -t && sudo systemctl reload ssh     # sshd on Rocky

PerSourceMaxStartups stops a single address from taking every slot. Do not set it too low if you run Ansible or other tools that open many sessions in parallel from one machine. The lasting fix is less bot traffic: keys only, fail2ban, sudo ufw limit OpenSSH (it blocks an address that opens six or more connections within 30 seconds), and optionally moving SSH to another port, which does not stop a determined attacker but empties the log of drive-by scanners.

Are the host keys missing?

sshd cannot start a session without at least one host key. That happens after a clean-up script deletes /etc/ssh/ssh_host_* on a cloned image and nothing regenerates them, after a restore that skipped /etc/ssh, or when the key files have the wrong permissions. On Debian 12 and Ubuntu 24.04 sshd loads the keys for every new connection, so the daemon keeps listening while each connection dies with Unable to load host key and no hostkeys available -- exiting in the log. OpenSSH 9.8 and newer load the keys once at start, so there the problem shows up as a daemon that will not start after the next restart.

on the server
ls -l /etc/ssh/ssh_host_*
sudo ssh-keygen -A                    # creates any missing host key types
sudo chmod 600 /etc/ssh/ssh_host_*_key    # Debian/Ubuntu; Rocky uses 640 root:ssh_keys
sudo sshd -t && sudo systemctl restart ssh    # sshd on Rocky

New keys mean every client that connected before now sees REMOTE HOST IDENTIFICATION HAS CHANGED. That warning is expected here; check the new fingerprint and replace the old entry as described in Host key verification failed.

Is the sshd configuration broken?

A running sshd keeps the configuration it started with, so a bad edit bites at the next restart or reboot. sshd -t names the file and line:

config check
sudo sshd -t
/etc/ssh/sshd_config.d/50-hardening.conf: line 3: Bad configuration option: PasswordAuthenticaton
/etc/ssh/sshd_config.d/50-hardening.conf: terminating, 1 bad configuration options

Fix the line, run sshd -t until it prints nothing, then restart. On a classic setup a daemon that fails to start gives Connection refused. On Ubuntu 22.10 and newer, including 24.04, systemd holds port 22 through ssh.socket and starts sshd on demand, so the TCP handshake can still succeed and the connection is dropped when sshd fails to start — which is exactly this error. systemctl status ssh shows the failure.

Is the server out of memory or disk?

For each connection sshd has to start a new process. If the kernel cannot provide one, because memory is exhausted or a process limit is hit, sshd accepts the connection and closes it again, logging fork: Cannot allocate memory. The out-of-memory killer may also have killed sshd’s session processes:

on the server
free -m
df -h /
sudo dmesg -T | grep -iE 'out of memory|killed process'

A full root filesystem rarely stops the banner by itself, but it breaks logins, package upgrades and service restarts, so free space first if df shows 100%. The fixes are in Killed: why Linux kills your process when memory runs out and No space left on device.

Is something other than sshd answering on the port?

If port 22 (or the port you use) is forwarded somewhere — a Docker-published port for a Git server, a NAT rule, a reverse proxy — the forwarder accepts the connection and resets it when nothing listens behind it. Check which process owns the socket:

who listens on 22?
sudo ss -tlnp 'sport = :22'
# users:(("sshd",pid=812,fd=3))          expected
# users:(("docker-proxy",pid=1450,fd=4)) a container owns the port

Is the problem on my side?

If the server log shows nothing at all for your attempts, test from a different network, for example a phone hotspot. If SSH works there, the server is fine and something on your normal path cuts the connection:

  • Corporate or campus firewalls that recognise the SSH protocol and reset it, or only allow port 22 to approved hosts.
  • Antivirus or “web protection” suites that intercept outgoing connections. Pause the network shield briefly and retry.
  • A VPN client with its own firewall rules or a kill switch. Try once with the VPN off and once with it on.

How is this different from refused or timed out?

  • Connection refused — the server answered the very first packet with a reset: nothing listens on the port, or a firewall rejects it. See SSH connection refused.
  • Connection timed out — no answer at all: a firewall drops the packets, the IP is wrong or the server is down. See SSH connection timed out.
  • kex_exchange_identification: reset or closed — the connection opened, then sshd or something in front of it hung up before the version exchange. The causes are the ones on this page.
  • Permission denied (publickey) — the SSH conversation worked and the server rejected your key. See Permission denied (publickey).

Almost every fix on this page needs a shell on a server you cannot SSH into. On a VMHeaven VPS that is the VNC console in the panel: it connects to the machine’s screen rather than through SSH, so you can read sshd’s log, lift a fail2ban ban or regenerate host keys while port 22 turns you away. Every KVM plan has it.

Frequently asked

What is the difference between 'Connection reset by peer' and 'Connection refused' in SSH?

'Connection refused' means the server answered the very first packet with a reset, so nothing is listening on the port or a firewall rejects it. 'kex_exchange_identification: Connection reset by peer' means the TCP connection did open, and then sshd or something in front of it hung up before sending the SSH version line. The first points at a stopped daemon or a firewall rule, the second at a ban, an sshd limit or an sshd that cannot start a session.

Can fail2ban cause kex_exchange_identification: Connection reset by peer?

Yes. A banned address can see a reset, a refusal or a timeout, depending on how the ban rejects packets and what sits in front of the server. Check with 'sudo fail2ban-client status sshd', lift the ban with 'sudo fail2ban-client set sshd unbanip <your-ip>' and add your static IP to 'ignoreip' in /etc/fail2ban/jail.local. On OpenSSH 9.8 and newer, sshd's own PerSourcePenalties can block an address after a few failed logins too.

Why does SSH work from one network but not from another?

Either the server is blocking the address of that network, through a fail2ban ban, a hosts.deny rule or an sshd penalty, or something on that network cuts SSH: a corporate firewall, an antivirus web shield or a VPN client. If the server log shows nothing for your attempts, the connection never reached sshd and the cause is on your side.

Does changing the SSH port fix Connection reset by peer?

Only when the cause is bot traffic filling sshd's MaxStartups slots. A new port gets far fewer drive-by scans, so the slots stay free. It does not help with a fail2ban ban, a hosts.deny rule, missing host keys or a broken configuration, and it is not a security measure on its own: keys only and fail2ban do the real work.

Related articles

All troubleshooting