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
- 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
- 4Source address — The client’s IP address
- 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.
-
banner exchangecontext · sshd-session.c:1253 (OpenSSH) -
%s%sConnection %s %smessage · packet.c:2058 (OpenSSH) -
invalid formaterror text appended to the message · ssherr.c:35 (OpenSSH)
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 beforeprivsep_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.
| Pattern | Reading |
|---|---|
| One connection per source, many different sources | Internet-wide scanners probing every port, including with HTTP or TLS payloads |
| Bursts from one address, no other sshd lines | A scanner or fuzzer working through protocols against your port 22 |
| Regular, identical intervals from an internal address | Your 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 address | A credential-guessing client whose earlier connections were malformed — triage the authentication lines, not this one |
Any Accepted line from the same source | Stop 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_configwill. - 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
PerSourcePenaltiesenabled. The manual says penalties are on by default. Thenoauthpenalty (default 1s) covers clients that “disconnect without attempting authentication”, accumulating to amaxof 10m, with aminof 15s accrued before enforcement begins — the manual warns to usenoauthcautiously, as it “may penalise legitimate scanning tools such as ssh-keyscan(1)”. - Keep
MaxStartupssane (default10: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
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.