Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

SSH is both an everyday command and a family of protocols. This course takes the second view: we will follow an SSH client from a newly opened byte stream to an authenticated, multiplexed connection capable of running a command.

The goal is not to design new cryptography or build a client from scratch. It is to understand what the client and server say, why each exchange exists, and where the protocol’s security claims begin and end.

The three protocol layers

SSH builds upward from a reliable, ordered byte stream. Each layer gives the next one a narrower and safer abstraction.

flowchart BT
    TCP["Reliable byte stream<br/>usually TCP port 22"] --> Transport
    Transport["Transport protocol<br/>key exchange · server identity · protected packets"] --> Auth
    Auth["User authentication<br/>public key · password · interactive methods"] --> Connection
    Connection["Connection protocol<br/>multiplexed, flow-controlled channels"] --> Uses
    Uses["Shell · command · subsystem · TCP forwarding"]

The separation matters. Encrypting bytes does not prove which server received them. Proving the server’s identity does not prove which user is connecting. Authenticating a user does not explain how several terminal and forwarding streams share one connection. SSH assigns each job to a different layer.

The foundational specification is RFC 4251, the SSH protocol architecture. Its companion documents define the transport, user authentication, and connection protocols.

One connection at a glance

sequenceDiagram
    participant App as Client application
    participant C as SSH client protocol
    participant S as SSH server
    App->>C: connect(host, user, command)
    C->>S: Open reliable byte stream
    C<<->>S: Identification lines
    C<<->>S: KEXINIT negotiation
    C<<->>S: Ephemeral key exchange
    S-->>C: Host key and signature
    C->>C: Verify server identity
    C<<->>S: NEWKEYS
    Note over C,S: Packets are now protected
    C<<->>S: Authenticate user
    C<<->>S: Open session channel
    C->>S: Request exec "command"
    C<<->>S: stdout, stderr, exit status
    C<<->>S: EOF and CLOSE
    C-->>App: command result

Key exchange supplies a signed host-key proof. The client combines that proof with a destination-to-key trust check before it sends user credentials. The first exchange also creates a stable session identifier. Later public-key user authentication signs data containing that identifier, binding the login proof to this SSH connection.

What you will learn

By the end of the course, you should be able to:

  • trace identification, negotiation, key exchange, user authentication, and channel traffic in order;
  • distinguish host keys, user keys, ephemeral key-exchange values, and derived traffic keys;
  • read SSH’s binary types and packet framing at the level needed to follow a protocol trace;
  • explain what the client must verify before accepting a server;
  • reason about how channel flow control keeps multiplexed streams independent; and
  • diagnose failures by protocol phase instead of guessing from a final error.

How to use the course

The chapters alternate between the wire view and the client’s view of the conversation. Diagrams omit fields that do not matter to the point being explained; the linked specification remains authoritative. Names such as string and uint32 refer to SSH wire types, not necessarily to types in a programming language.

You should be comfortable with bytes, hexadecimal notation, network sockets, and basic client/server programming. No cryptography background is assumed. The cryptography primer treats each primitive as a black box with explicit inputs, outputs, and required checks.

Security boundary: The sketches in this course explain the protocol; they are not recipes for implementing cryptography. Never “temporarily” accept an unknown or changed host key to get past a connection error.

Unless a chapter says otherwise, protected transport means that safe client policy selected confidentiality and integrity. SSH also registers none algorithms for unusual configurations; this course does not treat them as safe for credentials or sensitive channel data.

The learning path

We first trace a deliberately narrow case: a direct connection which verifies a host key, authenticates with a public key, executes one command, receives its output and exit status, and closes. Interactive terminals, agents, proxies, SFTP, and forwarding become easier to understand once that core exchange is clear.

A note on old and new SSH

The 2006 core RFCs describe SSH version 2, but algorithm advice evolves. Do not copy their original mandatory algorithm set into a new client. RFC 9142 updates key-exchange recommendations, while the IANA SSH protocol registries record assigned names. Later chapters call out extensions and newer algorithms where they change the protocol model.

Observe a connection

Before studying the packet format, watch a mature client perform the protocol. OpenSSH’s debug log uses implementation terminology, but its order closely follows the wire exchange.

A safe observation lab

Use a test server whose host-key fingerprint you can verify independently. The commands below do not weaken host verification and do not put a password on the command line.

First inspect the effective configuration without connecting:

ssh -G example.test | less

Then connect with verbose logging and run a harmless command:

ssh -vvv user@example.test 'printf "hello from the server\n"'

The options are documented by the OpenBSD ssh(1) manual. Debug output can contain hostnames, usernames, paths, key fingerprints, and command text. Review it before sharing it.

If the client asks whether to trust a new host key, pause. Compare the shown fingerprint with a value obtained through a separate trusted channel. The prompt is a security decision, not connection boilerplate.

Turn the log into phases

Exact messages vary by client version and server configuration. Instead of matching every log line, classify the evidence.

flowchart TD
    A["TCP connected"] --> B["Local and remote<br/>identification strings"]
    B --> C["Algorithm proposals"]
    C --> D["Chosen KEX, host key,<br/>cipher and integrity"]
    D --> E["Server host-key fingerprint"]
    E --> F{"Host trusted?"}
    F -- no --> X["Stop"]
    F -- yes --> G["New traffic keys active"]
    G --> H["Authentication methods"]
    H --> I["Authentication succeeded"]
    I --> J["Session channel opened"]
    J --> K["Command and exit status"]

Build a small table from your own log:

QuestionEvidence to find
Which protocol/software versions met?local and remote version strings
Which key exchange was selected?negotiated KEX name
Which long-term server key signed it?host-key algorithm and fingerprint
How are packets protected each way?cipher and MAC, or an AEAD cipher
How was the user authenticated?offered and accepted method
What application stream was opened?session channel and exec request
How did the result finish?EOF, exit status, and channel close

Do not expect debug lines to be protocol packets one-for-one. A client may log one decision after processing several packets, and one SSH packet may trigger several log entries.

A compact annotated trace

The following synthetic excerpt uses OpenSSH-like wording. It is not a literal wire dump, and real versions include more detail.

Connection established.
Local version string SSH-2.0-OpenSSH_X
Remote protocol version 2.0, remote software version OpenSSH_Y
kex: algorithm: curve25519-sha256
kex: host key algorithm: ssh-ed25519
Server host key: ssh-ed25519 SHA256:example
Host 'example.test' is known and matches the ED25519 host key.
kex: server->client cipher: ...
kex: client->server cipher: ...
Authentications that can continue: publickey,password
Offering public key: ...
Authenticated to example.test using "publickey".
Entering interactive session.
Sending command: printf "hello from the server\n"
Exit status 0

Read it as evidence, not as a packet list:

Log evidenceProtocol conclusion
Both version stringsIdentification exchange completed.
KEX and host-key namesBoth KEXINIT proposals had a usable match.
Host key matchesSignature proof and local trust policy both passed.
Directional cipher linesNew packet-protection algorithms were selected.
Methods can continueThe server rejected or answered an authentication request and supplied current policy choices.
AuthenticatedThe client received SSH_MSG_USERAUTH_SUCCESS.
Command and exit statusA session channel accepted exec and later reported status 0.

The log does not show every NEWKEYS, channel-window update, EOF, or CLOSE. Absence from the log is not absence from the protocol.

Inspect supported algorithms

OpenSSH can list locally supported algorithm names without connecting:

ssh -Q kex
ssh -Q key
ssh -Q cipher
ssh -Q mac

“Supported” is not the same as “enabled by default,” “negotiated,” or “safe for new deployments.” This distinction becomes central during KEXINIT negotiation.

Keep four sets distinct: supported, enabled by policy, offered to the peer, and selected for this connection.

What a packet capture can show

A capture can show the TCP connection, both textual identification lines, the initial unencrypted SSH packets, packet sizes, timings, and connection closure. After SSH_MSG_NEWKEYS, it cannot reveal protected message contents without the traffic keys.

timeline
    title Visibility to a passive packet capture
    TCP setup : addresses and ports
    Identification : readable protocol lines
    Initial key exchange : framing and public handshake data
    NEWKEYS : protection changes direction by direction
    Protected session : sizes and timing, not plaintext contents

The remaining metadata is still sensitive. Packet lengths and timing can leak behavioral clues, which is one reason the transport includes random padding. Padding reduces some obvious structure; it does not make all traffic patterns invisible. See the traffic-analysis discussion in RFC 4251 section 9.3.9.

Checkpoint

Given an ssh -vvv log, you should now be able to mark these boundaries:

  1. the reliable stream exists;
  2. algorithm negotiation begins;
  3. the server’s identity is accepted;
  4. packet protection becomes active;
  5. the user is authenticated; and
  6. a connection-protocol channel carries the command.

If a connection fails, name the last boundary crossed. That single habit turns “SSH is broken” into a much smaller investigation.

The client’s view of protocol state

SSH is easiest to understand as a conversation whose vocabulary changes over time. The same byte value can mean different things in different phases, and a message that is valid later may be a protocol error now.

Layers are also a sequence

The protocol layers are nested, but the client encounters them in a definite order. Transport stays active underneath authentication and connection traffic; it does not disappear when the next layer begins.

stateDiagram-v2
    [*] --> Identification
    Identification --> KeyExchange
    KeyExchange --> ServerVerified
    ServerVerified --> UserAuthentication
    UserAuthentication --> ConnectionProtocol
    ConnectionProtocol --> Closed
    ConnectionProtocol --> Rekeying
    Rekeying --> ConnectionProtocol

This produces three important boundaries:

  • server authentication is completed by the transport before user credentials are sent;
  • user authentication succeeds before connection channels are used; and
  • transport rekeying can occur later without repeating user authentication or discarding open channels.

Client and server roles are asymmetric

Both sides propose algorithms and contribute ephemeral key material, but their responsibilities are not mirror images.

QuestionClient’s roleServer’s role
Who starts the network connection?initiateslistens and accepts
Whose algorithm order wins?supplies preference ordersupplies compatible set
Who proves a host identity?verifies proof and trustsigns with host key
Who proves a user identity?supplies authentication proofapplies account policy
Who may open channels?either peereither peer

“Client” and “server” keep these meanings across a rekey. The peer which sends the first new SSH_MSG_KEXINIT has initiated that rekey, but it has not become the SSH client.

State is directional

SSH protects client-to-server and server-to-client traffic separately. Each direction has its own cipher state, integrity state, packet sequence number, and NEWKEYS transition.

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: NEWKEYS under old client-to-server keys
    Note left of C: New sending keys are active
    Note right of S: New receiving keys are active
    S->>C: NEWKEYS under old server-to-client keys
    Note right of S: New sending keys are active
    Note left of C: New receiving keys are active

For the initial exchange, “old keys” means no packet protection. During a rekey, it means the keys which were already carrying the authenticated session. The two NEWKEYS packets may cross in flight, so there can briefly be different epochs in the two directions.

Channel state is directional too. Each side advertises how much data it will receive, and SSH_MSG_CHANNEL_EOF closes only the sender’s data direction. This recurring two-direction pattern explains many otherwise surprising rules.

Several state machines coexist

Once user authentication succeeds, one transport can contain multiple channel conversations while transport-level events still occur.

flowchart TB
    T["Transport state<br/>keys · rekey · disconnect"] --> A["Authentication state<br/>method · partial success"]
    T --> C["Connection state<br/>global requests"]
    C --> C0["Channel 0<br/>open · windows · EOF · close"]
    C --> C1["Channel 1<br/>open · windows · EOF · close"]
    C --> C2["Channel 2<br/>open · windows · EOF · close"]

A zero window on channel 1 does not freeze channel 2. Closing channel 0 does not end the transport. Rekeying changes packet protection beneath all of them but does not create new logical channels.

The current conversation supplies context for message numbers. For example, message number 60 can mean a public-key query acceptance, a password-change request, or an interactive prompt. The active authentication method removes the ambiguity.

Four distinct identities

An SSH trace mentions several names and keys which answer different questions.

The network address tells the client where bytes were delivered. The host-key trust decision tells it which server received them. User authentication asks whether the client may act as an account on that server. A channel request then asks what program or forwarding operation that authenticated connection may start. Success at one boundary does not imply success at the next.

Follow the unit currently in motion

SSH changes framing and data units as the conversation progresses:

The units narrow in this order: reliable-stream bytes, identification lines, SSH packets, message payloads, channel data, and application byte streams.

An identification line is not a binary packet. A packet is not the same as a TCP segment. A channel data message can carry only part of an application’s stream, and its boundary normally has no meaning to that application.

When reading a trace, annotate every item with its unit, direction, layer, and current state. That is often enough to explain why a length, number, or close event behaves the way it does.

Protocol review

Before moving into packet details, make sure you can explain:

  1. why the transport layer remains active after user authentication starts;
  2. why sending and receiving can switch keys at different moments;
  3. why rekeying does not authenticate the user again;
  4. why a channel number has meaning only to the peer which chose it; and
  5. why message number 60 cannot be decoded without authentication-method state.

Identification exchange

SSH starts with one line of text in each direction. After an endpoint sends its identification line, it may begin sending binary packets without waiting for the peer’s line. The text-to-binary boundary is therefore directional.

The exchange answers a narrow question: can the endpoints continue with SSH version 2? It also records implementation identifiers that later become part of the authenticated key-exchange transcript.

The identification line

The line has this form:

