BlueByte
Too many authentication failuresFixed

SSH: Received disconnect ... Too many authentication failures

By Haneul SeoUpdated September 26, 20268 min

Hi, it's BlueByte. You run ssh deploy@bastion, you are never prompted for anything, and the connection dies with Too many authentication failures. The key you meant to use is on disk, it is authorised on the server, and none of that matters — your agent offered several other keys first, the server counted each one as an attempt, and it hung up before reaching yours. We'll walk through what both sides log, why every offered key costs an attempt, how to see the offers go out, the fix per cause, and how to keep your identity list short enough that this stops happening.

What the disconnect says, on the client and on the server

The client prints the server's disconnect message as it received it:

Received disconnect from 203.0.113.10 port 22:2: Too many authentication failures
Disconnected from 203.0.113.10 port 22

OpenSSH builds that line from the format Received disconnect from %s port %d:%u: %.400s — host, port, the numeric disconnect reason, then the server's text. The 2 is the protocol-error reason code the server sends with this particular disconnect, so it carries no extra meaning here.

The server side is more useful, and it is worth pulling before you start changing config:

sudo journalctl -t sshd --since "10 min ago" | grep -i "maximum authentication"
sshd[4412]: error: maximum authentication attempts exceeded for deploy from 198.51.100.24 port 51922 ssh2 [preauth]

OpenSSH emits that from auth_maxtries_exceeded() with the format maximum authentication attempts exceeded for %s%.100s from %.200s port %d ssh2, and prefixes the username with invalid user when the account does not exist. That prefix is the fastest way to tell a key-count problem from a typo in the username, and it costs you one command to check. Matching on the sshd syslog identifier rather than a unit name keeps that command working on either family, because Debian-derived systems call the unit ssh and Red Hat-derived ones call it sshd.

Why every key your agent holds costs you an attempt

sshd_config defines the budget. The manual describes MaxAuthTries as the maximum number of authentication attempts permitted per connection, notes that once the number of failures reaches half this value additional failures are logged, and gives the default as 6.

The part that surprises people is what counts. Public-key authentication is a negotiation: the client offers a public key, the server checks it against authorized_keys and answers yes or no, and only then does the client prove possession of the private half. Every offer the server rejects is a failed attempt. Six keys in your agent, six attempts, and the seventh — the one that would have worked — never gets sent.

Two defaults make the list longer than you expect. IdentitiesOnly defaults to no, which the ssh_config manual describes as allowing ssh-agent, a PKCS11Provider or a SecurityKeyProvider to offer more identities than the ones you configured. And IdentityFile is additive: the manual states that multiple IdentityFile directives will add to the list of identities tried, and calls out that this behaviour differs from other configuration directives, where the first specified value wins.

Where the extra identities come from

  • An agent that accumulated keys. AddKeysToAgent in a shell profile, a desktop keyring that loads everything in ~/.ssh, or a laptop that has collected keys from several jobs.
  • Stacked IdentityFile lines. A Host * block listing three keys adds all three to every connection, on top of the default files.
  • The default file list. With no configuration at all, ssh will try ~/.ssh/id_rsa, ~/.ssh/id_ecdsa, ~/.ssh/id_ecdsa_sk, ~/.ssh/id_ed25519, ~/.ssh/id_ed25519_sk and ~/.ssh/id_mldsa44_ed25519 — the exact set varies by version.
  • A lowered server budget. Hardening baselines routinely set MaxAuthTries 3, which halves your room without anything changing on your side.
  • The wrong username. Every key fails against an account that does not have them, so you hit the cap no matter how few you offer. The invalid user prefix in the server log names this one.

Count the keys, then watch the offers go out

Ask the agent what it is holding:

ssh-add -l
256 SHA256:9Xk2... alice@laptop (ED25519)
3072 SHA256:7Qp1... alice@old-job (RSA)
256 SHA256:2Fm8... alice@personal (ED25519)

Then watch a real connection decide what to send. This is the command that answers the question outright:

ssh -v deploy@bastion 2>&1 | grep -E "Offering|Will attempt|Authentications that can continue"
debug1: Will attempt key: alice@laptop ED25519 SHA256:9Xk2... agent
debug1: Offering public key: alice@laptop ED25519 SHA256:9Xk2... agent
debug1: Authentications that can continue: publickey
debug1: Offering public key: alice@old-job RSA SHA256:7Qp1... agent
debug1: Offering public key: alice@personal ED25519 SHA256:2Fm8... agent

Count the Offering lines. If the run stops at three or six offers and disconnects, you have your answer and there is nothing wrong with your key. To see the full list ssh would work through without connecting at all, ask it to print the effective configuration:

