sshd Linux auth.log / journal Triage: informational

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

  1. 1Time — When it was logged; this format has no year or time zone
  2. 2Host — The server’s host name
  3. 3Program — sshd-session, one process per connection, and its process ID: a connection’s lines share it
  4. 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: %.100s read error, e.g. Connection reset by peer · kex.c:1321 (OpenSSH)
  • Connection closed by remote host peer closed the connection · kex.c:1316 (OpenSSH)
  • banner line contains invalid characters malformed 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 errno is not EPIPE, sshd logs read: %.100s with the system error text — giving lines such as error: kex_exchange_identification: read: Connection reset by peer. This is logged at error level, so it is visible at the default LogLevel of INFO.
  • The peer closed the connection (errno == EPIPE). sshd logs Connection closed by remote host through 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.

PatternReading
Isolated lines, scattered through the dayInternet-wide port surveys and banner grabs hanging up after the TCP handshake
Steady, regular cadence from a host you ownA monitoring system doing TCP-only checks against port 22
Short bursts, several per secondA scanner working through your address range, or a client retrying hard
Lines with no following Connection closed/reset entryCheck whether the log is being rate-limited or rotated mid-burst
These lines, then Failed password or Invalid user from one addressThe 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 (default yes) changes nothing about these lines — it changes what happens to the sources that get further.
  • Leave PerSourcePenalties enabled. The manual says penalties are on by default; noauth refuses clients that “disconnect without attempting authentication” for 1s by default, accumulating up to max (default 10m), with min (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) and MaxStartups (maximum concurrent unauthenticated connections, default 10: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 LogLevel to 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

MITRE ATT&CK: T1595.001T1046

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.