SSH-protoversion-softwareversion SP comments CR LF

For example:

SSH-2.0-CourseClient_0.1<CR><LF>
SSH-2.0-ExampleServer_4.2 staging<CR><LF>

The comment and its preceding space are optional. The complete line is at most 255 characters including CRLF and must not contain NUL. Version 2.0 selects the protocol described by the SSH version 2 RFCs. The historical value 1.99 also advertises version 2 compatibility; it is not a separate protocol version.

The peers may send their lines without waiting for each other. The exchange has no request-response order.

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: Open byte stream
    par Client direction
        C->>S: Client identification line
        C->>S: First client binary packet
    and Server direction
        S->>C: Server identification line
        S->>C: First server binary packet
    end

This course uses two related terms:

  • identification line: the transmitted bytes, including the line ending;
  • identification string: the value placed in the exchange hash, without the line ending.

Keeping the distinction avoids hashing a reconstructed or normalized value.

Server lines before SSH

A server may send other lines before its SSH identification line. This was designed for notices from wrappers and gateways. A conforming client accepts these lines until it finds a line beginning with SSH-.

This system is restricted to authorized users.<CR><LF>
Maintenance window: Sunday 02:00 UTC.<CR><LF>
SSH-2.0-ExampleServer_4.2<CR><LF>

Pre-identification lines must not begin with SSH-. They are not included in the exchange hash. If a client displays them, it should filter terminal control characters. They are peer-controlled text.

This allowance is asymmetric. The client’s identification line is its first line on the connection.

The boundary to binary packets

There is no content-type marker between the line exchange and binary SSH. After the LF that terminates an endpoint’s identification line, that endpoint’s next byte starts its first binary packet.

... ExampleServer_4.2 CR LF | 00 00 04 ec ...
                             ^ first binary-packet byte

The first packet is normally SSH_MSG_KEXINIT. TCP may deliver the line and part of this packet in one read. Read boundaries do not change the protocol boundary.

Cleartext authenticated later

Identification lines are cleartext. A network observer can read them, and an active attacker can change bytes before key exchange has authenticated the connection.

Key exchange later hashes the exact client and server identification strings into the exchange hash. The server signs that hash. A simple in-transit change makes the client and server compute different hashes, so signature verification fails. A full man-in-the-middle can instead conduct two self-consistent exchanges and sign one with a substituted host key; the client’s host-key trust check rejects that case.

This is retrospective authentication, not confidentiality. A successful handshake detects modification of the identification strings but never hides them.

Check the boundary

  1. Can the server send a notice before its identification line? Can the client?
  2. Does CRLF enter the exchange hash?
  3. If one TCP read contains the server line and 20 more bytes, what are those 20 bytes?
  4. Why can a passive observer read an identification string even though the string is authenticated later?

References

SSH binary packets and data types

After the identification lines, SSH is a sequence of binary packets. A packet contains exactly one SSH message payload plus padding and, once keys are active, cryptographic protection. SSH framing recovers those packets from TCP’s continuous byte stream; TCP segment boundaries do not mark SSH messages.

The canonical definitions are the SSH data types in RFC 4251, section 5 and the packet format in RFC 4253, section 6.

Integers, strings, and name-lists

SSH encodes structured messages with a small set of types. Multi-byte integers use network byte order: most significant byte first.

TypeWire representationImportant property
byteone octetOften carries a message number.
booleanone octetZero is false; any nonzero value is read as true, though writers use zero or one.
uint32four octets, big-endianAn unsigned value from 0 to 2³² − 1.
uint64eight octets, big-endianAn unsigned value from 0 to 2⁶⁴ − 1.
stringuint32 byte length, then that many bytesBinary-safe; no terminator and no implied text encoding.
name-lista string containing comma-separated namesThe list may be empty; individual names may not be empty.
mpinta string containing a signed two’s-complement integerUsed by some cryptographic methods; canonical encoding matters.

For example, these values have the following encodings:

uint32 5
00 00 00 05

string "cat"
00 00 00 03 63 61 74

name-list "publickey,password"
00 00 00 12 70 75 62 6c 69 63 6b 65 79 2c 70 61 73 73 77 6f 72 64

The length of a string counts bytes, not characters. It can contain NUL bytes and is not inherently UTF-8. Whether a particular string represents text, a public-key blob, or arbitrary data is defined by the message containing it.

A name-list uses the same length prefix as a string. Its contents are names separated by commas, with no empty element. The zero-length string represents an empty list. Compare algorithm names as exact protocol identifiers; do not trim, case-fold, or normalize them.

One payload, one message number

The first byte of an SSH packet’s payload is its message number. The remaining payload bytes are fields whose types and order are defined for that message. Message numbers and packet sequence numbers are different things:

  • the message number is a byte inside the payload and identifies how to interpret that payload;
  • the packet sequence number is a per-direction uint32 maintained outside the packet and included in integrity calculations.

Some transport message numbers are fixed by RFC 4253:

NumberSymbolPurpose
1SSH_MSG_DISCONNECTEnd the SSH connection with a reason.
2SSH_MSG_IGNORECarry data that the recipient ignores.
3SSH_MSG_UNIMPLEMENTEDReport an unrecognized message.
4SSH_MSG_DEBUGCarry optional diagnostic text.
5SSH_MSG_SERVICE_REQUESTRequest a service such as user authentication.
6SSH_MSG_SERVICE_ACCEPTAccept the requested service.
20SSH_MSG_KEXINITOffer algorithms and begin key exchange.
21SSH_MSG_NEWKEYSSwitch one sending direction to new keys.

Numbers 30 through 49 are interpreted by the negotiated key-exchange method. Higher-layer RFCs define their own message layouts. A number’s meaning therefore depends on the current protocol state. An unknown message is answered with SSH_MSG_UNIMPLEMENTED, whose field identifies the sequence number of the unrecognized packet; see RFC 4253, section 11.4.

flowchart LR
    P["authenticated payload"] --> N["first byte<br/>message number"]
    N --> S{"current state"}
    S -->|"20 during transport"| K["interpret as KEXINIT"]
    S -->|"50 during userauth"| U["interpret as userauth message"]
    S -->|"unknown here"| X["send UNIMPLEMENTED"]

Transport control messages

Four generic messages can appear across several transport states:

  • SSH_MSG_DISCONNECT ends the SSH connection and supplies a reason code;
  • SSH_MSG_IGNORE carries bytes with no higher-layer meaning;
  • SSH_MSG_DEBUG carries peer-controlled diagnostic text; and
  • SSH_MSG_UNIMPLEMENTED reports an unrecognized message by packet sequence number.

A disconnect description and debug message are not trusted terminal text. They may explain a failure, but the numeric reason and the state in which the message arrived are stronger evidence.

SSH_MSG_SERVICE_REQUEST and SSH_MSG_SERVICE_ACCEPT cross a layer boundary. After initial key exchange, the client normally requests ssh-userauth. A successful authentication request names the service to start next, normally ssh-connection. A service name is not a channel type.

The base packet layout

The unencrypted form makes the framing easiest to see:

flowchart LR
    PL["packet_length<br/>uint32"] --> PDL["padding_length<br/>byte"]
    PDL --> PAY["payload<br/>message number + fields"]
    PAY --> PAD["random padding<br/>4 to 255 bytes"]
    PAD -. "after keys" .-> MAC["MAC or authentication tag<br/>algorithm-defined"]

packet_length is the combined byte length of padding_length, payload, and padding. It does not include its own four bytes or the MAC. Thus:

packet_length = 1 + payload_length + padding_length
base_packet_length = 4 + packet_length

There must be at least four padding bytes. The base packet length, excluding a MAC, must be a multiple of the cipher block size or eight bytes, whichever is larger. The padding length is chosen to meet that alignment. Padding frustrates simple traffic-size analysis, but it does not hide timing or make all messages the same size.

The padding bytes should be cryptographically random. They are removed before the payload is interpreted and have no meaning to the message layer.

Before and after keys

The identification lines come before binary framing. The first KEXINIT and initial key-exchange packets are binary packets, but there is not yet an active cipher or MAC. Protection changes independently by direction at NEWKEYS.

StageFramingProtection
IdentificationCRLF-terminated lineClear text; not a binary packet.
Initial key exchangeBinary packetRandom padding, no encryption or MAC.
After first NEWKEYSBinary packetNegotiated protection for that direction.
RekeyBinary packetOld keys until that direction switches at NEWKEYS.

For the original RFC 4253 encrypt-and-MAC construction, sending can be viewed as:

flowchart LR
    M["message payload"] --> C["compress if active"]
    C --> F["add lengths and padding"]
    F --> T["MAC over<br/>sequence number + plain packet"]
    F --> E["encrypt packet"]
    T --> W["wire bytes"]
    E --> W

The MAC is not itself encrypted in that base construction. Other negotiated packet-protection algorithms, including authenticated-encryption modes, define different details for protecting the length, ciphertext, and authentication tag. The negotiated packet algorithm is therefore part of the wire format: the base diagram must not be assumed to describe every modern mode byte for byte.

At a black-box level:

  • a compressor maps payload bytes to a usually shorter byte sequence;
  • an encryption algorithm maps plaintext and secret state to ciphertext;
  • a MAC maps a secret key plus packet data to an integrity tag;
  • an authenticated-encryption algorithm produces ciphertext and a tag together, and returns either verified plaintext or failure when opening it.

A protected packet has protocol meaning only after its MAC or authentication tag has been verified. Decrypted bytes from a packet with a bad tag are not an SSH message.

Packet-protection families

The negotiated names determine when packet bytes become trustworthy. Three families are common:

FamilyIntegrity coversReceiver consequence
RFC 4253 encrypt-and-MACSequence number and plaintext packetDecrypt enough to recover the packet, then verify the MAC.
Encrypt-then-MACSequence number, clear packet length, and ciphertextVerify the ciphertext before decrypting the packet body.
AEADAlgorithm-defined ciphertext and associated dataAccept plaintext only if the authentication tag verifies.

Encrypt-then-MAC algorithm names end in -etm@openssh.com; their packet construction is documented in the OpenSSH protocol extensions. AEAD mappings such as AES-GCM define their own length, nonce, and tag handling. There is no universal “modern SSH packet” layout beyond the base fields.

This distinction matters when reading a trace. A visible length may be an authenticated cleartext field in one mode, encrypted state in another, or handled by a separate algorithm-specific construction. Packet boundaries are not trusted merely because a tentative length was recovered.

Compression changes payloads, not channels

Compression applies to message payloads before packet protection and after packet verification. Its state is directional. The base zlib method starts after NEWKEYS; the deployed zlib@openssh.com method delays compression until user authentication succeeds, reducing exposure of decompression code to unauthenticated traffic.

RFC 8308 also defines the delay-compression extension. After authentication succeeds, the server starts compressing its direction after sending SSH_MSG_USERAUTH_SUCCESS. The client then sends SSH_MSG_NEWCOMPRESS uncompressed and compresses later packets in its direction. These methods use the same compression algorithm but have different transition rules.

Sequence numbers

Each direction has its own packet sequence number. Under the base protocol it starts at zero for the first binary packet, increments after every packet, wraps modulo 2³², and is not reset by rekeying. Although the number is not placed on the wire as a field, the base MAC calculation covers:

uint32(sequence_number) || unencrypted_packet

The endpoints therefore have to agree about exactly which packet is next. A different sequence number produces a different integrity result. These rules are in RFC 4253, section 6.4. The negotiated strict-KEX extension changes this invariant by resetting the appropriate directional sequence number immediately after each NEWKEYS.

Framing over TCP and size limits

Because TCP is a stream, a packet may arrive in several chunks or share a chunk with the next packet. These divisions are invisible at the SSH layer. The protocol-visible reconstruction is:

flowchart LR
    T["TCP bytes<br/>arbitrary chunks"] --> L["recover packet length<br/>as defined by active mode"]
    L --> P["complete protected packet"]
    P --> A{"integrity valid?"}
    A -->|no| F["packet rejected"]
    A -->|yes| S["remove framing and padding"]
    S --> C["decompress if active"]
    C --> M["one SSH message payload"]

With encryption active, “recover packet length” is algorithm-specific. A classic block cipher construction reveals the length by decrypting the first block. An authenticated-encryption mode may protect or expose the length differently. In all cases the advertised length, padding length, and required block alignment must agree before the bytes can represent a valid SSH packet.

RFC 4253 requires implementations to be able to process packets with an uncompressed payload of 32,768 bytes or less and a total packet size of 35,000 bytes or less. These are required interoperability capacities, not absolute protocol maxima; implementations may support larger packets. The normative wording is in RFC 4253, section 6.1.

An EOF after only part of the declared frame is a truncated packet, not a shorter message. Conversely, receiving only part of a packet while the TCP connection remains open says nothing about where the next TCP chunk will begin.

Worked packet

Consider this unencrypted illustrative packet. The separators are not on the wire, and real padding should not be a fixed pattern.

00 00 00 0c | 05 | 05 00 00 00 01 78 | aa bb cc dd ee

Read it from left to right:

  1. packet_length is 12, so 12 bytes follow the length field.
  2. padding_length is 5.
  3. The payload length is 12 - 1 - 5 = 6 bytes.
  4. The payload starts with message number 5, SSH_MSG_SERVICE_REQUEST.
  5. Its remaining bytes encode a one-byte string containing x.
  6. The complete base packet is 16 bytes, a multiple of eight.

