Tunnel services that promise one device per subscriber rarely get that promise from MaxSessions, and the first test usually proves it. The subscriber opens a second ssh process, the first session keeps running, and both tunnels carry traffic. Each configuration option counts a different thing, and none of them counts devices. Naming the promise first is what makes the choice between them obvious.

What each option actually counts

MaxSessions limits the number of open sessions carried by one authenticated network connection. A session here means a shell, an exec request, a subsystem such as SFTP, or a similar channel multiplexed inside a single SSH transport. A second ssh invocation opens a second transport, authenticates again and gets its own allowance. The first connection's session count is irrelevant to it.

MaxStartups limits concurrent connections that have not authenticated yet, expressed as a start threshold, a rate and a full threshold. It exists so a burst of half-open handshakes cannot exhaust the daemon's pre-authentication resources. Once a client authenticates, that connection stops counting against MaxStartups and becomes somebody's session. It was never a per-user quota, and raising it does not let a subscriber do more.

A device registration rule is not an sshd option at all. It belongs to whichever system issues credentials and tracks which device holds them, which means it needs enrolment, inventory and revocation rather than a counter. Mixing that requirement into sshd configuration is where most of these designs stall.

Where MaxSessions stops being a quota

A Match User block changes which configuration values apply to that user, so it is the natural place to set a tighter MaxSessions for a service account. What it cannot do is aggregate across connections, because the option is evaluated per transport. Two transports each get their own limit, and the user ends up with twice whatever the block allows.

Forwarding is a separate surprise. TCP forwarding does not need a session channel, so MaxSessions 0 can prohibit shells and subsystems while still permitting -L and -R tunnels. A forward-only service that treats the session count as its tunnel quota is measuring nothing at all. That behaviour is documented in the OpenSSH server configuration reference, and it is worth reading before designing around an option name.

Dump the effective configuration rather than the file you think is loaded. Includes and drop-in directories routinely mean the value in effect is not the value you edited.

MaxStartups protects the daemon, not the subscriber

The three-part form of MaxStartups describes how many unauthenticated connections may exist before the daemon starts dropping a percentage of new ones. Its default shape exists because an unauthenticated SSH connection is cheap to open and expensive to keep around. That is a resource-protection control for the server, and it applies to everybody at once.

Raising it during a login storm trades daemon protection for admission, which is sometimes the right trade and always a deliberate one. It does nothing for the subscriber who wants a second tunnel, because the first tunnel is already authenticated. Confusing the two leads to a change that fixes the wrong symptom and weakens the server at the same time.

When logins are refused in bursts, look at the daemon log for the pre-authentication message before you touch either option. The message names which guard fired, and the guards are not interchangeable.

A per-user slot needs shared state

A genuine one-active-transport policy needs a reservation held somewhere outside sshd. The admission step has to atomically claim the account's slot, release it when the connection closes, expire it when a client disappears without closing, and decide whether a new attempt is rejected or replaces the old one. That last decision is a product decision, not a technical detail, because replacing an active tunnel disconnects somebody.

The reservation also has to be shared by every server that accepts the account. Two hosts with independent local counters enforce two separate limits, and a subscriber who reconnects through the other gateway gets a second slot for free. Whatever stores the state needs its own failure behaviour, since an unreachable store must not lock every subscriber out.

For a one-device promise, issue a distinct credential per enrolled device and control registration and revocation at that layer. A copied private key still authenticates from anywhere, so issuing one key per user is not evidence about how many machines hold it.

Check the policy with two real connections

Write the acceptance tests as commands before you write the policy. The five cases are a second transport opened while the first is active, multiple channels inside one transport, a client that only forwards ports, a reconnect after a dropped link, and the same account arriving at another gateway.

Read the effective limits first, then open the connections the policy is supposed to refuse. Both halves of that test matter, and neither one is a substitute for the other.

sshd -T | grep -iE "maxsessions|maxstartups|allowtcpforwarding|permitopen"
ss -tn state established '( sport = :22 )' | head
ssh -N -L 8080:127.0.0.1:80 [email protected]

Run the tunnel command twice in two terminals and count the established connections on port 22. If both carry traffic while your policy claims one active transport, the policy is documentation rather than enforcement. Fix that gap in the admission layer, not by lowering MaxSessions and hoping the second transport notices.

مۇناسىۋەتلىك ماقالىلەر