OpenSSH server rejecting three terminal connections before authentication.
Last edited on August 4, 2026

kex_exchange_identification: read: Connection reset by peer does not prove an SSH password, key or account problem. When the server log also reports beginning MaxStartups throttling or dropped pre-authentication connections, OpenSSH is refusing some new handshakes because too many connections have not authenticated yet.

Existing administrator sessions can remain responsive during the same event. Authentication removes a connection from the MaxStartups population, so a healthy shell proves only that one client already crossed the gate. Keep that shell open—or secure provider-console access—before investigating. Voxfor’s first-login access checks provide the recovery baseline if keys, firewall state or console details are not already documented.

Read the failure phase before touching any limit

Start with one failed client’s UTC timestamp and verbose output. The exact disconnect wording varies by client and OpenSSH version, but its phase matters:

ssh -vvv -o ConnectTimeout=10 -l admin 203.0.113.10

A TCP refusal suggests no listener or an active reject. A timeout may belong to routing, firewall, provider filtering, conntrack or host pressure. A reset around key exchange can come from SSH admission policy, yet an intermediary may also reset the flow. Only server-side evidence can attribute the drop to sshd.

Preserve a narrow log window on the server:

sudo journalctl -u ssh -u sshd --since '15 minutes ago' --no-pager | grep -Ei 'MaxStartups|preauth|drop connection|throttl'

Some systems use ssh.service, others sshd.service; asking journalctl for both is read-only and one unit may simply have no entries. Log wording and detail depend on the packaged OpenSSH build and log level. Do not enable DEBUG globally during an Internet-facing incident: the upstream manual warns that debug logging can violate user privacy.

Broader admission failure needs a different owner. If fresh HTTPS, database and SSH flows fail together while established traffic survives, follow Linux conntrack capacity evidence instead of raising an SSH-only ceiling.

Four controls answer four different capacity questions

The current OpenSSH daemon manual defines separate controls for separate stages. Mixing them can produce a change that has no effect on the incident.

Directive Counted unit What releases or ends the unit Incident question
MaxStartups Concurrent unauthenticated network connections, globally Successful authentication, disconnect or LoginGraceTime expiry Is the daemon’s pre-auth population full?
PerSourceMaxStartups Unauthenticated connections from one source address or source block Same pre-auth lifecycle, subject to the lower applicable limit Is one NAT, bastion or scanner consuming too much of the gate?
MaxSessions Shell, login or subsystem channels inside one established SSH connection Channel closure Is connection multiplexing opening too many channels?
MaxAuthTries Authentication attempts inside one connection Success, disconnect or attempt limit Is one connection trying too many credentials?

Upstream defaults currently document MaxStartups 10:30:100. At 10 unauthenticated connections, new attempts face a 30% refusal probability; that probability rises linearly until every new attempt is refused at 100. The middle number is a percentage at the start threshold, not a connection count. Distribution packages may carry patches or older capabilities, so the running daemon remains the authority.

MaxSessions belongs after the network connection and authentication path exist. Microsoft Learn’s MaxStartups and MaxSessions comparison makes the practical split explicit: pre-auth connection drops and multiplexed channel failures are different symptoms.

Freeze the effective policy and incident window

Configuration files are clues; the effective daemon output is the receipt. OpenSSH normally uses the first obtained value for a keyword, and distribution include order can make an early snippet win over a later line. Capture files, version, effective values and active service before editing:

sudo sshd -V 2>&1 | head -n 1
sudo grep -RniE '^[[:space:]]*(Include|MaxStartups|PerSourceMaxStartups|PerSourceNetBlockSize|PerSourcePenalties|LoginGraceTime|MaxSessions)[[:space:]]' /etc/ssh/sshd_config /etc/ssh/sshd_config.d 2>/dev/null
sudo sshd -T | grep -E '^(maxstartups|maxsessions|logingracetime|persourcemaxstartups|persourcenetblocksize|persourcepenalties) '
systemctl is-active sshd 2>/dev/null || systemctl is-active ssh 2>/dev/null