The service name is deliberately illustrative rather than a useful SSH service. Framing and field decoding are separate from deciding whether a field value is valid in the current state.

Protocol review

Use the worked packet to check the following:

  • Which bytes are counted by packet_length, and which are outside it?
  • Why does a five-byte padding field make this particular base packet align?
  • Which byte selects the message, and how is that different from the unseen sequence number?
  • If TCP splits the four-byte length across two segments, does any byte of the SSH packet change?
  • After keys are active, at what point do the recovered plaintext bytes become an authenticated SSH message?
  • Why can packet-length handling differ between encrypt-and-MAC, encrypt-then-MAC, and AEAD?

Algorithm negotiation

After identification, the peers choose the algorithms that will protect the connection. Each endpoint sends an ordered proposal in SSH_MSG_KEXINIT. Both then apply the same deterministic selection rule.

This chapter assumes the binary types and packet framing from SSH binary packets and data types. The next chapter introduces the cryptographic operations used by the selected algorithms.

SSH_MSG_KEXINIT carries proposals

During the initial exchange, each peer normally sends an unprotected SSH_MSG_KEXINIT packet. Its exact payload is later included in the signed exchange hash.

Conceptually, the payload contains:

byte        SSH_MSG_KEXINIT (20)
byte[16]    random cookie
name-list   key exchange algorithms
name-list   server host key algorithms
name-list   encryption algorithms, client to server
name-list   encryption algorithms, server to client
name-list   MAC algorithms, client to server
name-list   MAC algorithms, server to client
name-list   compression algorithms, client to server
name-list   compression algorithms, server to client
name-list   languages, client to server
name-list   languages, server to client
boolean     first_kex_packet_follows
uint32      reserved (zero)

A name-list is a comma-separated sequence encoded as an SSH string. Names must not contain commas. Ordering expresses preference: left is preferred over right. An empty name-list has zero bytes of content.

The two directions are negotiated independently. A connection can therefore, at least in protocol terms, choose different ciphers or compression methods for client-to-server and server-to-client traffic. Most modern proposals are symmetrical, but the directional fields remain separate protocol decisions.

flowchart TB
    K[KEXINIT] --> U[One choice for the connection]
    U --> KA[Key-exchange method]
    U --> HK[Server host-key algorithm]
    K --> CS[Client to server]
    CS --> CSE[Encryption]
    CS --> CSM[MAC]
    CS --> CSC[Compression]
    K --> SC[Server to client]
    SC --> SCE[Encryption]
    SC --> SCM[MAC]
    SC --> SCC[Compression]

The 16-byte cookie makes otherwise identical proposals distinct. It is not an algorithm-selection nonce and has no role in the selection rule.

The selection rule: client preference wins

For each category, choose the first algorithm in the client’s list that also appears in the server’s list. This rule applies even when the server is performing the calculation and even for the server-to-client direction.

Suppose the proposals are:

client: curve25519-sha256,ecdh-sha2-nistp256,diffie-hellman-group14-sha256
server: diffie-hellman-group14-sha256,curve25519-sha256

The result is curve25519-sha256, because it is the first client preference that the server also supports. It does not matter that the server listed it second.

Equivalent pseudocode is:

for candidate in client_preferences:
    if candidate is in server_supported:
        return candidate
fail key exchange: no mutually supported algorithm

For a host-key algorithm, the candidate must also have the capability required by the chosen key-exchange method—for example, it must be able to make a signature when the KEX method requires one. If any required category has no match, negotiation fails; neither endpoint silently invents a fallback.

A worked multi-category example

CategoryClient preference orderServer supportSelected
KEXcurve25519-sha256, ecdh-sha2-nistp256ecdh-sha2-nistp256, curve25519-sha256curve25519-sha256
Host keyssh-ed25519, rsa-sha2-256rsa-sha2-256rsa-sha2-256
Encryption C→Scipher-a, cipher-bcipher-b, cipher-acipher-a
Encryption S→Ccipher-b, cipher-acipher-a, cipher-bcipher-b
Compression C→Snonenone, zlibnone

The placeholder cipher names make the ordering rule visible without implying a configuration recommendation. Algorithm policy changes over time; RFC 9142 updates KEX guidance that was originally published in RFC 4253.

No “chosen algorithms” message

Neither peer sends a separate result. Both have the same two ordered proposals and run the same deterministic rule, so both derive the same selection.

The exact original KEXINIT payloads must be retained because they later become transcript inputs.

The guessed-packet optimization

The first_kex_packet_follows boolean supports a latency optimization from the base protocol. A peer may guess the outcome from the first entries in the proposals and send the first method-specific KEX packet immediately after its KEXINIT.

If the guess is wrong, the receiver silently ignores that one guessed packet, then continues with the actual negotiated method. If the boolean is false, there is no guessed packet to discard.

flowchart TD
    A[Receive peer KEXINIT] --> B{first_kex_packet_follows?}
    B -- No --> E[Run negotiated KEX normally]
    B -- Yes --> C{Peer guessed KEX and<br/>host-key choices correctly?}
    C -- Yes --> D[Use following packet as<br/>first KEX packet]
    C -- No --> F[Silently ignore exactly<br/>the following guessed packet]
    D --> E
    F --> E

This is a wire-level rule even when the local endpoint never makes a guess: a peer’s wrong guessed packet is consumed and ignored rather than interpreted as the first packet of the negotiated method.

Markers in the KEX algorithm list

The kex_algorithms field is also used for capability markers. These look like algorithm names but are not runnable KEX methods.

Extension negotiation with ext-info-*

RFC 8308 defines two role-specific markers:

  • a client includes ext-info-c to say it can receive extension information from a server;
  • a server includes ext-info-s to say it can receive extension information from a client.

The different suffixes deliberately prevent them from matching each other and being selected as the KEX method. A peer that sees the appropriate marker may send SSH_MSG_EXT_INFO; it is not required to do so.

The markers belong in the initial KEXINIT, not later rekey proposals. They advertise a capability for the connection rather than an algorithm to renegotiate on every KEX.

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: KEXINIT includes ext-info-c
    S->>C: KEXINIT includes ext-info-s
    Note over C,S: Negotiate and complete KEX
    par Client outbound transition
        C->>S: NEWKEYS
        opt Client sends extension information
            C->>S: EXT_INFO as next packet
        end
    and Server outbound transition
        S->>C: NEWKEYS
        opt Server sends extension information
            S->>C: EXT_INFO as next packet
        end
    end

SSH_MSG_EXT_INFO contains a count followed by extension-name/value pairs. Unknown extension names must be ignored. A value is an arbitrary SSH string, not necessarily UTF-8 or a comma-separated list, so its named specification defines how to parse it.

The most visible example is server-sig-algs. It lets a server report the public-key signature algorithms it can process during user authentication. The user-authentication chapter follows that decision.

The first opportunity is directional: if an endpoint sends EXT_INFO after the initial KEX, it must be that sender’s next packet after its first NEWKEYS. RFC 8308 also permits a server to send a replacement EXT_INFO immediately before SSH_MSG_USERAUTH_SUCCESS. Extension negotiation does not change the algorithm choices already made by KEXINIT.

Strict key exchange

Strict key exchange is a deployed protocol extension designed in response to the Terrapin class of prefix-truncation attacks. It is unrelated to OpenSSH’s similarly named StrictHostKeyChecking user option.

Support is announced with role-specific pseudo-algorithms. Deployed names include kex-strict-c-v00@openssh.com and kex-strict-s-v00@openssh.com; the current specification also defines the standard-form names kex-strict-c and kex-strict-s. Strict KEX activates when the client/server pair uses the same form: either both standard names or both pre-standard names. A standard name on one side does not pair with a pre-standard name on the other. See the strict KEX Internet-Draft and OpenSSH’s protocol extension documentation.

When strict KEX is active:

  • the initial KEXINIT must be the peer’s first binary protocol packet;
  • only the expected negotiation and method-specific KEX messages are accepted during the initial exchange; and
  • packet sequence numbers reset just after each direction’s NEWKEYS, for the initial exchange and later rekeys.

The base RFC allows some generic transport messages during KEX. Strict KEX narrows that grammar: an unexpected IGNORE, DEBUG, or other non-KEX message during the initial exchange causes termination instead of being tolerated. That constraint prevents an attacker from using legal-but-unexpected packets to manipulate the implicit sequence-number state.

Protocol-design lesson

“Authenticated later” works only when the later authenticator covers all security-relevant state. Strict KEX closes gaps between the signed transcript and the packet sequence numbers used by the protected transport.

Compatibility and registries

SSH algorithm names are extensibility points rather than version bumps. New methods can be deployed by adding names to proposals, while old peers ignore names they do not know and select a mutual alternative.

The authoritative sets of names live in the IANA SSH protocol-parameter registries. RFC 9519 changed the registration policy for registries including KEX, encryption, MAC, compression, extension, and public-key algorithm names. It does not define a cryptographic algorithm or say which algorithm is safe to enable; use each method’s RFC and current algorithm-guidance documents for that.

Check your understanding

  1. A server sends two notice lines and then SSH-2.0-server\r\n. Where does the binary packet stream begin?
  2. The server prefers B,A; the client prefers A,B; both support both. Which algorithm is selected?
  3. Why can a network attacker read an identification string but not silently rewrite it in a successful authenticated handshake?
  4. Does ext-info-c mean “the client will send EXT_INFO” or “the client can receive EXT_INFO”?
  5. What state does strict KEX protect that the original exchange hash did not fully bind?
Answers
  1. At the first byte after the LF terminating the SSH-2.0-server line.
  2. A: selection always follows client preference order.
  3. The exact line, without CRLF, is an input to the exchange hash that the server signs. A rewrite makes the client and server transcripts differ.
  4. It means the client is prepared to receive it from the server.
  5. The ordering of packets during initial KEX and the packet sequence-number state at the transition to new keys.

References

Cryptography as protocol building blocks

An SSH client composes cryptographic primitives; it should not invent them. For this course, treat each primitive as a black box with typed inputs, outputs, and failure conditions.

The toolbox

flowchart TB
    R["Secure random bytes"] --> E["Ephemeral key pair"]
    E --> KA["Key agreement"]
    Transcript["Handshake transcript"] --> H["Hash"]
    KA --> KDF["Key derivation"]
    H --> KDF
    KDF --> Keys["Directional traffic keys and IVs"]
    HostPrivate["Server host private key"] --> Sig["Signature"]
    H --> Sig
    Keys --> Protect["Authenticated packet protection"]

These boxes solve different problems. Substituting one for another is usually a security bug.

Hash function

HASH(message bytes) -> fixed-size digest

A cryptographic hash gives a short commitment to bytes. Changing the message should unpredictably change the digest. SSH hashes an exact encoding of the key-exchange transcript to produce the exchange hash H.

Hashing does not prove who created a message. Anyone can hash public bytes. A signature or a keyed message authentication code supplies authentication.

Message authentication and authenticated encryption

A message authentication code (MAC) uses a secret key to detect modification.

MAC(key, packet context, bytes) -> authentication tag
VERIFY(key, packet context, bytes, tag) -> valid | invalid

Encryption hides content; a MAC protects integrity. Older SSH constructions negotiate these separately. Authenticated encryption with associated data (AEAD) deliberately combines both jobs:

SEAL(key, nonce, plaintext, associated_data) -> ciphertext, tag
OPEN(key, nonce, ciphertext, associated_data, tag) -> plaintext | failure

Never release unauthenticated plaintext. Never reuse a nonce with the same key when the algorithm forbids it. RFC 5116 defines the general AEAD interface; individual SSH cipher specifications define how it is mapped onto SSH packets. AES-GCM’s SSH mapping is described by RFC 5647.

Key agreement

Key agreement lets two peers independently compute the same secret without sending that secret over the network.

GENERATE(randomness) -> private_value, public_value
AGREE(our_private_value, peer_public_value) -> shared_secret | failure
sequenceDiagram
    participant C as Client
    participant S as Server
    C->>C: Generate c_private, C_public
    S->>S: Generate s_private, S_public
    C->>S: C_public
    S->>C: S_public
    C->>C: AGREE(c_private, S_public) = K
    S->>S: AGREE(s_private, C_public) = K
    Note over C,S: K never crosses the wire

The public values are not identities. An active attacker could replace both and create two secrets. SSH prevents that by including the exchange in a transcript which the server signs with its long-term host key.

Modern finite-field and elliptic-curve Diffie–Hellman methods fit this interface. RFC 8731 specifies Curve25519 and Curve448 key exchange for SSH. Public-value validation and all-zero/shared-secret failure checks belong inside a reviewed library.

Key encapsulation and hybrid exchange

A key-encapsulation mechanism (KEM) has a slightly different shape:

KEYGEN(randomness) -> public_key, private_key
ENCAPSULATE(public_key, randomness) -> ciphertext, shared_secret
DECAPSULATE(private_key, ciphertext) -> shared_secret | failure

Post-quantum/traditional hybrid SSH exchanges combine a post-quantum KEM secret with a traditional key-agreement secret. The goal is that the result remains secure if at least one component remains secure.

RFC 10042 specifies ML-KEM hybrid methods for SSH. A method name selects the entire construction, including encodings and combination rules; do not assemble a custom hybrid from primitive names.

Digital signatures

A signature proves possession of a private key without revealing it.

SIGN(private_key, message) -> signature
VERIFY(public_key, message, signature) -> valid | invalid

