sshd "error: kex_exchange_identification": What It Means
Published
sshd never finished the opening banner exchange with the client — the other end went away (or sent something unreadable) before sending its identification string, so the connection ended before key exchange and before any authentication.
What it looks like
Oct 18 03:51:11 web1 sshd-session[48211]: error: kex_exchange_identification: read: Connection reset by peer
- 1Time — When it was logged; this format has no year or time zone
- 2Host — The server’s host name
- 3Program — sshd-session, one process per connection, and its process ID: a connection’s lines share it
- 4Level — Logged at error level
Example lines as /var/log/auth.log shows them (/var/log/secure on RHEL and Fedora):
Oct 18 03:51:11 web1 sshd-session[48211]: error: kex_exchange_identification: read: Connection reset by peer Oct 18 03:51:12 web1 sshd-session[48213]: error: kex_exchange_identification: read: Connection reset by peer Oct 18 04:19:47 web1 sshd-session[49004]: error: kex_exchange_identification: read: Connection reset by peer
On our own server, the 30 days to 25 October 2026
kex_exchange_identification(struct ssh *ssh, int timeout_ms,
353
lines · about 12 a day
Last seen 25 October 2026
LogTriage's production API server (Hetzner, Germany): port 22 open to the internet, key-only since 6 October 2026 (password and keyboard-interactive authentication off); nginx in front of the API on ports 80 and 443. Counted weekly from the sshd journal; no address ever leaves the server. More in the monthly attack report; which free blocklists already list these attackers is measured in the IP blocklist report, and you can check the addresses in your own log.
Where the line comes from
Format strings as written in the source of OpenSSH 10.2p1 — the versions this server runs. Each links to the exact line.
-
kex_exchange_identification(struct ssh *ssh, int timeout_ms,function name, logged as the prefix · kex.c:1234 (OpenSSH) -
read: %.100sread error, e.g. Connection reset by peer · kex.c:1321 (OpenSSH) -
Connection closed by remote hostpeer closed the connection · kex.c:1316 (OpenSSH) -
banner line contains invalid charactersmalformed banner · kex.c:1332 (OpenSSH)
What it means
The prefix before the colon is a function name. kex_exchange_identification() lives in sshd’s kex.c and, as the comment above it puts it, “Sends our identification string and waits for the peer’s. Will block for up to timeout_ms (or indefinitely if timeout_ms <= 0).” That exchange of version banners (SSH-2.0-...) is the very first thing that happens on a new connection — before key exchange, before any user name is offered, before PAM is involved at all.
sshd reads the peer’s banner one byte at a time, and the code has three ways out that produce a message:
- The read fails. If the one-byte read does not return a byte and
errnois notEPIPE, sshd logsread: %.100swith the system error text — giving lines such aserror: kex_exchange_identification: read: Connection reset by peer. This is logged at error level, so it is visible at the defaultLogLevelof INFO. - The peer closed the connection (
errno == EPIPE). sshd logsConnection closed by remote hostthrough its verbose logging call, so a server left at the default INFO level never records it. - The banner is malformed. A NUL byte, or any byte arriving after a carriage return that is not a newline, produces
banner line contains invalid characters— again at verbose level — and the connection is treated as invalid.
Note what the line does not contain: no address, no port, no user. Those are in the companion line that the same process writes when it gives up — usually an untagged Connection closed by <ip> port <n> or Connection reset by <ip> port <n>, untagged because the pre-authentication child does not exist yet. See the Connection closed / reset page for how to read it.
Is it an attack?
Almost always it is not an attack in itself; it is the trace of a connection that never became an SSH session.
| Pattern | Reading |
|---|---|
| Isolated lines, scattered through the day | Internet-wide port surveys and banner grabs hanging up after the TCP handshake |
| Steady, regular cadence from a host you own | A monitoring system doing TCP-only checks against port 22 |
| Short bursts, several per second | A scanner working through your address range, or a client retrying hard |
Lines with no following Connection closed/reset entry | Check whether the log is being rate-limited or rotated mid-burst |
These lines, then Failed password or Invalid user from one address | The scan found the service and a brute-force tool came back — treat as a brute-force investigation |
Middleboxes also generate them: a firewall, IPS or NAT device that resets idle or disallowed flows produces read: Connection reset by peer on the server side even though the human client did nothing wrong. Clients that speak the wrong protocol to port 22 (an HTTP or TLS probe, for example) fall into the malformed-banner branch, which stays invisible at INFO.
How to investigate
The line has no address, so investigation means pivoting on the process ID and on the lines written around it.
# Debian/Ubuntu unit is ssh; on RHEL/Fedora use -u sshd
sudo journalctl -u ssh --since "24 hours ago" | grep 'kex_exchange_identification'
# Rate over the day: a steady background or a burst? (traditional auth.log timestamps)
sudo grep 'kex_exchange_identification' /var/log/auth.log \
| awk '{print $1, $2, substr($3,1,2)}' | uniq -c
# No address in the line — follow the PID to the rest of that connection
sudo grep 'sshd-session\[48211\]' /var/log/auth.log
# Connections that ended before the preauth child existed carry the addresses
sudo grep -E 'Connection (closed|reset) by' /var/log/auth.log | grep -v '\[preauth\]' \
| awk '{print $(NF-2)}' | sort | uniq -c | sort -rn | head -20
# The question that changes severity: did anything from those sources get in?
sudo grep -E 'Accepted (publickey|password)' /var/log/auth.log | tail -50
On a host where rsyslog is not installed, read the journal instead of /var/log/auth.log; on RHEL and Fedora the file is /var/log/secure. For an exported auth.log, the browser log analyzer groups the surrounding connection lines by source, and the SSH brute-force rule covers the follow-on stage where one source starts naming accounts.
How to stop it
You cannot stop a remote peer from hanging up mid-banner, so aim at volume and exposure.
- Reduce who can reach port 22. A firewall allow-list, a VPN or a bastion removes the scan traffic that produces most of these lines. Nothing else cuts the rate as much.
- Understand that key-only authentication does not help here. This failure happens before authentication, so turning off
PasswordAuthentication(defaultyes) changes nothing about these lines — it changes what happens to the sources that get further. - Leave
PerSourcePenaltiesenabled. The manual says penalties are on by default;noauthrefuses clients that “disconnect without attempting authentication” for 1s by default, accumulating up tomax(default 10m), withmin(default 15s) of penalty accrued before enforcement begins. The manual warns that this timeout “should be used cautiously otherwise it may penalise legitimate scanning tools such as ssh-keyscan(1)”. - Keep the unauthenticated-connection limits in place.
LoginGraceTime(“The server disconnects after this time if the user has not successfully logged in”, default 120 seconds) andMaxStartups(maximum concurrent unauthenticated connections, default10:30:100) bound how much a flood of half-finished connections can cost you. - Fix your own noise. If a monitoring system opens and closes TCP connections to port 22, point it at a check that completes a handshake, or document the source so triage skips it.
- Raise
LogLevelto VERBOSE only when you need the other two variants, and not to DEBUG: the manual states that “Logging with a DEBUG level violates the privacy of users and is not recommended.”
sudo sshd -t && sudo systemctl reload ssh
Test the configuration before reloading, and keep a working session open until a new one succeeds.
Lines you will see next to it
- Connection closed by … The client at the other end of the SSH connection went away — closed it cleanly, or reset it — and sshd logged the address, port and (if the client had already named an account) the user it was trying before it quit.
- Timeout before authentication for …, pid = … A connection was accepted but did not finish authenticating inside the login grace period, so sshd terminated the pre-authentication process and the listener logged the client and that process's pid.
- …Connection … sshd dropped the connection during the opening protocol banner exchange because what the client sent could not be parsed — almost always something that is not an SSH client at all.
Detect it
Frequently Asked Questions
- Does this line mean someone tried to log in?
- No. The banner exchange happens before key exchange and long before any authentication request, so no account name was offered and no credential was checked. The line records a TCP connection that reached the SSH daemon and then ended, nothing more.
- Why is there no IP address or port in the message?
- The format string is only "read: %.100s" filled in with the C library error text, so the address never appears in this line. Pivot on the process ID in the square brackets: the same sshd-session process normally logs a "Connection closed by" or "Connection reset by" line that carries the address and source port.
- What is the difference between "read: Connection reset by peer" and "Connection closed by remote host"?
- They are two branches of the same read loop. When the one-byte read fails and errno is EPIPE, sshd logs "Connection closed by remote host" through its verbose channel; any other short read is logged at error level as "read:" followed by the system error text. At the default LogLevel of INFO only the error-level variant is written to the log.
- Should I raise an alert on these?
- Not on the individual line, which on an internet-facing host is ordinary scan and health-check noise. Alert on shape instead: a sudden multiplication in rate, or a source that produces these and then comes back with authentication attempts or an Accepted line.
Run your own auth.log through it
Paste or drop /var/log/auth.log (or journalctl -u ssh output) into the free browser analyzer: brute force, password spraying, a successful login after failures and invalid-user sweeps are flagged with the evidence lines. Nothing is uploaded.