ssh -G deploy@bastion | grep -E "^identityfile|^identitiesonly|^user "

ssh -G resolves every Host and Match block for that destination, so it shows what will really be used rather than what you think you wrote.

Check what the server is counting to

If you administer the host, read the value the daemon actually resolved rather than the file:

sudo sshd -T | grep -iE "maxauthtries|logingracetime|pubkeyauthentication"
maxauthtries 3
logingracetime 120
pubkeyauthentication yes

sshd -T expands includes and drop-in files, which matters because most distributions now ship /etc/ssh/sshd_config.d/ fragments that a hardening role writes into.

Fix it from the client, with one key and one Host block

The immediate unblock is a single flag pair — it tells ssh to ignore the agent's extra offers and send exactly one identity:

ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519_bastion deploy@bastion

Make it permanent per host rather than globally, because the agent is genuinely useful everywhere else:

Host bastion
  HostName 203.0.113.10
  User deploy
  IdentityFile ~/.ssh/id_ed25519_bastion
  IdentitiesOnly yes

Put specific Host blocks above any Host * block. The ssh_config manual is direct about the ordering rule: unless noted otherwise, for each configuration directive the first specified value will be used. A Host * block at the top of the file wins over the block you wrote underneath it.

Two smaller moves cover the rest. ssh-add -d ~/.ssh/id_rsa_old drops one stale key; ssh-add -D clears the agent entirely and you re-add what you need. And IdentityAgent none in a Host block ignores the agent for that destination without touching SSH_AUTH_SOCK for anything else. On the server, raising MaxAuthTries is possible and rarely the right answer — it widens the same window every brute-force attempt uses, to work around a list only you can see.

A worked example: a bastion, nine keys, and MaxAuthTries 3

An engineer restores a laptop from backup and the keyring loads nine keys into the agent. Everything works except the production bastion, which disconnects immediately. ssh-add -l shows nine entries; ssh -v shows exactly three Offering public key lines and then the disconnect. The bastion's sshd -T reports maxauthtries 3, set by the hardening role that is applied only to internet-facing hosts — which is why every other server is fine. The engineer adds a four-line Host bastion block with IdentitiesOnly yes and the right IdentityFile. The next ssh -v shows one offer and a successful login, and nothing on the server changed.

Confirm the fix and keep the list short

Re-run with -v and count again:

ssh -v deploy@bastion 2>&1 | grep -cE "Offering public key"
ssh -o BatchMode=yes deploy@bastion true && echo "auth ok"

One offer and auth ok is what a pinned identity looks like. Keep it that way with IdentitiesOnly yes on every Host block you rely on, one key per destination rather than one key reused everywhere, and an occasional ssh-add -l to notice the agent filling up again. If you script against these hosts, BatchMode=yes turns a stalled prompt into an immediate failure, so a broken identity shows up as a failed job rather than a hung one.

How this differs from Permission denied (publickey)

Permission denied (publickey) means the server let you finish: it saw every key you had to offer, none matched authorized_keys, and it ran out of identities rather than attempts. There the fix is on the server — the right public key in the right file, with the right ownership and permissions. Too many authentication failures means the server cut you off mid-list, so the key you care about may never have been sent at all. The Offering count in ssh -v separates them in one command.

Host key verification failed is a different phase entirely. That check runs before authentication and is about the server proving its identity to you against known_hosts, so no key of yours has been offered yet when it fires.

Next time a connection dies before it asks you for anything, walk these checks back in order: read the server's log line for an invalid user prefix, count the Offering lines in ssh -v, compare that count to sshd -T, and then pin the identity.

Related questions

Why does this happen when I have not even typed a password?

Because public-key offers are authentication attempts in their own right. The client offers each public key it holds, the server checks it against authorized_keys and rejects the ones it does not know, and each rejection counts against MaxAuthTries. With the default of 6, an agent holding seven keys can exhaust the budget before any interactive method is reached, which is why the disconnect arrives with no prompt at all.

Should I raise MaxAuthTries on the server instead?

You can, and it is usually the wrong lever. The value caps how many attempts a single connection may make, so raising it widens that window for every client including unwanted ones, and it does nothing about the list of keys your agent is offering. Pinning the identity on the client fixes the cause and leaves the server's posture alone. Raising it is defensible on an internal host where several teams legitimately need multi-factor AuthenticationMethods chains that consume attempts.

IdentitiesOnly=yes is set but ssh still offers the wrong key.

Check the effective configuration rather than the file with ssh -G host, which resolves every Host and Match block for that destination. Two rules explain almost every case: the first value specified for a directive wins, so a Host * block placed above your specific block overrides it; and IdentityFile is the documented exception that adds to the list instead of replacing it, so leftover IdentityFile lines still contribute even with IdentitiesOnly on.