SSH uses signatures in two places with different identities:

A public key algorithm name may describe the key format, the signature scheme, or both. Keep the negotiated name, encoded key blob, and signature wrapper conceptually distinct. Ed25519’s SSH encoding is specified by RFC 8709; RSA/SHA-2 signatures are specified by RFC 8332.

Key derivation

The raw shared secret is not used directly as a packet key. SSH combines the shared secret K, exchange hash H, a purpose label, and the session identifier to derive separate material for each purpose and direction.

flowchart TB
    K["Shared secret K"] --> D["SSH key derivation"]
    H["Exchange hash H"] --> D
    SID["Session identifier"] --> D
    D --> CIV["Client → server IV"]
    D --> SIV["Server → client IV"]
    D --> CEK["Client → server encryption key"]
    D --> SEK["Server → client encryption key"]
    D --> CIK["Client → server integrity key"]
    D --> SIK["Server → client integrity key"]

The letters and expansion rules are exact protocol inputs, not descriptive labels that can be re-encoded freely. See RFC 4253 section 7.2.

Four kinds of key to keep separate

MaterialLifetimeMain purpose
Server host keylong-livedidentify the server and sign key exchange
User authentication keylong-livedprove the user’s identity to the server
Ephemeral KEX key/valueone exchangeestablish a fresh shared secret
Traffic key and IVuntil rekeyprotect packets in exactly one direction

Forward secrecy comes from key exchange with fresh ephemeral secrets. Those secrets must also be erased after key derivation. Later theft of the host private key should then not reveal old traffic. This property does not help if an endpoint, random generator, or plaintext was already compromised.

Protocol boundary

SSH specifies how these primitives are composed: which transcript is hashed, which identity signs it, how keys are separated by purpose and direction, and when those keys become active. The primitive’s internal mathematics remains a separate concern. Real SSH software relies on reviewed cryptographic implementations for scalar arithmetic, signature details, constant-time operations, and primitive-level validation.

Key exchange: from public values to protected traffic

Algorithm negotiation chooses a key-exchange method. Running that method must then accomplish three different jobs:

  • establish fresh secret material over an observable network;
  • authenticate the server and the handshake transcript; and
  • derive independent keys for protecting packets in both directions.

It does not authenticate the login user. User authentication is a later SSH protocol layered over this protected transport.

This chapter treats cryptographic primitives as black boxes: what goes in, what comes out, and what security role the output plays. The linked standards contain the mathematical and encoding details for deeper study.

Three mechanisms, three questions

Key exchange has three separate jobs.

MechanismQuestion it answers
Ephemeral key agreementCan both endpoints derive secret material that a passive observer cannot?
Host-key signatureDid the holder of this host key approve this exact handshake?
Host-key trust policyIs this key authorized to represent the host the user intended to reach?

Passing the first two checks is not enough if the client accepts any host key. An active attacker can run its own key exchange and sign its own transcript with its own key. The client’s known-hosts or certificate policy supplies the missing identity binding.

The basic signed key-exchange flow

Many SSH KEX methods fit the same two-message shape after KEXINIT:

sequenceDiagram
    participant C as Client
    participant N as Untrusted network
    participant S as Server
    C->>N: Client ephemeral public value Q_C
    N->>S: Q_C
    Note over C: Keep ephemeral private value q_C
    Note over S: Generate q_S and Q_S
    S->>N: Host public key K_S, Q_S, signature over H
    N->>C: K_S, Q_S, signature over H
    Note over C,S: Independently derive the same shared secret K
    Note right of C: Rebuild H, validate K_S, verify signature

The public values are safe to expose. The ephemeral private values and the resulting shared secret are not transmitted. “Ephemeral” means freshly generated for this exchange and discarded afterwards. Compromise of the server’s long-term host private key at a later date should therefore not reveal old traffic secrets; this property is called forward secrecy.

The server host key serves a different lifetime and purpose. It is normally persistent so clients can recognize the server across connections, and its private half signs the exchange hash.

Key agreement is method-specific

The negotiated method defines how the endpoints exchange public material, validate peer inputs, and derive the shared secret K. Finite-field Diffie–Hellman, ECDH, and post-quantum hybrid methods use different messages and checks, but they feed the same later SSH stages: compute the exchange hash, verify the host-key signature, and derive traffic keys.

The protocol invariant is exactness. Each endpoint must follow the selected method’s wire encodings and reject invalid public values. It must not import validation rules from a similar-looking method.

The optional Key-exchange method survey compares fixed-group DH, group exchange, ECDH, X25519, and standardized hybrid methods.

The exchange hash: authenticate the whole decision

Deriving the same secret is not enough. The client must know that an attacker did not alter the version strings, proposals, host key, or ephemeral values. SSH serializes those values and hashes them into the exchange hash H.

For the classic DH flow in RFC 4253, the conceptual inputs are:

H = HASH(
    V_C || V_S ||
    I_C || I_S ||
    K_S ||
    e || f ||
    K
)

Where:

  • V_C, V_S are the identification lines without CRLF;
  • I_C, I_S are the exact KEXINIT payloads, starting with their message number;
  • K_S is the encoded server public host key;
  • e, f are the client and server ephemeral public values; and
  • K is the shared secret.

This is conceptual notation rather than raw byte-array concatenation. Each item uses the SSH wire type required by that KEX specification. ECDH, group-exchange, and hybrid methods adjust the method-specific fields, so H has a method-specific wire definition even though its security role is stable.

Because the proposals are included, an attacker cannot silently delete a stronger shared algorithm from the lists to force a weaker mutual choice. The server and client would hash different KEXINIT payloads and signature verification would fail.

The host-key signature and the trust decision

The server signs H with its host private key. The client verifies the signature using K_S, the public host key carried in the reply. This proves that one entity controlled both the KEX response and the private key matching K_S.

The client must then establish that K_S is acceptable for the intended host, for example by:

  • matching a previously pinned key in a known-hosts database;
  • validating an SSH host certificate under a trusted certification key; or
  • asking a user to verify a fingerprint through an independent channel.

Skipping or merging these two gates causes security bugs. A valid signature made by an attacker’s own key is still a valid signature; it is not evidence that the key belongs to the requested server. The two checks may occur in either order, but both properties are required for an authenticated KEX.

From K and H to traffic keys

The KEX method outputs the shared secret K and exchange hash H. The base SSH key schedule combines them with a one-byte purpose label and the session identifier to produce separate key material:

flowchart LR
    K[Shared secret K] --> KDF[SSH key derivation]
    H[Current exchange hash H] --> KDF
    SID[Session identifier] --> KDF
    KDF --> A[Initial IV C→S]
    KDF --> B[Initial IV S→C]
    KDF --> C[Encryption key C→S]
    KDF --> D[Encryption key S→C]
    KDF --> E[Integrity key C→S]
    KDF --> F[Integrity key S→C]

In RFC 4253, Section 7.2, labels A through F distinguish those outputs. If a cipher needs more key bytes than one hash produces, the specified expansion procedure produces more. Modern combined cipher-and-integrity constructions may consume the categories differently; the negotiated method defines that mapping.

Independent directional keys prevent a packet sent in one direction from being replayed as if it came from the other and keep each direction’s cipher state separate.

Session identifier versus exchange hash

On the first KEX:

session_id = H_first

On a rekey, a new exchange produces fresh K and a new H, but session_id remains H_first for the entire SSH connection.

timeline
    title Exchange hashes and one stable session identifier
    Initial KEX : H1 calculated
                : session_id = H1
                : keys derived from K1, H1, session_id
    Rekey 1     : H2 calculated
                : session_id remains H1
                : keys derived from K2, H2, session_id
    Rekey 2     : H3 calculated
                : session_id remains H1
                : keys derived from K3, H3, session_id

The stable identifier binds later protocols—most notably public-key user authentication—to this SSH connection even as its transport keys change. The latest H never replaces it.

Activating keys with SSH_MSG_NEWKEYS

Key derivation alone does not define the byte at which an endpoint should switch algorithms. SSH_MSG_NEWKEYS is the boundary marker, independently for each direction.

  • Immediately after sending NEWKEYS, use the new algorithms and keys for subsequent outbound packets.
  • When receiving NEWKEYS, use the new algorithms and keys for subsequent inbound packets.
sequenceDiagram
    participant C as Client
    participant S as Server
    Note over C,S: KEX packets use the old protection<br/>(none during initial KEX)
    C->>S: NEWKEYS (old outbound protection)
    Note left of C: Switch C→S sending keys
    S->>C: NEWKEYS (old outbound protection)
    Note right of S: Switch S→C sending keys
    C->>S: First packet under new C→S keys
    S->>C: First packet under new S→C keys

The messages may cross on the network, creating a brief interval in which one direction uses new keys while the other still uses old keys. Under strict KEX, each direction’s packet sequence number is also reset just after its corresponding NEWKEYS boundary.

Only after key activation can extension information and higher-layer service requests proceed in the newly protected transport. See RFC 4253, Section 7.3 and RFC 8308 for the post-KEX extension exchange.

Rekeying a live connection

SSH can repeat key exchange without opening a new TCP connection or repeating user authentication. Either endpoint starts a rekey by sending a new SSH_MSG_KEXINIT; the other responds with its proposal.

Why rekey?

  • limit how much traffic is protected by one set of keys;
  • replace keys after an endpoint-defined time or byte count;
  • negotiate a new algorithm policy that both endpoints now offer; or
  • refresh the cryptographic state of a long-lived connection.

RFC 4253 recommends rekeying after one gigabyte or one hour, whichever comes first. This is a default recommendation, not a universal limit. Endpoints may use stricter policy, and selected algorithms can impose stricter limits.

During rekey:

  • the KEX packets are protected by the old algorithms until each directional NEWKEYS transition;
  • client and server roles do not change;
  • fresh K and H values are produced;
  • the original session identifier does not change; and
  • higher-layer application traffic pauses once KEX is in progress, apart from already in-flight packets allowed by the protocol state machine.

A peer can receive an arbitrary number of application packets that were already in flight before it sees the other side’s KEXINIT. This is why the rekey transition is not an instantaneous, connection-wide boundary.

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: CHANNEL_DATA under old keys
    S->>C: KEXINIT under old keys
    Note over C: An application packet may already be in flight
    C->>S: KEXINIT under old keys
    Note over C,S: Pause new application messages<br/>and run KEX under old protection
    C->>S: NEWKEYS under old C→S keys
    S->>C: NEWKEYS under old S→C keys
    Note over C,S: Resume with fresh directional keys

Crossed NEWKEYS messages, simultaneous KEX initiation, and application data already in flight all follow from the two independent directions shown above.

Security invariants

The essential invariants across initial KEX and rekey are:

  • ephemeral private values are unpredictable and are never sent;
  • received public values have the exact encoding, length, group membership, and special-value checks required by the selected method;
  • the exchange hash uses exact on-wire transcript values and type encodings;
  • host signature validity and host identity trust are distinct properties;
  • key derivation produces independent material for both directions;
  • sending and receiving switch keys at their own NEWKEYS boundaries;
  • the session identifier is assigned once, from the first H;
  • a rekey retains roles and the session identifier but replaces traffic keys.

Check your understanding

  1. Which key is usually persistent: an ephemeral ECDH key or a server host key?
  2. What does a valid signature over H prove, and what does it not prove by itself?
  3. Why are both KEXINIT payloads included in H?
  4. Why must public-value validation follow the selected method’s rules?
  5. The client has sent NEWKEYS but has not received the server’s NEWKEYS. Which new keys are active?
  6. Does the session identifier change after rekeying?
Answers
  1. The server host key. Ephemeral ECDH keys are freshly generated for a KEX.
  2. It proves that the holder of the private key corresponding to K_S approved that exchange hash. A separate trust check establishes whether K_S represents the intended host.
  3. To bind algorithm negotiation into the authenticated transcript and detect modification or downgrade.
  4. Methods use different encodings, groups, and invalid-value checks. A value accepted under another method’s rules may be unsafe or malformed here.
  5. New keys are active for the client’s outbound direction. Its inbound direction still uses the old keys until it receives server NEWKEYS.
  6. No. It remains the first exchange hash for the life of the connection.

Primary references

Key-exchange method survey

This optional chapter compares key-exchange families. The main Key exchange chapter covers the state transitions that all of them share.

A method specification must define more than a cryptographic primitive. It also defines the messages, public-value encodings, validation rules, shared secret K, and exact exchange-hash inputs. Those details are part of the SSH wire protocol.

Finite-field Diffie–Hellman

Classic Diffie–Hellman works in a group described by a large prime p and a generator g. The client and server generate fresh private values and exchange the corresponding public values:

client public e = g^x mod p
server public f = g^y mod p
shared secret K = f^x mod p = e^y mod p

An observer sees p, g, e, and f but cannot feasibly recover K. This claim depends on suitable parameters, unpredictable private values, and validation of received public values.

RFC 4253, Section 8 defines the original fixed-group flow. The group belongs to the algorithm definition, so selecting the method also selects the group. The original SHA-1 methods are obsolete for new deployments; RFC 9142 gives current status guidance.

Diffie–Hellman group exchange instead lets the client request a size range and the server choose parameters:

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: GEX_REQUEST(min, preferred, max)
    S->>C: GEX_GROUP(p, g)
    C->>S: GEX_INIT(e)
    S->>C: GEX_REPLY(host key, f, signature)