sshd -V is not available on every older build; a missing version flag does not invalidate the remaining checks. Likewise, unsupported per-source directives may be absent from sshd -T. Never assume a directive from today’s upstream manual exists on an older LTS or enterprise package.

Pair the policy with the incident’s client timestamps, daemon messages, authentication latency and source distribution. ss -nt can show TCP endpoints, but it cannot tell you which connection still occupies an unauthenticated OpenSSH slot. Use it as corroborating network evidence, not as the MaxStartups count:

sudo ss -Hnt state established '( sport = :22 )'
sudo journalctl -u ssh -u sshd --since '15 minutes ago' --no-pager

Avoid an improvised connection flood against production. Reproduce concurrency only in staging or an approved maintenance window, with provider console access and a stop condition below the tested host’s resource boundary.

Is the demand global, per source, or outside SSH?

Source shape changes the safe response.

Evidence shape Likely interpretation Safer next move
Many source addresses, low completion, repeated pre-auth drops Scan or hostile connection pressure Reduce public exposure or filter upstream; do not normalize attack volume into daemon capacity
One public address, many known employees or build workers Shared NAT, bastion or CI fan-out Measure legitimate peak and avoid an undersized per-source ceiling
Distributed known automation at one schedule boundary Deployment or orchestration burst Stagger jobs, reuse multiplexed connections where appropriate, or admit a measured global peak
SSH-only drops with explicit daemon throttling logs MaxStartups gate is active Change the owning global/per-source contract after capacity review
Several new services fail without daemon throttling evidence Host/network admission problem Investigate firewall, conntrack, listener backlog, provider path and resource pressure

Source address is not user identity. An office, carrier-grade NAT, jump host or CI egress gateway may represent dozens of authorized clients. Conversely, a distributed scan can consume global slots without any one address reaching a per-source threshold. Count successful authentications and known jobs, not just addresses.

Private administration can remove much unsolicited Internet demand. When that access model fits the workload, Tailscale SSH controls provide a separate path for identity and network policy. Do not close public SSH until the private path and provider recovery console are tested from a fresh client.

Choose an admission envelope instead of a magic number

Start from the largest legitimate pre-auth burst observed across human logins, CI, SFTP and orchestration. Authentication duration matters: 20 arrivals that authenticate in one second consume a different concurrent population from 20 arrivals stalled on a slow external identity provider for 30 seconds.

Define three evidence-backed boundaries:

  1. start: the point above normal legitimate concurrency where random early refusal may begin.
  2. rate: the initial refusal probability that protects capacity without making ordinary bursts unreliable.
  3. full: the maximum unauthenticated population the host can defend while preserving CPU, memory, file descriptors and recovery access.

The upstream syntax is start:rate:full; values must satisfy the running daemon’s parser. A worked policy might read 24:30:72, but those numbers are not a recommendation. They are acceptable only if measured legitimate concurrency fits below the first threshold and a controlled test proves 72 pre-auth workers cannot starve the host.

PerSourceMaxStartups can preserve global room against one address, where supported. Before using it, inventory shared NAT and bastion fan-out. The upstream default is no per-source limit, and the lower of the per-source and global policies applies. Newer PerSourcePenalties behavior is also version-dependent; consult the release notes and local man page rather than adding it as an automatic hardening line.

Lowering LoginGraceTime frees abandoned slots sooner, but slow links, hardware-token interaction, PAM, multi-factor prompts and external identity lookups may need real time. Measure successful authentication duration before shortening it. A zero value removes the time limit and can make slot residence unbounded, so it is not an incident shortcut.

Apply one reversible change without losing the return path

Keep the known-good SSH session open and confirm provider console access. Identify the file that owns the effective value; do not append a conflicting line blindly. Back up that file with permissions preserved, change one policy, and record the exact old value.

For an environment whose measured contract approved the worked values, a dedicated snippet could contain:

# Example only: replace with measured and approved values.
MaxStartups 24:30:72
PerSourceMaxStartups 16
LoginGraceTime 60

