Connection closed by 172.22.0.3 port 8080, with kex_exchange_identification: Connection closed by remote host above it when you add -v, appeared here when ssh was pointed at a port where a web server listened, not an SSH server. Check the port: connect to the port where sshd listens.

The -v output shows what answered instead of SSH: in this test, an HTML error page from Python's http.server.

The error

$ ssh -o BatchMode=yes -p 8080 dev@web true
Connection closed by 172.22.0.3 port 8080
exit=255

Why it happens

Port 8080 on web ran python3 -m http.server. The plain message says only that the other side closed the connection. With -v, the client showed every line it received while waiting for the server's SSH identification line, and they were HTML (the command filters out the configuration lines at the top; the cut is marked):

$ ssh -v -o BatchMode=yes -p 8080 dev@web true 2>&1 | grep -v -E '^debug1: (Reading|/etc/ssh|identity file|Local version|Authenticator|OpenSSH)'
OpenSSH_9.6p1 Ubuntu-3ubuntu13.20, OpenSSL 3.0.13 30 Jan 2024
debug1: Connecting to web [172.22.0.3] port 8080.
debug1: Connection established.
debug1: kex_exchange_identification: banner line 0: <!DOCTYPE HTML>
...
debug1: kex_exchange_identification: banner line 8:         <p>Error code: 400</p>
debug1: kex_exchange_identification: banner line 9:         <p>Message: Bad HTTP/0.9 request type ('SSH-2.0-OpenSSH_9.6p1').</p>
...
kex_exchange_identification: Connection closed by remote host
Connection closed by 172.22.0.3 port 8080
exit=0

The connection itself was established, as the third line shows, so the host and port were reachable. The web server read the client's own identification line, SSH-2.0-OpenSSH_9.6p1, as an HTTP request, answered with a 400 error page and closed the connection. (The exit=0 in this block is the exit code of grep, the last command of the pipeline; the unfiltered run above exited with 255.)

The fix

Connect to the SSH port. In this test the SSH server was the container sshd on port 22, and the login worked there, while port 8080 answered HTTP as it should:

$ ssh -o BatchMode=yes sshd hostname
9aec0d80cdf9
exit=0
$ curl -sS -o /dev/null -w '%{http_code}\n' http://web:8080/
200
exit=0

The user dev for that login came from the client's ~/.ssh/config. A "banner exchange ... invalid format" message did not appear in this test; this OpenSSH version printed the two lines above. Other causes of a closed connection at this step were not tested.

How it was reproduced

Reproduced inside ubuntu:24.04 containers on one user-defined Docker network on Docker engine 29.9.3, with OpenSSH_9.6p1 Ubuntu-3ubuntu13.20 in the client. The container web ran Python 3.12.3's http.server on 8080; the container sshd ran OpenSSH's server on 22 with a test user dev. The containers and the network were removed afterwards.

Frequently asked

What does kex_exchange_identification: Connection closed by remote host mean?
The server closed the connection before it sent an SSH identification line. In this test the port belonged to a web server, which answered with an HTML error page.
How do I see what answered on an SSH port?
Run ssh with -v. The kex_exchange_identification: banner line entries show each line the other side sent; here they were HTML.
How do I fix Connection closed by port when using ssh?
In this test the fix was to connect to the port where sshd listened. Port 8080 was a web server and answered HTTP, not SSH.

When nothing at all listens on the port, the message is different: SSH: connect to host port 22: Connection refused. More decoded errors in the Fixes category.