The requested sizes, selected group, and public values enter the exchange hash. RFC 4419 defines the wire flow. RFC 9142 prohibits generated MODP groups below 2048 bits and deprecates the SHA-1 variant.

Elliptic-curve Diffie–Hellman

ECDH has the same protocol role as finite-field DH but uses points in an elliptic-curve group. Each endpoint combines its private scalar with the peer’s public point to obtain the shared result.

RFC 5656 integrates NIST-curve ECDH into SSH. A received public point must be valid for the selected group. An invalid point makes key exchange fail.

curve25519-sha256 uses X25519 and fixed-size byte strings:

X25519(local 32-byte private scalar, peer 32-byte public value)
    -> 32-byte shared result

The peer value must have the expected length, and an all-zero result must make the exchange fail. These checks are specific to X25519; another method can have different encoding and validation rules. RFC 8731 defines the Curve25519 and Curve448 SSH methods.

Hybrid post-quantum exchange

A sufficiently capable quantum computer would break traditional DH and ECDH. This creates a harvest-now, decrypt-later risk for traffic that must remain secret for many years.

A post-quantum/traditional hybrid combines two secrets:

  1. a traditional ECDH secret, for confidence in established cryptography; and
  2. a post-quantum KEM secret, for resistance to known quantum attacks.

With the specified combiner, the goal is to remain secure if either component remains secure.

A key-encapsulation mechanism has three operations:

KeyGen()       -> public key pk, secret key sk
Encaps(pk)     -> ciphertext ct, shared secret ss
Decaps(sk, ct) -> the same shared secret ss, or failure

In the SSH hybrid flow, the client creates the KEM key pair and sends its public key. The server encapsulates a secret and returns the ciphertext. The traditional ECDH exchange runs alongside it.

RFC 10042 defines three ML-KEM hybrid methods:

  • mlkem768nistp256-sha256;
  • mlkem1024nistp384-sha384; and
  • mlkem768x25519-sha256.

For these methods, the SSH shared secret is:

K = HASH(K_PQ || K_CL)

K_PQ is the ML-KEM secret and K_CL is the classical ECDH secret. Exact component lengths are checked before processing; an invalid input or failed decapsulation ends the exchange.

RFC 9941 defines the deployed sntrup761x25519-sha512 method. It has the same broad role but different components, encodings, and combiner. Similar names do not make hybrid methods wire-compatible.

A post-quantum key exchange protects session-secret establishment. If the host signature is traditional, server authentication is not thereby post-quantum. KEX and host-key algorithms are separate negotiation categories.

What varies by method

The stable SSH pattern is: exchange public material, derive K, compute H, and verify the server’s signature. Method specifications vary at four points:

QuestionExamples of method-specific detail
Which messages are sent?Fixed DH, group exchange, and ECDH use different message sequences.
How are public values encoded?DH uses mpint; X25519 uses a fixed-size SSH string.
What must be rejected?Invalid groups, points, lengths, special values, or KEM ciphertexts.
What enters H?Group parameters and hybrid components add method-specific transcript fields.

When reading a method RFC, find those four answers before comparing performance or algorithm status. A value valid for one method must not be accepted under a different method’s rules.

References

Server authentication

Before a client sends a password, opens a shell, or forwards a port, it must answer a more basic question: which server received this connection? Encryption without this check can create a private tunnel to an attacker.

Server authentication joins two separate facts:

  1. The server proves that it controls a private host key.
  2. The client decides whether the matching public host key is trusted for the requested host.

The protocol provides the proof. Local policy provides the trust decision. RFC 4251, Section 4.1 describes the SSH host-key trust models. RFC 4253, Section 8 shows the host key bound into a signed key exchange.

From host-key proof to server identity

Key exchange explains how the server signs the exchange hash H. The client verifies that signature with the public host key carried in the key-exchange reply. This proves control of the matching private key and binds the proof to this exchange.

The proof does not name a network destination. An attacker can complete the same protocol with the attacker’s own host key. The client must also apply a trust rule that binds the presented public key to the destination it intended to reach.

sequenceDiagram
    participant C as Client
    participant S as Server
    participant T as Client trust store

    C->>S: Identification and KEXINIT
    S->>C: Identification and KEXINIT
    C->>S: Client ephemeral value
    Note over C,S: Both derive shared secret K and exchange hash H
    S->>C: Public host key K_S, server ephemeral value, SIGN(host_private, H)
    C->>C: Recompute H
    C->>C: VERIFY(K_S, H, signature)
    C->>T: Is K_S trusted for this destination?
    T-->>C: match, new key, or conflict
    Note over C,S: Continue only if proof and trust policy both succeed

The two checks are independent. A trusted stored key without a valid exchange signature proves nothing about the current peer. A valid signature made by an untrusted key proves control of that key, not the server’s identity.

Trust models

Pre-provisioned host keys

An administrator can distribute a verified host public key before the first connection. The client then accepts only that key for the destination. This is strong and simple, but distributing and rotating entries across many clients takes work.

Trust on first use

Trust on first use, commonly shortened to TOFU, remembers the key observed on the first connection and requires the same key later.

flowchart TD
    Presented["Server presents host key"] --> Lookup{"Stored identity for host?"}
    Lookup -->|"Matching key"| Accept["Accept proof"]
    Lookup -->|"No entry"| VerifyNew["Verify fingerprint through an independent channel"]
    VerifyNew -->|"Verified and approved"| Store["Store host-to-key binding"]
    Store --> Accept
    VerifyNew -->|"Cannot verify"| Stop["Stop or explicitly accept TOFU risk"]
    Lookup -->|"Different key"| Investigate["Stop and investigate"]

TOFU detects an attacker who appears only after a trustworthy first connection. It does not detect an attacker present during that first connection. A fingerprint displayed in the same network path is not independent verification; obtain it from an administrator, an authenticated inventory system, a console, or another trusted channel.

Certification authority

With CA-based trust, the client trusts a CA public key and accepts host identities certified under it, subject to certificate names and validity policy. This moves the maintenance problem from every host key to a smaller set of CA keys.

The deployed OpenSSH certificate format wraps a host public key with signed metadata. A client validates at least:

  • that the certificate is a host certificate, not a user certificate;
  • the CA signature and local trust in that CA;
  • a principal matching the intended destination;
  • the validity interval and revocation policy; and
  • every critical option it must understand to accept the certificate.

The key identifier is useful for logs and revocation, but it is not the hostname match. OpenSSH records trusted host CAs with an @cert-authority marker in known_hosts. The format is a deployed extension rather than an X.509 certificate. See the OpenSSH certificate specification and the sshd(8) known-hosts format.

The known_hosts database

In OpenSSH-style clients, known_hosts implements a local mapping from a connection identity to one or more acceptable public host keys. A simplified entry looks like this:

host-patterns key-type base64-key optional-comment

Host patterns can include hostnames and addresses; non-default ports are commonly represented as [host]:port. Hostnames may be hashed to reduce what a stolen file reveals. Hashing hides names at rest, but it does not make an untrusted key trustworthy.

OpenSSH supports two especially important markers:

  • @cert-authority makes the listed key a trusted signer for host certificates matching the host pattern.
  • @revoked rejects the listed key if it is encountered.

The file is a trust database, not a record of whatever the network most recently said. Automatically replacing a conflicting key erases the very signal designed to reveal interception or an unexpected server replacement.

What identity should be looked up?

The trust lookup should follow the destination the user intended and the client’s explicit configuration. Details such as aliases, non-default ports, proxy jumps, and host-key aliases can change the lookup name.

flowchart LR
    Intent["User requests<br/>alias or hostname"] --> Config["Resolve client configuration"]
    Config --> Route["Choose network route<br/>direct or jump host"]
    Config --> TrustName["Choose host-key lookup identity"]
    Peer["Final SSH peer presents host key"] --> Check["Check proof and trust binding"]
    TrustName --> Check

A jump host transports bytes toward the final server; it does not replace final-server host verification. Each SSH connection has its own peer identity and trust decision.

Fingerprints and key formats

A fingerprint is a compact digest of a public key blob. It is useful for comparing keys over a human channel, but it is not an identity by itself: the trusted statement must bind the fingerprint to a particular hostname or host role.

Do not confuse these representations:

flowchart TD
    Material["Public key material"] --> Blob["SSH binary public-key blob<br/>algorithm name + key fields"]
    Blob --> Fingerprint["Hash and display encoding<br/>for human comparison"]
    Blob --> File["Public-key file encoding<br/>for storage or exchange"]
    Blob --> Wire["Length-prefixed SSH string<br/>inside protocol messages"]

RFC 4716 defines an SSH2 public-key file format with BEGIN SSH2 PUBLIC KEY and END SSH2 PUBLIC KEY markers. This is a storage and interchange format, not the framing sent directly as a key-exchange field. OpenSSH also uses its own one-line public-key format in files. These representations are distinct even though both may contain Base64 text.

Host-key changes

A changed key can be legitimate: a host was rebuilt, an algorithm was retired, a service moved, or keys were rotated. It can also mean DNS or routing manipulation, a misdirected connection, or an active machine-in-the-middle attack.

When a stored binding conflicts:

  1. Stop before user authentication. Do not send a password to an unverified peer.
  2. Confirm the exact hostname, port, resolved configuration, and route.
  3. Obtain the new fingerprint through an independent trusted channel.
  4. Understand why the old key changed and whether it should be revoked.
  5. Update the narrowest correct trust entry only after verification.

Safety note: Instructions to “just delete known_hosts” discard unrelated trust and normalize bypassing warnings. Diagnose and replace the specific stale binding only after verifying the new identity.

Host-key rotation can be designed so old and new keys overlap, letting clients learn the replacement through an already authenticated connection. The precise mechanism depends on client and server extensions, but the invariant remains: a new trust binding needs an authenticated path.

OpenSSH deploys such a path with hostkeys and hostkeys-prove global requests. After user authentication, the server can advertise its host-key set. The client asks for possession proofs for new keys; each proof covers the session identifier and the new host key. The client records a new key only after verifying its proof through the connection already authenticated by a trusted key.

This mechanism supports overlap and algorithm migration. It does not make an unexpected key at the start of an unauthenticated connection trustworthy. The host-key update Internet-Draft documents the deployed protocol.

Protocol review

At the end of server authentication, you should be able to answer all of these questions from the protocol exchange and the client’s trust policy:

  • Which configured destination identity did I check?
  • Which host-key algorithm and exact public key did the server present?
  • Was the signature over the exchange hash valid?
  • Which trust rule accepted the key: an exact stored key, a verified first-use decision, or a trusted CA?
  • If the key conflicted, did I abort before sending user credentials?

Only after both cryptographic proof and trust policy succeed should the client send SSH_MSG_NEWKEYS-protected user-authentication traffic.

Lab: reason about three connections

For each situation, write down whether the signature check succeeds, whether the identity check succeeds, and what the client should do.

  1. A known server presents the stored public key and a valid signature.
  2. An attacker presents a different public key and a valid signature made by the matching attacker key.
  3. A rebuilt server presents a new key that an administrator has independently verified.

Situation 2 has valid cryptography but invalid identity. Situation 3 becomes safe only after the trust database is updated from authenticated information.

References

User authentication

Once the protected transport is active and the server’s host key is trusted, the client can ask the server to authenticate a user. This phase answers a different question from host verification:

  • Server authentication: is this the intended server?
  • User authentication: may this peer act as a particular account?

This course assumes that client policy requires confidentiality and integrity before user authentication. The SSH framework has a none cipher for unusual configurations. A safe client does not use password authentication without confidentiality. RFC 4252 specifies the authentication framework and core methods; RFC 4256 specifies the generic interactive method.

Entering the authentication service

After key exchange, the client requests the ssh-userauth service using the transport protocol’s service mechanism. The server accepts it before authentication requests begin.

sequenceDiagram
    participant C as Client
    participant S as Server
    Note over C,S: Confidentiality and integrity are active
    C->>S: SSH_MSG_SERVICE_REQUEST("ssh-userauth")
    S-->>C: SSH_MSG_SERVICE_ACCEPT("ssh-userauth")
    C->>S: SSH_MSG_USERAUTH_REQUEST
    alt Authentication complete
        S-->>C: SSH_MSG_USERAUTH_SUCCESS
        Note over C,S: Requested service starts, normally "ssh-connection"
    else Rejected or another factor required
        S-->>C: SSH_MSG_USERAUTH_FAILURE(methods, partial_success)
    end

Every SSH_MSG_USERAUTH_REQUEST begins with the same fields:

byte      SSH_MSG_USERAUTH_REQUEST
string    user name
string    service name
string    method name
...       method-specific fields

The service name here is the service to run after authentication, normally ssh-connection; it is not ssh-userauth. The username and service are repeated in every request. If either changes, the server must clear any accumulated authentication state. Authentication progress therefore belongs to one particular (user, requested service) pair. See RFC 4252, Section 5.

The server drives policy

On rejection, the server returns:

byte       SSH_MSG_USERAUTH_FAILURE
name-list  authentications that can continue
boolean    partial success

The list says which method names may productively continue at that point. It is not a permanent capability list. The boolean is true when the just-completed method succeeded but policy requires another authentication method. Only SSH_MSG_USERAUTH_SUCCESS means authentication is complete.

