SSH Protocol

On this page

SSH breaks down "remote operations on untrusted networks" into three problems: which host you are connecting to, which user the remote side accepts, and what features the authenticated user is allowed to open. Understanding these three boundaries is more helpful for distinguishing between Host key verification failed, Permission denied, and forwarded connections being refused than memorizing a key exchange flow.

LayerWork CompletedWhat Is Still Not Guaranteed After Success
TransportAlgorithm negotiation, host authentication, encryption and integrity, key exchangeThe user can log in
User AuthenticationProves user identity and satisfies authentication policiesAll session features are authorized
ConnectionMultiplexed channels, flow control, session and forwarding requestsApplication operations are necessarily completed or persisted
SSH's Three Boundaries: Trusted Host, Authorized User, Authorized Features Transport: Negotiate algorithms, exchange keys, verify host Signature proves possession of the private key; known_hosts / host CA determines trust User Authentication: Prove user identity Authentication methods are selected by both parties' policies; may require multiple methods to complete Connection: Open channels and request specific features Successful login does not guarantee shell, SFTP, or port forwarding are all permitted The H from the initial exchange is fixed as the session ID; subsequent rekeys update transport keys without resetting identity binding.

Key Exchange: Shared Secret, Exchange Hash, and Host Key

SSH typically establishes a TCP connection first, then sends SSH-2.0-SoftwareIdentifier followed by a CRLF terminator. The version line is not encrypted content; subsequently, KEXINIT is exchanged via binary packets to negotiate key exchange, host keys, and transport algorithms for both directions. The intersection and order of algorithm lists affect the result; you cannot treat the default values of a single OpenSSH version as the only algorithms for the SSH protocol.

Using ECDH-based exchange as an example: the client and server each generate temporary keys and jointly compute the shared secret K; the exchange hash H covers specified fields such as version information, both parties' KEXINIT, the server's host public key, temporary exchange values, and K. The server signs H with its host private key, and the client verifies the signature, thereby binding the negotiation process to this host key. H is not simply a rename for the shared secret, nor is it used directly as the traffic encryption key.

Only the H from the first key exchange is fixed as the session ID. Subsequent rekeys produce new K, H, and transport keys, while the session ID remains unchanged. Both parties send NEWKEYS, switching keys in their respective send and receive directions; do not interpret this as a global switch that changes both directions simultaneously. RFC 4253 §7–9

Modern implementations also support hybrid post-quantum key exchange. For example, OpenSSH 10.0 sets mlkem768x25519-sha256 as the default preference; this is a change in key exchange and does not mean host authentication automatically becomes the same algorithm. On the other hand, disabling ssh-rsa signatures usually refers to disabling RSA/SHA-1, not that all RSA public keys become invalid; the same RSA key can use RSA/SHA-2 signatures, provided both sides support and allow it. OpenSSH Release Notes

Why Check known_hosts After Signature Verification Passes?

An attacker can also generate their own keys and correctly sign their own exchange. Signature verification only proves that the other party holds the private key corresponding to the provided public key; to confirm "this is the host I want to connect to," you still need to bind the hostname to a trusted key or trusted issuer.

Trust SourceHow Binding Is EstablishedContent to Maintain
Pre-installed Host Public KeyWrite public key to known_hosts via trusted channelHost changes, key rotation, revocation
Trust on First Use (TOFU)Save after first verification or acceptance, compare subsequentlyCredibility of the first accepted key
OpenSSH Host CertificatesTrust CA for matching host ranges, verify certificate constraintsCA, principal, validity period, and revocation
DNS SSHFPUse fingerprints in DNS when configuration and verification conditions are metDNSSEC validation chain and record maintenance

Therefore, initial trust can be completed automatically via configuration distribution or host CA; it is not "the only step in SSH that cannot be automated." ssh-keyscan only collects public keys obtained from the network; using it alone does not constitute trusted verification; clearing old records also does not explain a host key change. ssh Host Authentication, VerifyHostKeyDNS

OpenSSH certificates are SSH's own certificate format, not TLS X.509 certificates. Host certificates answer the server identity question, while user certificates answer the login identity question; trusting a host CA does not mean that any user signed by this CA gains login permissions.

User Authentication: Signature Requests and "Partial Success"

