SSH jump host silently dropping connections after OpenSSH 9.5
A recent OpenSSH 9.5 release tweaked how the server handles TCP keepalive packets. My jump host – a modest Ubuntu 24.04 box – started dropping established SSH sessions after a few minutes of idle time. The problem popped up during a routine audit when several users complained of “connection closed” messages, yet nothing appeared in the logs.
What changed in 9.5?
OpenSSH 9.5 introduced new defaults for ClientAliveInterval and ClientAliveCountMax on the server side:
ClientAliveInterval 0
ClientAliveCountMax 3
With ClientAliveInterval set to zero, the server stops sending keepalive packets altogether. If a client stays idle, the server will close the session after ClientAliveCountMax retransmissions of the last packet it received. In practice, that means a silent drop after roughly 3–5 minutes on a congested network.
How I detected the problem
- Client logs –
ssh -vvv user@jumpprintedConnection closed by remote hostwithout any server‑side message. - Server logs –
/var/log/auth.loghad no disconnect entry. - Network capture –
tcpdump -i eth0 port 22showed the client sending a keepalive, but the server never replied.
The lack of a server log entry made it clear that the session was terminated by the server’s internal timeout logic, not by an explicit exit.
Quick fix: restore the old keepalive behaviour
The easiest way to bring back the familiar behaviour is to re‑enable the keepalive mechanism:
sudo tee /etc/ssh/sshd_config.d/keepalive.conf <<'EOF'
ClientAliveInterval 60
ClientAliveCountMax 3
EOF
sudo systemctl reload sshd
Now the server sends a keepalive every 60 seconds, matching OpenSSH 9.4 and earlier. The session will survive idle periods of up to three minutes, which is plenty for most jump‑host use cases.
A more robust approach: use ServerAliveInterval on the client
If you don’t want to touch the server, just set a client‑side keepalive:
ssh -o ServerAliveInterval=60 -o ServerAliveCountMax=3 user@jump
Or put it in ~/.ssh/config:
Host jump
ServerAliveInterval 60
ServerAliveCountMax 3
This keeps the connection alive without modifying the server defaults – handy when you’re using a cloud‑managed bastion.
Why the change matters for security
Tightening the defaults reduces the window during which an idle session can be hijacked. The downside is that legitimate sessions may drop, especially on high‑latency or lossy links. To balance usability and security:
- Monitoring – run
sshd -T | grep clientaliveafter a reload to see the effective values. - Logging – set
LogLevel VERBOSEif you need audit trails for keepalive failures. - Testing – use
ssh -vvvto confirm behaviour after any change.
Trade‑offs to consider
| Option | Pros | Cons |
|---|---|---|
| Server‑side keepalive (default 9.5) | Lower resource usage, tighter security | Silent drops, hard to debug |
| Re‑enable old keepalive | Familiar behaviour, easy to troubleshoot | Slightly higher resource usage |
| Client‑side keepalive | No server changes, flexible per‑user | Requires client configuration, may not work for all users |
For internal jump hosts, re‑enabling the old keepalive is usually the fastest route to stability. For shared or managed bastions, client‑side keepalive is safer.
Checklist for future upgrades
- Read the release notes – OpenSSH’s changelog often flags behavioural changes that affect long‑running connections.
- Test in staging – Spin up a copy of the jump host and run
ssh -vvvfor a few hours. - Document the config – Store keepalive settings in version‑controlled files (
/etc/ssh/sshd_config.d/). - Monitor session drops –
journalctl -u sshdorauditdcan catch unexpected disconnects early.
Follow these steps and you’ll keep your jump host reliable without compromising on security.
See also
- When systemd‑resolved silently drops a local /etc/hosts entry and the service you can’t reach
- When resolvectl Caches Fail: Quick Steps to Clear Stale DNS Entries Without Restarting NetworkManager
- When ss Shows No Open TCP Ports Even Though Services Are Running
- When nftables silently drops HTTP traffic: a step‑by‑step port‑filter investigation
- Why My Nginx Service Stops After a Kernel Upgrade: A Systemd Timer Debugging Session in a Home‑Lab Server