stateDiagram-v2
    [*] --> Trying
    Trying --> Trying: FAILURE, partial=false<br/>choose an offered method
    Trying --> MoreFactors: FAILURE, partial=true
    MoreFactors --> MoreFactors: FAILURE<br/>follow updated method list
    MoreFactors --> Authenticated: SUCCESS
    Trying --> Authenticated: SUCCESS
    Trying --> Aborted: timeout, limit, disconnect
    MoreFactors --> Aborted: timeout, limit, disconnect

For example, a server that requires a public key and then an interactive one-time code may reply after the valid signature with partial success = true and a next-method list containing keyboard-interactive.

Do not infer account existence from a method list. Servers may return bogus or uniform-looking failures for unknown accounts to reduce username enumeration. Authentication also has finite time and attempt limits; RFC 4252, Section 4 recommends server-side limits rather than allowing indefinite retries.

The none method

none has no method-specific fields. It is primarily a discovery request:

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: USERAUTH_REQUEST(user, "ssh-connection", "none")
    alt Account intentionally needs no authentication
        S-->>C: USERAUTH_SUCCESS
    else Authentication is required
        S-->>C: USERAUTH_FAILURE(next methods, false)
    end

The server must not advertise none in its method list. It accepts none only when the user is actually allowed access without authentication. A client can use the failure response to seed method selection, but it must tolerate servers that provide a limited or policy-shaped list. See RFC 4252, Section 5.2.

Public-key authentication

Public-key authentication proves possession of a user’s private key. The server performs two independent checks:

  1. Is this public key authorized for this user under current policy?
  2. Does the signature verify with that public key?

Passing only one check is not enough.

Optional key query

The client may first ask whether a public key is potentially acceptable without creating a signature:

byte      SSH_MSG_USERAUTH_REQUEST
string    user name
string    service name
string    "publickey"
boolean   FALSE
string    public key algorithm name
string    public key blob

The server responds with SSH_MSG_USERAUTH_PK_OK echoing the algorithm and blob, or with SSH_MSG_USERAUTH_FAILURE. The query can avoid unlocking a private key, invoking a hardware token, or doing an expensive signing operation for a key the server will not use.

PK_OK is not authentication success and is not a guarantee that the later signed request will satisfy all policy. It merely permits the client to proceed with that key.

Signed request

The client may follow the query with a signed request, or skip the query and send the signed form immediately:

byte      SSH_MSG_USERAUTH_REQUEST
string    user name
string    service name
string    "publickey"
boolean   TRUE
string    public key algorithm name
string    public key blob
string    signature

The signature covers the session identifier followed by the complete signed request fields, including the TRUE flag, algorithm, and key blob. The session identifier is the exchange hash from the first key exchange and remains unchanged across later rekeys.

flowchart LR
    SID["Session identifier"] --> Encode["SSH encode signed request data"]
    Req["user · service · method<br/>TRUE · algorithm · public key"] --> Encode
    Encode --> Sign["SIGN"]
    Private["User private key"] --> Sign
    Sign --> Signature["SSH signature value"]

    Signature --> Verify["VERIFY"]
    Public["Offered public key"] --> Verify
    Encode --> Verify
    Verify --> Proof["Proof bound to this SSH session and request"]

Binding the signature to the session identifier prevents an observed authentication signature from being replayed on a different SSH connection. Binding the username, service, and key prevents those fields from being substituted after signing. The exact byte sequence is defined by RFC 4252, Section 7: the signature covers the SSH binary encodings, not a textual rendering of the fields.

Signature algorithm versus key format

The key blob and signature algorithm are related but not always named alike. For RSA, RFC 8332 defines the SHA-2 signature names rsa-sha2-256 and rsa-sha2-512 while retaining the ssh-rsa encoding for the RSA public-key blob. The outer signature algorithm string therefore need not equal the format name inside the key blob.

RFC 8709 specifies ssh-ed25519 and ssh-ed448, whose keys are used only for signing. These signatures prove possession; they do not encrypt the request or channel data.

A private-key passphrase is also distinct from the account password. The passphrase usually decrypts a local private-key file or unlocks a signing device. It should not be placed in an SSH userauth packet. A signing agent can return a signature without disclosing the private key to the SSH client.

Discovering acceptable signature algorithms

The server can send the server-sig-algs extension in SSH_MSG_EXT_INFO. Its value is a name-list of public-key algorithms that the server can process in a publickey authentication request.

This is especially useful for RSA keys. The stored key blob can still use the ssh-rsa format while the authentication signature uses rsa-sha2-256 or rsa-sha2-512. The extension lets the client choose a usable signature algorithm without trying each one as an authentication attempt.

The extension describes protocol capability, not account authorization. A listed algorithm can still fail because the key is not authorized, a required factor is missing, or server policy rejects the request. If the extension is absent, the client cannot infer that a particular algorithm is unsupported.

RFC 8308, Section 3.1 defines server-sig-algs and its timing.

Host-bound public-key authentication

Agent forwarding lets a remote host ask an agent to sign a standard userauth request for another SSH connection. The session identifier binds that request to the new connection, but it does not tell the agent which server host key the connection accepted.

OpenSSH’s deployed publickey-hostbound-v00@openssh.com method adds the initial server host key to the signed request. A compatible agent can then apply destination constraints using both the userauth request and the server identity. This narrows delegated signing authority; it does not make a compromised server harmless.

This is an OpenSSH extension, not a core RFC 4252 method. Servers advertise it through extension information. Its wire format is documented in the OpenSSH protocol extensions.

Password authentication

The normal password request adds a false change-password flag and the password:

byte      SSH_MSG_USERAUTH_REQUEST
string    user name
string    service name
string    "password"
boolean   FALSE
string    plaintext password

“Plaintext” describes the field before transport protection. With the safe policy assumed here, the transport encrypts the complete packet. This method does not hash the password. Hashing it in the client would only create a password-equivalent value unless the server protocol expected that value.

The server may send SSH_MSG_USERAUTH_PASSWD_CHANGEREQ for an expired password. A retry with the boolean set to true carries both the old and new passwords. RFC 4252, Section 8 gives the complete formats and response meanings.

Safety note: RFC 4252 recommends disabling password authentication when the transport does not provide confidentiality. A password, old or new, is secret even though the wire layout calls it a plaintext field.

Password authentication is one request with one password field. It is not the same wire protocol as keyboard-interactive, even when both cause a terminal to display Password:.

Keyboard-interactive authentication

The keyboard-interactive method is a generic series of server prompts and client answers. It can support one-time passwords, challenge-response systems, password-expiry dialogs, and multi-step authentication without teaching the client each backend mechanism.

The initial request contains language and submethod hints but no answers:

byte      SSH_MSG_USERAUTH_REQUEST
string    user name
string    service name
string    "keyboard-interactive"
string    language tag
string    submethods

The server then sends one or more SSH_MSG_USERAUTH_INFO_REQUEST messages. Each includes a name, instructions, language tag, a prompt count, and that many (prompt, echo) pairs. The client replies with SSH_MSG_USERAUTH_INFO_RESPONSE, a response count, and exactly that many response strings.

sequenceDiagram
    participant U as User
    participant C as Client
    participant S as Server
    C->>S: USERAUTH_REQUEST("keyboard-interactive", hints)
    S-->>C: INFO_REQUEST(name, instructions, prompts[])
    loop Each prompt
        C->>U: Display prompt
        U-->>C: Enter response (echo according to flag)
    end
    C->>S: INFO_RESPONSE(responses[])
    alt More information needed
        S-->>C: INFO_REQUEST(...)
    else Method or all authentication succeeds
        S-->>C: USERAUTH_SUCCESS or FAILURE
    end

Only one information request may be outstanding at a time, but the client must handle multiple request-response rounds. A request with zero prompts can carry informational text; the client still answers with a zero-response message. The echo boolean controls whether the user’s input should be shown: false is appropriate for secrets, while true may be appropriate for a visible identifier. See RFC 4256, Sections 3.2 and 3.3.

The prompts, instructions, banners, and error descriptions came from the remote server. Even after host authentication, treat them as untrusted display data:

  • filter terminal control characters and avoid interpreting markup;
  • make it clear which host is requesting input;
  • do not guess that every hidden prompt is an account password;
  • do not autofill secrets based only on prompt text.

This matters because a compromised but correctly identified server can still present misleading prompts.

Authentication policy flow

Method choice combines the server’s current policy with the methods, credentials, and preferences available to the client.

flowchart TD
    Failure["USERAUTH_FAILURE<br/>methods + partial flag"] --> Offered["Methods the server says<br/>can continue"]
    Offered --> Available["Methods supported locally<br/>with available credentials"]
    Available --> Rank["Apply configured preference"]
    Rank --> Try["Start exactly one method exchange"]
    Try --> Result{"Server response"}
    Result -->|"SUCCESS"| Done["Start requested service"]
    Result -->|"FAILURE"| Failure
    Result -->|"Method continuation"| Continue["Complete that method's exchange"]
    Continue --> Result

Method-specific message numbers overlap. For example, message number 60 means SSH_MSG_USERAUTH_PK_OK, SSH_MSG_USERAUTH_PASSWD_CHANGEREQ, or SSH_MSG_USERAUTH_INFO_REQUEST depending on the active method. The numeric byte alone does not determine the message’s meaning; the preceding method exchange supplies the necessary context.

A client may abandon an in-progress method by sending a new SSH_MSG_USERAUTH_REQUEST. The server then abandons the previous attempt and continues with the newly named method. Requests still follow the ordering rules in RFC 4252, Section 5.1, so the active method gives method-specific replies their meaning.

Authentication banners

Before success, the server may send SSH_MSG_USERAUTH_BANNER containing text for the user. It does not change method state and is not a prompt. Displaying it is usually helpful, but apply the same control-character filtering used for interactive prompts. See RFC 4252, Section 5.4.

Protocol review

Authentication is complete only when the exchange satisfies all of these protocol properties:

  • the server host identity was accepted before any credential was sent;
  • the transport supplies confidentiality and integrity;
  • the client received exactly one SSH_MSG_USERAUTH_SUCCESS;
  • method-specific messages were interpreted in the context of the active method;
  • a public-key signature covered the exact RFC-defined binary fields;
  • partial success caused another factor to be attempted rather than success; and
  • password and interactive-response fields were recognized as secrets protected by the transport.

Lab: trace a two-factor login

Sketch the messages for a policy requiring publickey followed by keyboard-interactive. Include:

  1. the ssh-userauth service request;
  2. an optional unsigned public-key query;
  3. the signed public-key request;
  4. failure with partial success = true and an updated method list;
  5. a one-prompt keyboard-interactive round; and
  6. the final success message.

For every message, record the active authentication method and whether the packet contains secret data. This exercise exposes both the context-dependent meaning of message number 60 and the difference between a completed method and completed authentication.

References

Channels and flow control

After user authentication, the ssh-connection service multiplexes many logical byte streams over one transport. Each stream is a channel with its own identifiers, flow-control windows, requests, and close state.

RFC 4254 specifies this layer. The next two chapters apply its common rules to sessions and TCP forwarding.

One transport, many channels

SSH multiplexes independent activities into the transport packet stream.

Closing one channel does not close the others or end the SSH transport.

Channel numbers are local

Each peer chooses its own number for the same logical channel. Suppose the client calls a new channel 7 and the server calls it 42:

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: CHANNEL_OPEN(sender=7, window=C_recv, max=C_max)
    S-->>C: OPEN_CONFIRMATION(recipient=7, sender=42,<br/>window=S_recv, max=S_max)
    C->>S: CHANNEL_DATA(recipient=42, data)
    S-->>C: CHANNEL_DATA(recipient=7, data)

The sender channel field introduces the number chosen by the sender. Subsequent messages carry the recipient’s local number. The same logical channel therefore has a pair of identifiers, not one universal channel ID:

local channel 7 <-> remote channel 42

The words sender and recipient are relative to the message carrying the field. Each channel’s identifier pair keeps its data, windows, and lifecycle separate from every other channel.

Opening a channel

Either peer may open a channel. The generic message is:

byte      SSH_MSG_CHANNEL_OPEN
string    channel type
uint32    sender channel
uint32    initial window size
uint32    maximum packet size
...       channel-type-specific fields

The recipient returns SSH_MSG_CHANNEL_OPEN_CONFIRMATION, which introduces its own channel number and receive limits, or SSH_MSG_CHANNEL_OPEN_FAILURE, which contains a reason code and description. Standard failure reasons distinguish administrative prohibition, connection failure, unknown type, and resource shortage. See RFC 4254, Section 5.1.

stateDiagram-v2
    [*] --> Opening: OPEN sent with sender ID and receive limits
    Opening --> Open: OPEN_CONFIRMATION establishes peer ID and limits
    Opening --> Failed: OPEN_FAILURE
    Open --> HalfClosedLocal: send EOF
    Open --> HalfClosedRemote: receive EOF
    HalfClosedLocal --> Closing: send or receive CLOSE
    HalfClosedRemote --> Closing: send or receive CLOSE
    Open --> Closing: send or receive CLOSE
    Closing --> Closed: CLOSE sent and received
    Failed --> [*]
    Closed --> [*]

A channel number does not become reusable merely because one side has sent CLOSE. Under RFC 4254, the channel is closed for a peer only once that peer has both sent and received SSH_MSG_CHANNEL_CLOSE.

Windows and flow control

TCP already has flow control, but one TCP receive buffer is shared by all multiplexed SSH traffic. SSH adds a receive window to each channel and each direction, so a slow consumer can stop its own stream without requiring every channel to stop.