Data signed by publickey authentication includes the session ID and specified user authentication request fields, binding the proof to the current connection and requested identity. The client can first ask the server if it accepts a certain public key, then send the signature; receiving SSH_MSG_USERAUTH_PK_OK only acknowledges the candidate, not that login is complete. Complete success is indicated by SSH_MSG_USERAUTH_SUCCESS. RFC 4252 §5, 7

Method or MechanismActual Function
publickeyVerifies control over the corresponding private key, while checking authorization rules for that user
passwordSubmits password within the protected SSH transport; the server still accesses the password
keyboard-interactiveGeneral challenge/response, can carry OTP, PAM, etc.
User CertificatesAdds CA signature, principal, validity period, and constraints to the public key, still requiring private key proof

TOTP is not a directly named method in the base SSH user authentication protocol. The order of authentication attempts is also not "publickey, OTP, password" as specified by the protocol; client preferences, server-allowed methods, and multi-factor policies jointly determine the process. The server can report that a method has partially succeeded but another method is still needed; this is different from a complete failure caused by an incorrect private key.

OpenSSH's AuthenticationMethods publickey,keyboard-interactive can express a two-step policy, but whether the second step is TOTP depends on the server's interactive authentication configuration. Keys in authorized_keys can also come with command or forwarding restrictions; possessing an accepted key does not equate to gaining an unrestricted shell. sshd_config

Binary Packets: Boundaries First, Then Messages

The base transport packet includes 4 B packet_length, 1 B padding_length, payload, random padding, and integrity data processed according to the algorithm definition. packet_length does not include its own 4 B, nor does it include the MAC at the end of the traditional format; padding is at least 4 B. Do not mechanically apply this base description to the encryption scope of all AEAD or Encrypt-then-MAC algorithms.

Below is a pedagogical SSH_MSG_IGNORE packet constructed for the initial unencrypted phase: message type 1 B, string length 4 B, hello 5 B, totaling 10 B payload. Aligned to 8 B, the 5 B header plus 10 B payload requires 9 B padding; the complete packet is 24 B, with packet_length being 20.

import struct
payload = bytes([2]) + struct.pack('>I', 5) + b'hello'
padding = 4
while (5 + len(payload) + padding) % 8:
    padding += 1
# Fixed bytes are only for reproducible encoding demonstration; real sends use required random padding.
packet = struct.pack('>IB', 1 + len(payload) + padding, padding)
packet += payload + bytes(range(padding))
assert len(payload) == 10 and padding == 9
assert len(packet) == 24
assert struct.unpack('>I', packet[:4])[0] == 20
assert packet[5] == 2 and packet[10:15] == b'hello'
print('Base SSH packet: payload 10 B, padding 9 B, total length 24 B')

A single TCP read may only capture part of a packet, or it may contain multiple packets. The receiver must accumulate and verify based on length, rather than judging the number of SSH messages by the number of socket reads. RFC 4253 §6

Channels: Independent Numbers and Windows, Shared TCP

A session channel can request a shell, exec, or subsystem; SFTP is an application protocol within a subsystem. Port forwarding commonly uses direct-tcpip or forwarded-tcpip. This is not a fixed numbering table where "channel 0 is always shell, channel 1 is always forwarding."

Both sides allocate local channel IDs independently. If the client opens using 7, and the server confirms and allocates 42, then subsequent data from the client to the server uses recipient 42, and data from the server back to the client uses recipient 7. Numbers are only meaningful within the respective connection and endpoint.

Each direction has a receive window and a maximum channel packet size; the amount sent is limited by both. After the receiver processes and frees the buffer, it uses WINDOW_ADJUST to increase the other party's send quota; stdout and extended data like stderr must also comply with the channel window and cannot accumulate infinitely.

window, max_packet, queued = 65536, 32768, 90000
sent = []
while window and queued:
    n = min(window, max_packet, queued)
    sent.append(n)
    window -= n
    queued -= n
assert sent == [32768, 32768] and queued == 24464
assert window == 0  # At this point, you must wait for the window in this direction to replenish.
window += 16384
n = min(window, max_packet, queued)
assert n == 16384 and queued - n == 8080
print('Window exhausted, paused; WINDOW_ADJUST only restores the corresponding quota')