The server log says maximum authentication attempts exceeded for invalid user deploy.

That prefix means the account does not exist on that host, so no key could ever have matched and you were always going to reach the cap. OpenSSH inserts invalid user into the message when the username is not valid. Fix the username — check the User line in your Host block, since ssh otherwise falls back to your local username — before you touch anything about keys or agents.

Ansible or a CI job hits this while my interactive ssh works.

The two runs almost never see the same identity list. A CI runner may have an agent forwarded into it, or a different HOME and therefore a different config file, while your shell has the Host block you added. Reproduce it as that user with ssh -G to print the resolved configuration, then set IdentitiesOnly=yes and an explicit IdentityFile in the automation's own ssh config or in ansible_ssh_common_args so the run does not depend on whatever the agent happens to hold.

References

Haneul Seo

Infrastructure engineer · 10+ years running Linux fleets

More in this category

Start request repeated too quicklyFixed

systemd: Start request repeated too quickly

systemd refuses to start a unit that was started more than StartLimitBurst times (default 5) within StartLimitIntervalSec (default 10s), and Restart= counts against that limit. With the 100 ms default RestartSec a crashing service burns all five attempts in under a second. Find the real crash in the journal, fix it, run reset-failed, and give restarts room with RestartSec.

systemd
1722Fixed

Active Directory: replication fails with error 1722, The RPC server is unavailable

RPC reports 1722 (0x6ba, RPC_S_SERVER_UNAVAILABLE) when a lower layer fails to connect, so the real fault is almost never RPC itself — it is DNS, a blocked port, or a host-side setting on one of the two domain controllers. repadmin tells you which partner is failing, dcdiag /test:dns rules out name resolution, and Test-NetConnection plus the dynamic port range settle the firewall question. The most common miss is a rule that allows TCP 135 but not 49152–65535.

Windows Server Active Directory
Windows Server RDSFixed

Windows Server RDS: The remote session was disconnected because there are no Remote Desktop License Servers available to provide a license

The 120-day RD Licensing grace period ended and the session host has no usable license server, so it refuses sessions. GetGracePeriodDays returning DaysLeft 0 and an empty SpecifiedLSList confirm it in seconds. The fix is a real, activated license server with CALs that are new enough for the host — a 2019 CAL cannot serve a 2022 session host — configured through the deployment or the Licensing policies, plus RPC ports open between the two.

Windows Server RDS
0x80070035 / Event ID 31017Workaround

Windows 11: "Your organization's security policies block unauthenticated guest access" when opening a NAS share (0x80070035)

The SMB client on Windows 10 Enterprise/Education/Pro for Workstations, Windows 11 Pro and Windows Server 2019+ refuses guest logons by default, and Windows 11 24H2 Enterprise/Pro/Education also requires SMB signing, which guest sessions can't do. A NAS share that only offers guest access therefore fails with the 'block unauthenticated guest access' dialog, Error code 0x80070035, or System error 3227320323, and Event ID 31017 'Rejected an insecure guest logon' lands in the SmbClient/Security log. The fix Microsoft recommends is a real account on the NAS and signing support in its firmware; Set-SmbClientConfiguration -EnableInsecureGuestLogons $true (plus -RequireSecuritySignature $false on 24H2) is the escape hatch, and it costs you signing and encryption on that client.

Windows (SMB client)
E: Could not get lock /var/lib/dpkg/lock-frontendFixed

Ubuntu/Debian: E: Could not get lock /var/lib/dpkg/lock-frontend — who holds it and how to wait for it

Another package manager, usually Ubuntu's unattended-upgrades fired by a persistent systemd timer at boot, holds the dpkg frontend lock while your apt-get runs. apt-get gives up at once while apt waits because Ubuntu ships binary::apt::DPkg::Lock::Timeout "120" for the apt binary only. Read the PID from the message, let the run finish or pass -o DPkg::Lock::Timeout=<seconds> to apt-get, run dpkg --configure -a only after a genuinely interrupted run, and never delete the lock file: it is an fcntl lock the kernel releases when the holder exits.

APT (Ubuntu/Debian)
xcrun: error: invalid active developer pathFixed

macOS: xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools)

git, make, clang and other /usr/bin developer commands on macOS are shims that hand off to the active developer directory, and xcrun is reporting that the directory xcode-select points at has no tools in it — most often because a major macOS upgrade left /Library/Developer/CommandLineTools empty, or Xcode was moved or deleted. Check xcode-select -p and the package receipt, then reinstall the Command Line Tools with xcode-select --install, or point xcode-select at the Xcode you actually have.

macOS