The window advertised in CHANNEL_OPEN or OPEN_CONFIRMATION is a promise: “you may send me this many bytes of channel data.” It describes the sender’s receive capacity, not its send capacity.

For each direction, sending follows this arithmetic:

allowed_channel_data = min(remote_window, remote_max_packet)
remote_window  -= bytes_sent

When the receiver has made capacity available, it sends:

SSH_MSG_CHANNEL_WINDOW_ADJUST(recipient_channel, bytes_to_add)

The sender adds that credit without allowing the 32-bit window to overflow. Data must stop when the remote window reaches zero, but control messages such as window adjustments, EOF, CLOSE, and channel requests do not consume window space.

sequenceDiagram
    participant A as Sender
    participant B as Receiver
    Note over A: remote_window = 10
    A->>B: CHANNEL_DATA(6 bytes)
    Note over A: remote_window = 4
    A->>B: CHANNEL_DATA(4 bytes)
    Note over A: remote_window = 0<br/>pause this channel
    Note over B: application consumes 8 bytes
    B-->>A: CHANNEL_WINDOW_ADJUST(+8)
    Note over A: remote_window = 8<br/>resume

Both SSH_MSG_CHANNEL_DATA and SSH_MSG_CHANNEL_EXTENDED_DATA consume the same channel window. For session channels, extended-data type 1 is stderr. The maximum channel packet size is independent from the transport packet ceiling, and both limits apply. RFC 4254, Section 5.2 defines these rules.

The receiver decides when and by how much to replenish the window. This ties permission to send to the receiver’s capacity to consume data. Until WINDOW_ADJUST adds credit, a zero-window sender must pause that channel even if the underlying TCP connection remains writable.

Requests are not data

SSH has two request scopes:

ScopeMessageExampleReply if requested
Whole connectionSSH_MSG_GLOBAL_REQUESTAsk the server to listen for remote forwardingSSH_MSG_REQUEST_SUCCESS or FAILURE
One channelSSH_MSG_CHANNEL_REQUESTAllocate a PTY or start a commandSSH_MSG_CHANNEL_SUCCESS or FAILURE

Both include a want reply boolean. When it is false, no success or failure reply is sent. When it is true, the generic reply contains no request identifier, so its meaning comes from ordering. Global replies preserve global request order; channel-request replies preserve order within that channel, while replies for different channels may be interleaved. See RFC 4254, Sections 4 and 5.4.

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: CHANNEL_REQUEST(ch=42, "pty-req", want_reply=true)
    C->>S: CHANNEL_REQUEST(ch=42, "shell", want_reply=true)
    Note over S: Reply order on channel 42 is significant
    S-->>C: CHANNEL_SUCCESS(ch=7)
    S-->>C: CHANNEL_SUCCESS(ch=7)

For setup operations whose success affects correctness, request and check a reply. A client that sends exec with no reply and immediately treats bytes as command output cannot distinguish refusal from delayed output or later channel closure.

EOF and CLOSE

SSH_MSG_CHANNEL_EOF means “I will send no more data in this direction.” It is a half-close. No protocol reply is required, and the other direction remains usable.

SSH_MSG_CHANNEL_CLOSE means “terminate this channel.” A recipient must send CLOSE back unless it already did. EOF is conventional before a graceful close but is not required.

sequenceDiagram
    participant C as Client stdin side
    participant S as Remote command
    C->>S: CHANNEL_DATA("request body")
    C->>S: CHANNEL_EOF
    Note over C,S: Client-to-server data direction has ended
    S-->>C: CHANNEL_DATA("final response")
    S-->>C: CHANNEL_REQUEST("exit-status", 0)
    S-->>C: CHANNEL_EOF
    S-->>C: CHANNEL_CLOSE
    C->>S: CHANNEL_CLOSE
    Note over C,S: Both sent and received CLOSE<br/>IDs may be reused

Local stdin reaching EOF ends only the client-to-server data direction; the remote command may still send final output and an exit status. EOF and CLOSE can be sent even when the data window is zero, so exhausted data credit does not make shutdown impossible. See RFC 4254, Section 5.3.

Protocol review

The connection protocol has these invariants:

  • one logical channel has different local IDs at its two endpoints;
  • each direction has independently advertised windows and maximum packet sizes;
  • ordinary and extended data consume the same receive credit;
  • a zero-window channel does not block control traffic or unrelated channels;
  • request replies derive their meaning from scope and ordering;
  • EOF half-closes one direction without discarding the other; and
  • channel IDs are reused only after CLOSE has been both sent and received.

Lab: simulate two channels

Open two channels on paper. Give the client and server different local IDs for each. Then trace this sequence:

  1. The first channel exhausts its server-advertised window.
  2. The second channel continues to deliver data.
  3. The first receiver consumes data and replenishes only that channel.
  4. The second channel’s client sends EOF while still receiving final data.
  5. Both channels complete independent CLOSE handshakes.

At each step, record the recipient channel number and both directional window values. If one exhausted window stalls the other channel, the design is not actually multiplexed.

References

Sessions: commands, terminals, and subsystems

A session channel carries one remote program. The program can be a shell, a command, or a named subsystem. Opening the channel does not start the program; the client starts it with a channel request.

Several session channels can share one SSH connection. Each has independent data, flow control, exit information, and close state. This chapter applies the mechanics from Channels and flow control.

Open, prepare, start

A client first opens a channel of type session. The open message contains no command or terminal settings. RFC 4254 recommends that clients reject server-initiated session opens; the normal direction is client to server.

After confirmation, the client may send setup requests such as env and pty-req. It then sends exactly one start request:

RequestRequest-specific dataMeaning
shellnoneStart the account’s default shell.
execcommand stringAsk the server to execute a command.
subsystemsubsystem nameStart a configured service such as sftp.

Only one of these requests can succeed on a session channel.

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: CHANNEL_OPEN("session", client ID, window, max packet)
    S-->>C: OPEN_CONFIRMATION(client ID, server ID, window, max packet)
    opt Setup
        C->>S: CHANNEL_REQUEST("env" or "pty-req", reply=true)
        S-->>C: CHANNEL_SUCCESS or CHANNEL_FAILURE
    end
    C->>S: CHANNEL_REQUEST("exec", command, reply=true)
    S-->>C: CHANNEL_SUCCESS
    C<<->>S: CHANNEL_DATA
    S-->>C: exit-status, EOF, CLOSE
    C->>S: CLOSE

The request’s want reply flag matters. A client should request a reply when later behavior depends on success. Otherwise a refused exec request can look like a command that produced no output and closed immediately.

exec carries a string, not an argument vector

The exec request contains one SSH string. RFC 4254 does not define an array of arguments, quoting rules, a shell language, or a character encoding for it. The server decides how to interpret the string.

string "exec"
boolean want_reply
string command

A client therefore cannot infer a portable argv boundary from the wire. Quoting rules used by a command-line SSH program belong to that program and the remote execution environment, not to the SSH connection protocol.

A PTY changes the byte-stream environment

pty-req asks the server to attach the remote program to a pseudo-terminal. It supplies a terminal type, character and pixel dimensions, and encoded terminal modes. The request is separate from shell or exec.

string "pty-req"
boolean want_reply
string terminal type
uint32 columns
uint32 rows
uint32 pixel width
uint32 pixel height
string encoded terminal modes

Without a PTY, a session behaves more like ordinary input, output, and error streams. With a PTY, terminal line discipline can echo input, translate bytes, interpret control characters, and combine output streams. A binary protocol or machine-readable command normally should not request a PTY.

After a terminal size changes, the client can send window-change. This is a channel request with the new dimensions. The client should set its want reply flag to false. It is unrelated to SSH_MSG_CHANNEL_WINDOW_ADJUST: terminal dimensions describe a display, while a channel window controls data credit.

The terminal-mode string is its own compact protocol: opcode/value pairs end with TTY_OP_END. It is not a native termios structure and should not be copied as one.

Environment requests are policy requests

An env request carries a name and value. The server may reject it, and many servers accept only configured names. The request does not modify the SSH transport or the server process that handles the connection; it asks the server to construct part of the new program’s environment.

Environment values cross a privilege boundary. A server needs policy for variables that affect library loading, command lookup, localization, or application configuration.

Data and extended data

The client usually sends program input with SSH_MSG_CHANNEL_DATA. The server uses the same message for normal output. It may use SSH_MSG_CHANNEL_EXTENDED_DATA with type SSH_EXTENDED_DATA_STDERR for the error stream.

Both message types consume the same receive window. Their SSH message boundaries do not become record boundaries in the program’s byte stream.

When a PTY is present, the remote terminal normally presents one combined output stream. A client must not assume that extended data will preserve a separate standard-error stream in that case.

Signals and process completion

The client can request signal on a session channel. SSH uses signal names such as TERM, without the SIG prefix. Whether the server can deliver a signal, and what the remote program does with it, is outside the protocol.

The server reports completion with channel requests sent in the other direction:

  • exit-status carries a uint32 process status;
  • exit-signal reports signal termination and may include error text.

These reports do not close the channel. A client can receive final data, an exit report, EOF, and CLOSE as separate events. TCP EOF is not a substitute for an SSH exit status: it ends the entire transport and may leave the command result unknown.

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: DATA(input)
    C->>S: EOF
    Note over C,S: Only client-to-server data ended
    S-->>C: DATA(final output)
    S-->>C: CHANNEL_REQUEST("exit-status", 0, reply=false)
    S-->>C: EOF
    S-->>C: CLOSE
    C->>S: CLOSE

Subsystems

A subsystem is a named protocol service carried inside the session channel. After a successful subsystem request, channel data belongs to that subsystem protocol. SSH still supplies transport protection, multiplexing, flow control, and channel closure, but it does not interpret the subsystem messages.

SFTP is the common example. “SFTP over SSH” means that SFTP packets are the application byte stream of a session channel; SFTP is not an SSH channel type and is not the same protocol as SCP.

Trace a command correctly

For one command channel, record these events separately:

  1. channel open confirmation;
  2. optional setup-request results;
  3. start-request result;
  4. input EOF, if sent;
  5. normal and extended output;
  6. exit status or exit signal, if sent; and
  7. any EOF events, then CLOSE sent and received.

This prevents three common mistakes: treating channel open as command start, treating EOF as full close, and treating transport loss as a zero exit status.

References

TCP forwarding

TCP forwarding uses SSH channels as byte relays. It lets one endpoint request a connection from the other endpoint’s network position. Channel identifiers, windows, and close behavior follow Channels and flow control.

The familiar labels local, dynamic, and remote forwarding describe local client behavior. On the SSH wire, the core mechanisms are direct-tcpip channels and a combination of tcpip-forward requests with forwarded-tcpip channels.

User-facing modeListenerSSH action for each connectionPeer that connects to target
Local forwardingClientClient opens direct-tcpipServer
Dynamic forwardingClientClient translates SOCKS, then opens direct-tcpipServer
Remote forwardingServerServer opens forwarded-tcpipClient

Direct TCP channels

A direct-tcpip open asks the SSH peer to make an outbound TCP connection:

string    "direct-tcpip"
uint32    sender channel
uint32    initial window size
uint32    maximum packet size
string    host to connect
uint32    port to connect
string    originator IP address
uint32    originator port

The client normally sends this open to the server. RFC 4254 recommends that clients reject server-initiated direct-tcpip opens.

For a conceptual local forward -L 127.0.0.1:8080:db.internal:5432, the client listens locally. Each accepted connection creates a new channel:

sequenceDiagram
    participant A as Local application
    participant C as SSH client
    participant S as SSH server
    participant T as db.internal:5432
    A->>C: Connect to 127.0.0.1:8080
    C->>S: OPEN direct-tcpip<br/>target db.internal:5432
    S->>T: TCP connect
    alt Target connected
        T-->>S: Connected
        S-->>C: OPEN_CONFIRMATION
        A<<->>T: Bytes in channel data
    else Connect failed or policy denied
        S-->>C: OPEN_FAILURE
    end

The server resolves the target name and makes the connection. The target may be visible from the server’s network but not from the client. The originator address is peer-supplied context; it is not an authenticated user identity.

Dynamic forwarding differs before the channel opens. The SSH client accepts a SOCKS request, extracts its destination, and opens a direct-tcpip channel for that destination. SOCKS messages do not cross the SSH connection.

Requesting a remote listener

Remote forwarding has two levels of state:

  1. A tcpip-forward global request creates a server-side listener.
  2. Each accepted TCP connection creates a forwarded-tcpip channel.
sequenceDiagram
    participant C as SSH client
    participant S as SSH server
    participant R as Remote application
    participant T as Client-side target
    C->>S: GLOBAL_REQUEST tcpip-forward<br/>bind address and port, reply=true
    S-->>C: REQUEST_SUCCESS
    R->>S: Connect to server listener
    S->>C: OPEN forwarded-tcpip<br/>connected address and originator
    C->>T: TCP connect
    alt Target connected
        T-->>C: Connected
        C-->>S: OPEN_CONFIRMATION
        R<<->>T: Bytes in channel data
    else Target failed or open was not authorized
        C-->>S: OPEN_FAILURE
    end

The forwarded-channel fields contain the server address that received the connection and the reported originator. They do not contain the client-side target. The client remembers that target as part of the forwarding request’s local state.