If PerSourceMaxStartups is unsupported or shared-NAT evidence does not justify 16, omit it. Validate the complete configuration before reload:

sudo sshd -t
sudo sshd -T | grep -E '^(maxstartups|persourcemaxstartups|logingracetime|maxsessions) '

Any syntax error is a stop condition. Reload only the unit already active on that host—commonly ssh on Debian/Ubuntu or sshd on RHEL-family systems. A reload preserves established sessions; a restart adds avoidable lockout risk. Ubuntu’s OpenSSH server guidance is the distribution reference for snippet and service behavior.

From a second terminal and the real client network, open a fresh login and verify the host key before closing the recovery session. The same fresh-client discipline used during SSH host-key rotation matters here because an old shell cannot prove new admission.

Rollback restores the previous owner file, passes sshd -t, reloads the same active unit and repeats a fresh login. If the host becomes pressured during a controlled concurrency test, stop the generator first; do not keep increasing the ceiling.

Prove human access and automation separately

One successful login is necessary but not enough. Test a normal administrator, one representative SFTP or CI job, and the highest approved legitimate burst through the real network path. Preserve:

  • fresh-login success rate and authentication duration;
  • daemon throttle/drop messages in the same UTC window;
  • source-address distribution and known NAT ownership;
  • CPU, memory, file-descriptor and load behavior;
  • automation completion without duplicate deployment or transfer effects;
  • the previous policy and rollback receipt.

Acceptance means legitimate fresh clients authenticate reliably while the host retains a defended pre-auth boundary. A quiet log can be misleading if clients stopped trying, and a larger ceiling can hide the symptom while resource exposure worsens.

When the operating team cannot own log review, access-policy changes and recovery testing, the decision belongs in the broader managed-versus-unmanaged responsibility model. That link is a support-ownership decision, not a substitute for the incident evidence.

FAQ: OpenSSH pre-authentication capacity

What does beginning MaxStartups throttling mean?

beginning MaxStartups throttling means the OpenSSH daemon reached the configured point where it begins refusing a percentage of new unauthenticated connections. Confirm the effective MaxStartups value and incident-time daemon logs before changing it.

Is MaxStartups the total number of SSH users or sessions?

No. MaxStartups counts concurrent unauthenticated network connections. Successful authentication removes a connection from that population; established shell, login and SFTP channels are governed by other controls.

Why can an existing SSH session work while every new login fails?

An established session has already passed authentication and no longer consumes a MaxStartups slot. New clients still need admission through the pre-authentication gate and may be probabilistically or completely refused.

Can a shared NAT cause valid users to hit PerSourceMaxStartups?

Yes. One office, bastion, carrier NAT or CI egress address can represent many authorized clients. Measure legitimate fan-out before enabling or lowering a per-source limit, and retain a tested recovery path.

Should I raise MaxStartups during an SSH brute-force wave?

Do not raise MaxStartups merely to accommodate hostile demand. Reduce exposure or filter the attack at an appropriate upstream layer first; increase capacity only when measured legitimate concurrency and host-resource tests justify it.

Does MaxSessions fix a key-exchange reset before login?

Usually no. MaxSessions limits shell, login or subsystem channels multiplexed inside one established connection. A reset before authentication belongs to the pre-auth or network path, where MaxStartups may be relevant.

Is reducing LoginGraceTime always safe?

No. A shorter LoginGraceTime releases abandoned pre-auth slots sooner, but it can reject legitimate clients using slow links, hardware tokens, PAM, multi-factor authentication or external identity services. Measure successful authentication time first.

Close with the gate’s own evidence

Record the failed client timestamp, daemon throttle message, effective global and per-source policy, legitimate concurrency shape, changed owner file, syntax result, fresh human login and representative automation outcome. That evidence identifies whether OpenSSH protected a saturated pre-auth gate or merely appeared beside a different network failure. The correct repair preserves both admission reliability and a resource boundary the host can defend.

Leave a Reply

Your email address will not be published. Required fields are marked *