Independent windows prevent a slow channel from eating up the application buffers of other channels, but they do not eliminate head-of-line blocking caused by underlying TCP packet loss. CHANNEL_EOF indicates that no more data will be sent in that direction, while the reverse direction can continue; only CHANNEL_CLOSE participates in closing the entire channel. The command's exit-status is a different type of channel request and cannot be replaced by "normal TCP closure" for business exit codes. RFC 4254 §5–6

Sent to 42, Why Does the Reply Write 7?

When the direction arrow changes, the recipient takes the local number saved by the peer. Increasing the window does not break the 32768 single-packet limit; reducing the queued data does not require filling the window. These three quantities have different managers and lifecycles.

Preparing the visual
Sent to 42, Why Does the Reply Write 7?

Channel numbers are allocated independently by both ends; sending uses the peer's number. Single data volume is limited by direction window, max packet, and queued amount.

Forwarding: Who Listens, Who Resolves the Target, Where Encryption Ends

Port Forwarding: L / R refers to listen location, target connected by the other end -L 127.0.0.1:8080:internal.example:80 SSH Client Client listens on :8080 SSH Server Server connects to internal:80 SSH Encrypted Segment -R 127.0.0.1:3000:localhost:22 SSH Client Client connects to localhost:22 SSH Server Server listens on :3000 SSH Encrypted Segment Arrows indicate the initiation direction of the forwarded connection; the underlying SSH connection is still from client to server.

The following two are command examples for on-demand use; gateway.example and internal.example need to be replaced with your own hosts.

# Access from local 8080, connect to internal.example:80 via gateway.
ssh -N -o ExitOnForwardFailure=yes \
  -L 127.0.0.1:8080:internal.example:80 user@gateway.example

# Access from gateway's loopback 3000, connect to local 22 via SSH client.
ssh -N -o ExitOnForwardFailure=yes \
  -R 127.0.0.1:3000:localhost:22 user@gateway.example

The target name in -L is usually resolved and connected by the SSH server; localhost in the above -R is on the SSH client side. The listen address limits who can connect, but does not determine which side the target is on. Whether remote listening can bind to non-loopback addresses is also affected by server policies such as GatewayPorts.

SSH encryption terminates at both ends of SSH; the segment where the server then connects to internal.example:80 does not automatically gain SSH encryption. If the target runs HTTPS, application TLS can continue to protect traffic to the target service. -N does not request remote commands; ExitOnForwardFailure can detect failures in establishing listening or forwarding, but does not guarantee success for every future connection to the target. ssh Forwarding Options

RequirementMechanismBoundary
Fixed TCP Service-L / -RMaps connections, not a full IP VPN
Application Selects Multiple TCP Targets-D Dynamic SOCKSApplication must use proxy; not general UDP forwarding
Login to Target Host via Jump Host-J ProxyJumpStill requires verifying target host identity
Remote Calls Local Signing AgentAgent ForwardingPrivate keys are not directly copied, but the remote side can request signatures within authorized scope

ProxyJump usually does not require forwarding the agent to the jump host: the client can establish its own SSH connection to the final target via the TCP channel forwarded by the jump host. Agent forwarding serves a different purpose and cannot be treated as "private key files didn't leave the local machine, so the remote side cannot use the identity." ssh Agent and Jump Host

Locate by Phase, Instead of Uniformly Attributing "Wrong Key"

PhenomenonPriority Check
Cannot connect TCPAddress, port, routing, service listening, and connection policies
No common algorithmsError belongs to KEX, host signature, or transport algorithm category
Host verification failedTarget name, port mapping, trusted records, and planned key rotation
User authentication failedLogin username, candidate identities, server methods, and authorization constraints
Features denied after loginSubsystem, command restrictions, forwarding policies, and target connections
Transmission stuck or connection interruptedChannel backpressure, TCP packet loss, idle state, and timeouts

When you only want to check command parsing, you can use ssh -G -F /dev/null to output effective options without reading personal configuration; -G does not establish an SSH session. For actual connection debugging, use -v or -vvv to compare against the failure phase. Keep-alives are used to discover unresponsive connections, not as proof that business processing is complete; after reconnecting, do not assume that commands started previously were not executed.