If SSH or SFTP logins started failing right after an OpenSSH 10 upgrade, the usual cause is not your firewall, your users, or the SFTP subsystem. It is key exchange. OpenSSH 10 disables old finite-field Diffie-Hellman key exchange methods in sshd by default, so older clients suddenly fail with errors like Unable to negotiate with x.x.x.x port 22: no matching key exchange method found. If the client only offers legacy KEX methods such as diffie-hellman-group14-sha1 or diffie-hellman-group1-sha1, the SSH transport never finishes. That means SFTP fails too, because SFTP runs on top of SSH. The subsystem is never reached.

In our experience managing production Linux servers, this is exactly the kind of breakage that shows up during OS refreshes, panel migrations, and VPS rebuilds. Everything looks fine until one old accounting package, one embedded device, or one partner-side file transfer client stops connecting. Then you find out that “legacy” in SSH terms still means “actively used somewhere in your business.” Debian 13 makes this more common because it ships OpenSSH 10 in the default stack. If you are refreshing systems now, this class of incident is normal. It is not hypothetical.

The fix is straightforward, but you need to do it carefully. Re-enable the smallest possible legacy KEX set, validate it from logs, and plan an exit. Do not blindly paste every weak algorithm back into sshd_config. That is lazy, and it expands your attack surface for no reason.

What actually changed in OpenSSH 10

OpenSSH 10 changed the server defaults. The important part is this: sshd no longer offers the old finite-field Diffie-Hellman families by default. On an upgraded Debian 13 server, a legacy client that still depends on one of those methods may fail before authentication even starts.

  • diffie-hellman-group14-sha1 was already on the way out for years and is no longer in the default set.
  • diffie-hellman-group-exchange-* methods are also removed from the server default proposal in OpenSSH 10.
  • If your legacy client has no overlap with the new default KEX set, the connection dies during negotiation.
  • SFTP is affected because it uses the same SSH transport layer.

A common operational mistake is to blame SFTP itself. That is wrong. If the SSH transport cannot agree on KEX, there is no SFTP session, no chroot issue, no internal-sftp issue, and no file permission issue to debug. The first thing we check is always the negotiation failure in the SSH logs.

The exact symptoms you will see

The client side usually shows one of these:

Unable to negotiate with 203.0.113.10 port 22: no matching key exchange method found.
Their offer: diffie-hellman-group14-sha1

Unable to negotiate with 203.0.113.10 port 22: no matching key exchange method found.
Their offer: diffie-hellman-group1-sha1,diffie-hellman-group14-sha1

Connection closed by remote host

On the server side, you will usually find the useful evidence in one of these places:

  • journalctl -u ssh on systemd-based distributions
  • /var/log/auth.log on Debian and Ubuntu if rsyslog is in use
  • /var/log/secure on RHEL-based systems
journalctl -u ssh -n 100 --no-pager
grep -i "no matching key exchange method found" /var/log/auth.log
grep -i "Unable to negotiate" /var/log/auth.log

The useful line is the one that tells you what the client offered. That list determines your fix. If the log shows only diffie-hellman-group14-sha1, that is a narrower compatibility problem. If it shows group1 and old SHA1-based exchange only, the client is much older and the correct answer may be “replace it,” not “keep weakening the server.”

Do not start with a blind global rollback

The bad fix is to throw a long list of obsolete algorithms back into sshd_config and call it done. That works, but it is the kind of shortcut that lingers for years. The right fix starts with the exact client offer shown in the logs.

  • If the client offers diffie-hellman-group14-sha1, start there.
  • If the client also supports something better, use that instead.
  • If the client only works with diffie-hellman-group1-sha1, treat it as a last-resort exception.
  • If this is a partner integration or an internal batch process, put an owner and retirement date on the exception.

After enough incident reviews, the pattern is obvious. Temporary SSH compatibility exceptions tend to become permanent unless someone writes down why they exist and who is supposed to remove them.

Runbook: re-enable the minimum legacy KEX in sshd_config

Back up the file first.

cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%F-%H%M%S)

Then append the least-bad algorithm that matches the failing client. In most cases, start with diffie-hellman-group14-sha1.

# /etc/ssh/sshd_config
KexAlgorithms +diffie-hellman-group14-sha1

Validate the syntax before reloading.

sshd -t

If syntax is clean, reload.

systemctl reload ssh

On Debian-based systems, ssh is usually the correct unit name. On some builds it may be sshd. Do not guess. Check it.

systemctl status ssh
systemctl status sshd

Then test again from the failing client or reproduce from a modern client using the same KEX constraint.

ssh -oKexAlgorithms=diffie-hellman-group14-sha1 user@server
sftp -oKexAlgorithms=diffie-hellman-group14-sha1 user@server

If that works, stop there. Do not add more.

When group14-sha1 is not enough

Some older clients do not offer anything beyond diffie-hellman-group1-sha1 or old group-exchange variants. That is where you need judgment.

  • Better option: replace or upgrade the client.
  • Acceptable stopgap: enable one additional method temporarily, document it, and isolate the exposure.
  • Bad idea: dump group1, old CBC ciphers, and old MACs back into the global defaults with no controls.

If you absolutely must support a very old endpoint for a short period, the config might look like this:

# /etc/ssh/sshd_config
KexAlgorithms +diffie-hellman-group14-sha1,+diffie-hellman-group1-sha1

That is ugly. Treat it like an incident workaround, not a design choice.

In our experience, the first thing that breaks after re-enabling old KEX is not always KEX itself. It is often the next negotiation stage. A client that needs group1 may also need old host key or cipher support. If you see the error move from KEX failure to host key or cipher mismatch, that is your signal that the client is too old to keep accommodating on a general-purpose internet-facing SSH service.

The mistake people make with Match blocks

This matters, because a lot of blog posts get it wrong. You cannot use a normal Match Address block inside one sshd_config file to scope KexAlgorithms to a legacy source IP. That sounds neat, but it does not work the way people think.

Why? Because KEX happens before user authentication and before most of the post-handshake logic that Match is intended to control. The OpenSSH manual reflects that reality. Match allows a subset of keywords, and KexAlgorithms is not one of them. So if someone tells you to fix this with:

Match Address 198.51.100.25
    KexAlgorithms +diffie-hellman-group14-sha1

that is not a real server-side fix.

This is one of those details that only shows up when you actually test the thing instead of copying advice from generic SSH-hardening posts. The right scoped approach is different.

How to scope legacy SSH compatibility properly

If you truly need to scope legacy support, you have two realistic options.

  • Run a separate SSH listener on a dedicated IP or non-standard port with the legacy KEX exception.
  • Keep the exception on the main listener only temporarily, and restrict source IPs at the firewall level while you replace the client.

The separate-listener pattern is cleaner. Example:

# main sshd_config
Port 22
ListenAddress 0.0.0.0

# legacy instance on port 2222 or dedicated IP
# separate config file, e.g. /etc/ssh/sshd_config_legacy
Port 2222
ListenAddress 203.0.113.10
KexAlgorithms +diffie-hellman-group14-sha1
PasswordAuthentication no
PermitRootLogin no
AllowUsers legacy-sftp-user
Subsystem sftp internal-sftp

Then pair that with firewall rules that only allow the partner IP or the specific office IP range to reach the legacy listener.

If you cannot justify that extra cleanup work, be honest with yourself. What you are doing is not a surgical exception. It is a global weakening of the SSH service.

Validate with logs, not assumptions

Once the change is in place, test from both sides and watch the logs live.

journalctl -u ssh -f

ssh -vvv user@server
sftp -vvv user@server

What you want to confirm:

  • The previous KEX failure disappears.
  • The session now reaches authentication.
  • SFTP subsystem startup is successful if the use case is file transfer.
  • No new host key or cipher mismatch error replaces the old KEX error.

You should also inspect the effective server config, not just the file you edited.

sshd -T | grep -i kexalgorithms

That command saves time. On busy systems with include files, automation, and panel tooling, what you think is active and what sshd is actually using are not always the same thing.

What usually goes wrong after the first fix

  • You reloaded the wrong unit. Debian often uses ssh, not sshd.
  • You replaced the default KEX list instead of appending to it. Use + unless you deliberately want to override everything.
  • You enabled too many weak algorithms. That solves the ticket and creates a longer-term security problem.
  • You fixed KEX but ignored the next failure. Old clients often fail on host key or cipher negotiation immediately after KEX is restored.
  • You assumed Match could scope KEX. It cannot in the normal single-daemon way people describe.

A real-world operational insight here: the most dangerous version of this incident is not the outage. It is the quiet workaround that stays in place after the outage is over. Six months later nobody remembers why group1 is enabled, and nobody knows which business process still depends on it.

When to stop patching around the problem

If the failing endpoint is a ten-year-old appliance, an abandoned desktop SFTP client, or an application nobody can update, then you are not solving an SSH problem. You are managing technical debt. At that point, the honest options are:

  • replace the client
  • segment it behind a dedicated legacy listener
  • move the workflow to a managed and controlled transfer path
  • retire the dependency

For teams that do not want to spend their week debugging post-upgrade SSH breakage, ServerSpan’s Linux administration service is built for exactly this class of production issue. If the underlying problem is bigger than one SSH daemon and you are already rebuilding or refreshing infrastructure, a clean virtual server deployment is often the faster path than layering exception after exception onto an aging node.

For related context, see Debian 13 Trixie Is Now Available on Enginyring Virtual Servers and How to Detect Intruders in Your VPS Server: Complete Security Guide.

A short recovery checklist

  1. Read the SSH logs and capture the exact client-offered KEX list.
  2. Add only the minimum legacy method needed, starting with diffie-hellman-group14-sha1.
  3. Validate with sshd -t.
  4. Reload the correct SSH service unit.
  5. Confirm the effective config with sshd -T | grep kexalgorithms.
  6. Retest with ssh -vvv or sftp -vvv.
  7. If the client still needs group1 or more legacy concessions, move it to a dedicated listener or replace it.
  8. Document the exception and set a removal date.

If you handle the incident that way, you fix the outage without turning one old SFTP client into a permanent server-wide downgrade.

Source & Attribution

This article is based on original data belonging to serverspan.com blog. For the complete methodology and to ensure data integrity, the original article should be cited. The canonical source is available at: OpenSSH 10 breaks legacy SSH/SFTP: Fix “no matching key exchange method found”.