sshd Linux auth.log / journal Triage: low

sshd "banner exchange: Connection from …: invalid format": What It Means

Published

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.

What it looks like

Oct 18 03:51:11 web1 sshd-session[48211]: banner exchange: Connection from 203.0.113.7 port 54321: invalid format

  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. 4Source address — The client’s IP address
  5. 5Source port — The client’s source port; with the address it identifies one connection

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]: banner exchange: Connection from 203.0.113.7 port 54321: invalid format
Oct 18 04:02:37 web1 sshd-session[48344]: banner exchange: Connection from 198.51.100.22 port 40118: invalid format
Oct 18 04:19:05 web1 sshd-session[48512]: banner exchange: Connection from 2001:db8::5f port 51902: invalid format

On our own server, the 30 days to 25 October 2026

…Connection …

306

lines · about 10 a day · 176 source addresses

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.

What it means

sshd-session arms the login grace timer and then runs the opening protocol exchange, in which both ends send their SSH identification string. That is the call to kex_exchange_identification() at sshd-session.c:1251. If it returns an error, sshd hands that error to sshpkt_fatal() with the tag banner exchange.

The packet layer’s handler formats the line from %s%sConnection %s %s — the tag, a ": " separator, the word from because this is the server side, and the remote identifier, which is the client’s address and source port. The error text is appended: here it is ssherr.c’s string for SSH_ERR_INVALID_FORMAT, invalid format. The function logs and dies, so the connection ends there.

Three things follow from where in the code this happens:

  • No [preauth] tag. The banner exchange runs before privsep_preauth() (sshd-session.c:1273), so the session process writes the line itself rather than the monitor relaying it from the pre-authentication child.
  • No user name, ever. The authentication context is allocated after the banner exchange. Nothing on this connection asked to be anybody.
  • Address and port are all you get. The port is the client’s source port for that one TCP connection, which is how you tie this line to the other lines sharing the same PID — including the kex_exchange_identification: line that sshd’s key-exchange code may have logged just before it, which is where any detail about what the client actually sent will be.

Is it an attack?

In practice this is the signature of something that is not an SSH client talking to port 22.

PatternReading
One connection per source, many different sourcesInternet-wide scanners probing every port, including with HTTP or TLS payloads
Bursts from one address, no other sshd linesA scanner or fuzzer working through protocols against your port 22
Regular, identical intervals from an internal addressYour own health check, uptime monitor or load balancer pointed at the SSH port but not speaking SSH
Mixed with Invalid user or Failed password from the same addressA credential-guessing client whose earlier connections were malformed — triage the authentication lines, not this one
Any Accepted line from the same sourceStop triaging the banner noise and investigate the session

On its own the line is low-value: it proves only that a TCP connection reached sshd and sent something unparseable. It is worth reading when the same source also appears in authentication lines, or when a source you own starts producing it after a configuration change.

How to investigate

Pivot on the source address and on the PID.

# Last 24 hours from the journal (Debian/Ubuntu unit is ssh; RHEL/Fedora use -u sshd)
sudo journalctl -u ssh --since "24 hours ago" | grep "banner exchange"

# Busiest sources (/var/log/auth.log; on RHEL/Fedora /var/log/secure)
sudo grep -oP 'banner exchange: Connection from \K\S+' /var/log/auth.log \
  | sort | uniq -c | sort -rn | head -20

# Everything one connection logged, by PID — any kex_exchange_identification
# line on the same PID carries the detail this line does not
sudo grep 'sshd-session\[48211\]' /var/log/auth.log

# Do the same addresses also try to authenticate?
sudo grep -E 'Failed password|Invalid user' /var/log/auth.log | grep '203.0.113.7'

# Did anything from them get in?
sudo grep -E 'Accepted (publickey|password)' /var/log/auth.log | tail -50

An address that only ever appears in banner exchange lines is noise. An address that appears here and in authentication lines belongs in the brute-force workflow — the SSH brute-force rule encodes that one-source-many-accounts shape, and an exported auth.log can be grouped by source in the browser log analyzer. If the source is yours, capture what it sends before changing anything; a monitor doing a bare TCP connect and a monitor sending an HTTP request look identical in this log line.

LogLevel defaults to INFO; the manual notes that logging with a DEBUG level “violates the privacy of users and is not recommended”, so prefer a short packet capture to raising the daemon’s verbosity on a production host.

How to stop it

You cannot stop a remote host from sending garbage to an open port, so the work is reducing exposure and limiting what each connection costs you.

  • Do not expose port 22 to the whole internet. A firewall allow-list, a VPN or a bastion removes these connections entirely; nothing in sshd_config will.
  • Fix your own senders. If the address is a load balancer or monitoring probe, point its check at a plain TCP connect, or at the right port.
  • Leave PerSourcePenalties enabled. The manual says penalties are on by default. The noauth penalty (default 1s) covers clients that “disconnect without attempting authentication”, accumulating to a max of 10m, with a min of 15s accrued before enforcement begins — the manual warns to use noauth cautiously, as it “may penalise legitimate scanning tools such as ssh-keyscan(1)”.
  • Keep MaxStartups sane (default 10:30:100): these are unauthenticated connections, and that is the limit on how many may be in flight at once.
  • Keep LoginGraceTime (default 120 seconds) at or below the default so stalled connections are cut off.
sudo sshd -t && sudo systemctl reload ssh

Validate the configuration before reloading, and keep an existing session open until a new one works.

Lines you will see next to it

  • kex_exchange_identification(struct ssh *ssh, int timeout_ms, 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.
  • Unable to negotiate with …: …. Their offer: … The client and the server had no key exchange method, host key type, cipher or MAC in common, so sshd logged what the client offered and ended the connection before any account was named.
  • 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.

Detect it

MITRE ATT&CK: T1595.001T1595.002T1021.004

Frequently Asked Questions

Does this line mean somebody tried to log in?
No. The banner exchange happens before any authentication request, so the connection never named an account and never offered a password or a key. The line carries only the client's address and source port, and the process exits immediately afterwards, so nothing on this connection could have succeeded.
Why is there no "[preauth]" tag on this message?
Because the message is written by the session process itself, not by the privilege-separated pre-authentication child. In sshd-session.c the banner exchange runs at line 1251, while privsep_preauth() is only called later at line 1273, so there is no monitor relaying the message and nothing to tag.
How is this different from "Unable to negotiate with …"?
Both come from the same error handler in sshd's packet layer, one branch apart. "Unable to negotiate" is logged when key exchange fails because the two ends share no algorithm, and it prints the client's offer. The "banner exchange" line is logged for other failures of the identification exchange, here with the error text "invalid format" — the client's opening data could not be parsed at all.
Will turning off password authentication reduce these lines?
No. These connections die before authentication of any kind is attempted, so PasswordAuthentication, KbdInteractiveAuthentication and AllowUsers make no difference to them. Only restricting who can reach port 22 — a firewall, a VPN or a bastion — reduces the volume.

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.