If the requested port is zero, the server allocates a port. A successful reply then carries the allocated port as a uint32. The client needs that value before it can report where the listener exists.

Cancellation and races

The client cancels a listener with cancel-tcpip-forward, using the same bind address and actual listener port. If the request used port zero, this means the allocated port returned in REQUEST_SUCCESS, not zero. The client should wait for a reply when it needs a clear end boundary.

A forwarded connection can race with cancellation. A channel open that was already in flight may arrive before the cancellation result. The client must match each forwarded-tcpip open to forwarding state that was authorized when the server accepted the connection; it must reject unsolicited opens.

Closing a shell channel does not cancel a listener or close existing forwarded channels. They are independent connection-protocol objects. Ending the SSH transport ends all of them.

Where protection ends

SSH protects channel data only between the SSH client and SSH server.

For local or dynamic forwarding, the server-to-target connection needs its own application security if the target network is untrusted. SSH does not verify the target’s identity.

For remote forwarding, listener scope controls exposure. A loopback bind normally limits who can connect on the server. A wildcard bind can expose the client-side target to other hosts that can reach the server.

Forwarding also changes reachability. A direct channel may reach an internal server-side service. A remote listener may expose a private client-side service. Server and client policy can restrict bind addresses, ports, and targets before granting these capabilities.

X11 forwarding, agent forwarding, and Unix-domain-socket forwarding use additional channel types or requests. They are not alternative spellings of TCP forwarding.

Agent forwarding has a particularly sharp boundary: the remote host receives access to a signing service, not the private-key bytes. A compromised remote host may still ask that service to sign while the forwarding channel is open. Key constraints and host-bound authentication can narrow this delegated capability, but ordinary SSH transport protection does not remove it.

Check the viewpoint

  1. Who resolves a direct-tcpip target name?
  2. Where is SOCKS interpreted during dynamic forwarding?
  3. What persistent state links a forwarded-tcpip open to its client-side target?
  4. Does closing a session channel cancel a remote-forwarding listener?
  5. Which links in a local forward are protected by SSH?

References

Security properties and boundaries

SSH is often summarized as “an encrypted connection,” but that description is too weak. Its security comes from a chain of distinct claims, and each claim has a boundary.

The chain of claims

The claims build in order: reliable stream, signed key exchange, trusted server identity, protected packets, authenticated user, and authorized channel operations.

Breaking any link changes what later success means. A password sent through an encrypted tunnel is not safe if the tunnel terminates at an attacker. A valid user signature does not authorize a port forward unless server policy allows it. A trusted server may still run a compromised operating system.

The full security discussion spans RFC 4251 section 9 and the security considerations of every algorithm-specific RFC.

What the network attacker can do

Before host authentication completes, assume an active network attacker can read, change, delete, delay, or replace traffic. If the attacker changes a covered value while relaying one exchange, the endpoints form different exchange hashes and signature verification fails.

A full machine-in-the-middle can instead run two self-consistent exchanges and sign the client-facing exchange with its own host key. The destination-to-key trust check must reject that key. Together, transcript binding and host-key trust prevent an invisible downgrade of signed proposals. Neither prevents denial of service: an attacker can always drop the connection.

After keys are active, confidentiality hides packet contents and integrity protection detects changes. Packet authentication does not guarantee delivery; the peer or network can still stop sending.

Server proof and server trust

The key-exchange signature proves control of the private key corresponding to the presented host key. A separate trust rule binds that key to the destination the user intended.

Trust on first use can detect a later key change, but it cannot identify an attacker present on the first connection. A changed key can be a legitimate rotation or an attack; the protocol cannot decide which. That decision needs information from outside the connection.

Algorithm negotiation and downgrade resistance

KEXINIT chooses the first client-preferred algorithm also offered by the server. The signed exchange hash covers both complete KEXINIT payloads, so a network attacker cannot delete strong choices and still produce a valid host signature.

The authenticated negotiation can still select a weak algorithm if both peers offer it and client policy prefers or permits it. Transcript integrity cannot repair unsafe policy.

An implementation may understand an algorithm without enabling or offering it. Of the offered algorithms, only mutual names can be selected. Client preference chooses among those names.

Registration is not endorsement. The IANA SSH registries record interoperable names, while documents such as RFC 9142 give updated security and implementation recommendations.

Forward secrecy and rekeying

Ephemeral key agreement means later theft of the server’s long-term host key should not reveal old recorded traffic keys. This is forward secrecy. It depends on fresh ephemeral secrets being generated and discarded.

Rekeying creates new traffic keys for a live connection. It limits the amount of data protected under one key set and respects algorithm-specific usage limits. It does not undo exposure that has already occurred and does not change the original session identifier.

timeline
    title Key lifetimes in one SSH connection
    Initial KEX : ephemeral values produce traffic keys 1
                : first exchange hash becomes session identifier
    Protected traffic : traffic keys 1 active
    Rekey : new ephemeral values produce traffic keys 2
          : session identifier remains unchanged
    More traffic : traffic keys 2 active

Hybrid post-quantum key exchange addresses a different future threat: an adversary recording traffic now and later gaining a cryptographically relevant quantum computer. It strengthens secret establishment. A traditional host signature remains traditional server authentication, even when the KEX itself is hybrid.

What remains visible

SSH does not hide the network endpoints, connection duration, or all traffic patterns. The identification strings and initial key-exchange packets are visible before NEWKEYS. Later encryption hides message contents, but an observer can still see packet timing and some representation of packet sizes.

Random padding obscures exact content lengths to a degree; it does not make SSH a traffic-flow confidentiality system. See RFC 4251 section 9.3.9.

Authentication is only as strong as allowed policy

A server can require one method or a sequence of factors. If it accepts either a strong public-key proof or a weak password, an attacker may target the weaker route. If it requires both, partial success carries the conversation from the first accepted method to the next.

Passwords and keyboard-interactive responses are protected on the network, but the authenticated server receives them. Public-key authentication instead proves possession by signing connection-bound data; the private key need not leave the client or signing agent.

Channels cross new trust boundaries

Connection-layer protection ends at the SSH endpoints.

For a direct-tcpip channel, traffic from the server to the target is not protected by SSH unless that application protocol supplies its own protection. A remote forward may expose a client-side service to machines which can reach the server’s listener. Agent forwarding delegates access to a signing service through the remote host. Each feature creates a capability which server and client policy must deliberately authorize.

Endpoint security is assumed

SSH cannot protect plaintext from a compromised client or server. A malicious server can capture commands, terminal input, forwarded data, passwords, and interactive responses which legitimately reach it. A compromised client can steal credentials before the protocol protects them.

Host authentication answers “which SSH host key participated?” It is valuable, but it is not an attestation that the remote operating system, account, command, or forwarded destination is trustworthy.

Security review

For any point in a connection, ask:

  1. Which identity has been authenticated so far?
  2. Which bytes are confidential, integrity-protected, or still observable?
  3. Which exact transcript or sequence state is authenticated?
  4. Which decision comes from protocol proof, and which comes from local policy?
  5. Where does the SSH protection end for this channel’s data?

These questions give a more accurate security description than the single word “encrypted.”

Reading traces and diagnosing failures

An SSH trace should reconstruct each peer’s protocol state, message direction, and active security properties. A list of log lines is only source material.

Three views of one connection

flowchart LR
    Capture["Network capture<br/>segments · visible bytes · timing"] --> Model["Protocol reconstruction"]
    Client["Client debug log<br/>local choices · state · errors"] --> Model
    Server["Server debug log<br/>policy · accepted requests"] --> Model

A packet capture shows what crossed an observation point, but after NEWKEYS it normally cannot show message contents. A client log shows what the client believed it sent or received, often after decoding and validation. A server log can reveal why local policy rejected a request. None is a perfect, independent transcript of the whole system.

Debug output may contain hostnames, usernames, paths, fingerprints, commands, and authentication details. Sanitize it before sharing. Peer-supplied banner or error text is untrusted even when it appears in a trusted program’s log.

Annotate the phase first

Start by placing every observation on the connection timeline.

timeline
    title SSH diagnosis boundaries
    Byte stream : address and connection result
    Identification : protocol and software versions
    Negotiation : both proposals and selected algorithms
    Key exchange : shared-secret computation and host proof
    Host trust : destination-to-key decision
    User authentication : methods, factors, success
    Connection protocol : channels, requests, flow control, close

The last completed boundary narrows the failure dramatically. For example, a host-key warning shows that negotiation reached presentation and trust checking of the host key. It does not prove whether signature verification happened first; clients may order those two checks differently.

Keep observations and inferences separate

Suppose a client log says:

kex: algorithm: curve25519-sha256
kex: host key algorithm: ssh-ed25519

This directly reports the client’s selected names. You may infer that each name appeared in both KEXINIT proposals and that it was the first mutually supported value in the client’s corresponding list. To prove that inference from the wire, you would need both complete proposals.

Similarly, seeing ciphertext after NEWKEYS proves that protected packet bytes were exchanged, not that user authentication or a command succeeded.

TCP segments are not SSH packets

A capture tool presents TCP segments according to where it observed the stream. SSH packet boundaries can cross segments, and one segment can contain several SSH packets. Retransmissions are TCP behavior and do not mean SSH processed the same logical bytes twice.

Before binary packets, each direction is line-oriented. After either endpoint sends its identification line, its next byte begins the binary stream in that direction. The two directions can cross this boundary at different times.

Diagnose by boundary

flowchart TD
    Fail["Connection failed"] --> TCP{"Byte stream opened?"}
    TCP -- no --> IO["Address · route · refusal · timeout"]
    TCP -- yes --> ID{"Identification accepted?"}
    ID -- no --> Version["Banner · protocol version · line grammar"]
    ID -- yes --> KEX{"Algorithms selected?"}
    KEX -- no --> Lists["Client policy versus server offers"]
    KEX -- yes --> Host{"Server authenticated?"}
    Host -- no --> Trust["Exchange signature · name · trust binding"]
    Host -- yes --> User{"User authenticated?"}
    User -- no --> Cred["Offered method · credential · server policy"]
    User -- yes --> Chan["Channel · request · window · EOF/CLOSE"]

No matching algorithm

Compare the relevant KEXINIT category, not a single undifferentiated list. A failure might concern the KEX method, host-key algorithm, one direction’s cipher, MAC, or compression. The correct selection is the first client-listed value also present on the server list.

Do not solve the mismatch by blindly enabling every legacy name. Determine whether one side can offer a mutually acceptable current algorithm and why its policy excluded that choice.

Host-key conflict

A changed-host-key failure is not a user-authentication failure. Record the requested destination identity, effective port or host-key alias, route through any jump host, presented fingerprint, and stored trust rule. Verify a legitimate rotation through an independent channel before updating the narrow binding.

User authentication rejected

Track each SSH_MSG_USERAUTH_FAILURE as a pair: its current method list and partial success flag. A valid public-key signature can be accepted as one factor without completing authentication. Method number 60 must be interpreted from the active method state, not from the number alone.

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: signed publickey request
    S-->>C: FAILURE(keyboard-interactive, partial=true)
    Note over C,S: Public key factor succeeded<br/>authentication is not complete
    C->>S: keyboard-interactive exchange
    S-->>C: SUCCESS

A channel appears to hang

Check the two channel directions independently. A sender with zero remote window must wait for WINDOW_ADJUST; the other channel direction and other logical channels may continue. Local EOF ends only one data direction. Final output and exit status may legitimately arrive after the client has sent EOF.

flowchart TD
    Stall["No channel data moving"] --> W{"Remote window zero?"}
    W -- yes --> Adj["Look for receiver consumption<br/>and WINDOW_ADJUST"]
    W -- no --> Req{"Start request accepted?"}
    Req -- no --> Setup["OPEN / request success or failure"]
    Req -- yes --> Close{"EOF or CLOSE observed?"}
    Close -- EOF only --> Half["Only one direction ended"]
    Close -- CLOSE both ways --> Done["Channel finished"]

Read disconnects in context

SSH_MSG_DISCONNECT carries a reason code, description, and language tag. The reason is useful evidence, but the description is peer-controlled text and may be vague. Abrupt TCP EOF carries no SSH reason at all. A packet authentication failure may intentionally reveal little because processing unauthenticated data would be unsafe.

The message-number and disconnect-reason registries are collected by IANA, with the original transport behavior in RFC 4253 section 11.

Trace review

For each important line or packet, write down:

  • its direction and protocol layer;
  • the state before and after it;
  • whether its contents were cleartext or protected;
  • which identity or channel it concerned; and
  • whether your conclusion is directly observed or inferred from a protocol rule.

That discipline turns a verbose log into a protocol explanation and prevents a late symptom—such as a closed channel—from being mistaken for the earlier cause.

Reference map

The SSH specifications are a family, not one linear document. Use this map to find the authoritative field layout or behavior behind a course explanation.

Core documents

Algorithms and extensions used in this course

Authentication and key representation

Reading rules

Core RFCs are old enough that later documents update parts of them. Read the “Updates” and “Updated by” metadata on the RFC Editor page, and separate three questions:

  1. Is the wire format assigned and understood?
  2. Is the algorithm recommended for a new implementation today?
  3. Is it allowed by this client’s deployment policy?

Those answers can differ. IANA registration records interoperability names; it does not by itself make an algorithm secure or appropriate.

The OpenSSH project also documents widely deployed extensions on its specifications page. Treat an active Internet-Draft or vendor extension as such until it is published as an RFC.