What SSH Actually Is
SSH — Secure Shell — is a cryptographic network protocol for operating network services securely over an unsecured network. At its core, it is a client-server architecture: an SSH client initiates a connection, and an SSH daemon (sshd) listens on the server, typically on port 22. Every piece of data that travels between them is encrypted, authenticated, and integrity-protected.
What most developers interact with day-to-day — typing ssh user@host — is the tip of an enormous iceberg. Beneath that single command lies a precisely ordered sequence of cryptographic handshakes, key negotiations, and protocol layers that happen in milliseconds.
The Protocol Stack: Three Layers
The SSH protocol is formally defined in a family of RFCs (RFC 4251–4254) and is composed of three distinct sub-protocols stacked on top of TCP:
┌─────────────────────────────────────────┐
│ SSH Connection Protocol │ ← channels, sessions, port forwarding
├─────────────────────────────────────────┤
│ SSH User Auth Protocol │ ← password, publickey, keyboard-interactive
├─────────────────────────────────────────┤
│ SSH Transport Layer Protocol │ ← encryption, MAC, key exchange
├─────────────────────────────────────────┤
│ TCP │
└─────────────────────────────────────────┘
Each layer has a precise, well-defined job. Let's walk through each one in order of execution.
Stage 1 — TCP Connection and Version Exchange
Everything begins with a plain TCP handshake to port 22. Once the TCP connection is established, both sides immediately send a cleartext version string:
SSH-2.0-OpenSSH_9.6
This string declares the SSH protocol version (always 2.0 in modern usage — SSH-1 is deprecated and broken) and the software implementation. Both sides read each other's version string, and if they are incompatible, the connection closes immediately. This is the only plaintext exchange in the entire session. Everything after this is encrypted.
Stage 2 — The Transport Layer: Key Exchange (KEX)
This is the most cryptographically dense part of SSH. The goal is to establish a shared secret between client and server without ever transmitting that secret across the wire — even in encrypted form. This is achieved through a Key Exchange Algorithm, most commonly Diffie-Hellman (DH) or its elliptic-curve variant ECDH.
Algorithm Negotiation
Before the actual key exchange begins, client and server negotiate which algorithms to use. Both sides send a SSH_MSG_KEXINIT packet listing their supported algorithms in preference order for each category:
-
Key Exchange algorithms:
curve25519-sha256,ecdh-sha2-nistp256,diffie-hellman-group14-sha256 -
Host key algorithms:
ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-512 -
Encryption ciphers:
chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-ctr -
MAC algorithms:
hmac-sha2-256,hmac-sha2-512,umac-128-etm@openssh.com -
Compression:
none,zlib@openssh.com
The agreed algorithm for each category is the first one in the client's list that the server also supports.
The Diffie-Hellman Key Exchange
The actual key exchange works as follows (using DH as the canonical example):
- Both sides agree on a large prime
pand a generatorg(these are public, standardized values). - The client generates a random private integer
x, computese = g^x mod p, and sendseto the server. - The server generates a random private integer
y, computesf = g^y mod p, and sendsfto the client. - The client computes the shared secret
K = f^x mod p. - The server computes the shared secret
K = e^y mod p.
Both arrive at the same value of K — the shared secret — without either side ever transmitting x or y. An eavesdropper who sees e and f cannot derive K without solving the discrete logarithm problem, which is computationally infeasible for sufficiently large primes.
With modern OpenSSH, Curve25519 is the preferred KEX algorithm. It uses elliptic-curve Diffie-Hellman (ECDH) over the Curve25519 elliptic curve, which offers 128-bit security with keys far smaller than classic DH, and has been designed to resist side-channel attacks.
Host Key Verification and the Exchange Hash
The key exchange alone doesn't prevent a man-in-the-middle attack. An attacker could intercept both sides and run two separate key exchanges. This is where the server's host key comes in.
After computing the shared secret K, the server assembles an exchange hash H:
H = hash(client_version || server_version || client_kexinit || server_kexinit || server_host_public_key || e || f || K)
The server then signs H with its private host key (e.g., an Ed25519 key stored in /etc/ssh/ssh_host_ed25519_key). This signature, along with the server's public host key, is sent to the client.
The client must now decide: do I trust this host key?
- If the client has seen this server before, it checks
~/.ssh/known_hostsfor a matching entry. - If the key matches, the client verifies the signature over
Husing that public key. A valid signature proves that whoever sent this data possesses the corresponding private host key — i.e., this is the real server. - If the key is new, the client prompts the user:
The authenticity of host X can't be established. Are you sure you want to continue?
This is the famous TOFU (Trust On First Use) model. It is the primary defense against man-in-the-middle attacks.
Session Key Derivation
From the shared secret K and the exchange hash H, both sides independently derive the same set of symmetric session keys using a KDF (Key Derivation Function):
Encryption key (client → server): hash(K || H || "C" || session_id)
Encryption key (server → client): hash(K || H || "D" || session_id)
IV (client → server): hash(K || H || "A" || session_id)
IV (server → client): hash(K || H || "B" || session_id)
MAC key (client → server): hash(K || H || "E" || session_id)
MAC key (server → client): hash(K || H || "F" || session_id)
Separate keys for each direction means a compromise of one direction doesn't compromise the other. After this, both sides send SSH_MSG_NEWKEYS to signal that all subsequent packets will be encrypted with these session keys. The transport layer is now live.
Stage 3 — The User Authentication Protocol
With an encrypted, integrity-protected channel established, the server now knows the client can communicate securely — but it doesn't yet know who the client is. That's what the User Auth protocol resolves.
The client sends SSH_MSG_SERVICE_REQUEST for ssh-userauth. The server confirms, and authentication begins.
Password Authentication
The simplest method. The client sends the username and password, encrypted inside the already-secure channel. The server verifies against /etc/shadow (or PAM, or LDAP). If correct, authentication succeeds.
This method is discouraged in production because it is vulnerable to brute force and credential stuffing. Most hardened servers disable it entirely with PasswordAuthentication no in sshd_config.
Public Key Authentication
This is the gold standard. The protocol works like this:
- The client declares intent: "I want to authenticate as user
aliceusing this public key." - The server checks whether that public key is listed in
~/.ssh/authorized_keysfor useralice. - If found, the server sends a challenge: a unique blob of data.
- The client signs the challenge with the corresponding private key (stored in
~/.ssh/id_ed25519or similar). - The client sends the signature back.
- The server verifies the signature with the public key. Only the holder of the private key could have produced a valid signature. Authentication succeeds.
The private key never leaves the client machine. Not even a fragment of it crosses the wire. This is what makes public key authentication so robust.
SSH Agent and Agent Forwarding
In practice, private keys are often protected by passphrases. Typing the passphrase every time would be impractical. The SSH agent (ssh-agent) solves this: it holds decrypted private keys in memory and performs signing operations on behalf of the SSH client. The client communicates with the agent over a local Unix socket (stored in $SSH_AUTH_SOCK).
Agent forwarding (ssh -A) extends this: when you SSH from machine A to machine B, and then from B to machine C, the signing requests can be forwarded back through the chain to the agent on machine A. Machine B never sees the private key. This is extremely convenient but carries risk — anyone with root access on machine B can use your agent socket to impersonate you on machine C.
Certificate-Based Authentication
Modern SSH deployments at scale use SSH certificates instead of raw public keys. A Certificate Authority (CA) signs user or host public keys, embedding authorized principals, validity periods, and extension flags. The authorized_keys approach requires every server to list every authorized user's public key. With certificates, every server only needs to trust the CA's public key, and the CA issues short-lived certificates to users. This dramatically simplifies key management at scale.
Stage 4 — The Connection Protocol: Multiplexed Channels
After authentication, the SSH Connection Protocol takes over. It multiplexes multiple logical channels over the single encrypted TCP connection. Each channel is identified by a number and can carry different types of traffic simultaneously.
Channel Types
-
session: A remote command execution or interactive shell. This is what you get when you simply runssh user@host. -
direct-tcpip: Local port forwarding. Traffic sent to a local port is tunneled to a destination via the SSH server. -
forwarded-tcpip: Remote port forwarding. The server forwards traffic from one of its ports through the SSH tunnel to the client. -
x11: X11 forwarding — tunneling graphical application display sessions.
How Channels Work
Opening a channel:
Client → Server: SSH_MSG_CHANNEL_OPEN (type="session", sender_channel=0, window_size=2MB, max_packet=32KB)
Server → Client: SSH_MSG_CHANNEL_OPEN_CONFIRMATION (recipient_channel=0, sender_channel=1, ...)
Each channel has independent flow control via window sizes — the sender cannot push more data than the receiver's advertised window allows. When the receiver processes data, it sends SSH_MSG_CHANNEL_WINDOW_ADJUST to expand the window.
Data flows inside SSH_MSG_CHANNEL_DATA packets. The channel is closed with SSH_MSG_CHANNEL_CLOSE. Multiple channels can be open simultaneously, all multiplexed over the single TCP connection.
Interactive Shell Sessions
When you want an interactive shell, the client requests a PTY (Pseudo-Terminal):
SSH_MSG_CHANNEL_REQUEST (request-type="pty-req", term="xterm-256color", columns=220, rows=50, ...)
SSH_MSG_CHANNEL_REQUEST (request-type="shell")
The server allocates a PTY pair: a master side (controlled by sshd) and a slave side (the terminal seen by the remote shell). Terminal resize events are sent with SSH_MSG_CHANNEL_REQUEST (request-type="window-change"). For non-interactive commands (ssh user@host ls -la), the PTY request is skipped and only exec is requested.
Port Forwarding: SSH as a Tunnel
SSH's channel mechanism enables powerful tunneling capabilities.
Local Port Forwarding
ssh -L 5432:db.internal:5432 user@jumphost
Instructs the SSH client to listen on local port 5432. Any connection to that port is wrapped in an SSH channel and forwarded to db.internal:5432 as seen from the jump host. The traffic between your machine and the jump host is encrypted; the jump host connects to the database in plaintext (or its own encrypted connection).
Remote Port Forwarding
ssh -R 8080:localhost:3000 user@publicserver
The SSH server listens on publicserver:8080. Connections to that port are tunneled back through SSH to localhost:3000 on your machine. This is how developers expose local development servers to the internet without a public IP.
Dynamic Port Forwarding (SOCKS Proxy)
ssh -D 1080 user@host
Creates a SOCKS5 proxy on local port 1080. Applications configured to use this proxy send their traffic through the SSH tunnel, with the server making outbound connections on their behalf. This turns an SSH server into a makeshift VPN exit node.
Packet Structure: What's Actually on the Wire
Every SSH packet after the transport layer handshake follows this binary structure:
┌──────────────────────┬──────────────┬───────────┬─────────────────┬──────────────┐
│ packet_length (4B) │ padding (1B) │ payload │ random_padding │ MAC (0-32B) │
└──────────────────────┴──────────────┴───────────┴─────────────────┴──────────────┘
-
packet_length: Length of everything that follows, excluding the MAC. -
padding_length: How many bytes of random padding follow the payload. -
payload: The actual message (message type byte + content). -
random_padding: Random bytes to ensure the total packet size is a multiple of the cipher's block size, and to prevent traffic analysis based on message length. -
MAC: Message Authentication Code, computed oversequence_number || packet_length || padding_length || payload || random_padding. This detects any tampering in transit.
With AEAD ciphers like chacha20-poly1305 or aes256-gcm, the MAC is integrated into the cipher tag — there is no separate MAC field.
The known_hosts File and TOFU
Every time you connect to a new SSH server, OpenSSH stores the server's host key in ~/.ssh/known_hosts:
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
On every subsequent connection, OpenSSH verifies that the server presents the same host key. If it has changed, you see the famous warning:
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
This can mean a legitimate server rebuild — or a man-in-the-middle attack. Never dismiss this warning casually.
The sshd_config and ssh_config Files
The server's behavior is controlled by /etc/ssh/sshd_config. Key directives:
Port 22
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
AllowUsers alice bob
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 60
ClientAliveCountMax 3
The client's behavior is controlled by ~/.ssh/config, which allows per-host configuration:
Host myserver
HostName 203.0.113.42
User alice
IdentityFile ~/.ssh/id_ed25519_myserver
ForwardAgent yes
ServerAliveInterval 60
Algorithm Security Reference
| Algorithm | Type | Security | Notes |
|---|---|---|---|
curve25519-sha256 |
KEX | ✅ Strong | Default in modern OpenSSH |
diffie-hellman-group14-sha1 |
KEX | ⚠️ Weak | SHA-1 deprecated |
ssh-ed25519 |
Host/User key | ✅ Strong | Preferred key type |
ecdsa-sha2-nistp256 |
Host/User key | ✅ Good | NIST curve, widely supported |
ssh-rsa |
Host/User key | ⚠️ Legacy | Disabled by default in OpenSSH 8.8+ |
chacha20-poly1305 |
Cipher | ✅ Strong | Resistant to timing attacks |
aes256-gcm |
Cipher | ✅ Strong | Hardware-accelerated on modern CPUs |
aes128-ctr |
Cipher | ✅ Acceptable | Requires separate MAC |
hmac-sha2-256-etm |
MAC | ✅ Strong | Encrypt-then-MAC is the correct order |
hmac-sha1 |
MAC | ❌ Broken | SHA-1 is cryptographically deprecated |
What Happens When You Type ssh user@host
A complete timeline:
t=0ms TCP SYN → server:22
t=1ms TCP SYN-ACK ← server
t=1ms TCP ACK → server
t=1ms "SSH-2.0-OpenSSH_9.6\r\n" → server
t=2ms "SSH-2.0-OpenSSH_9.6\r\n" ← server
t=2ms SSH_MSG_KEXINIT → server (client algorithm list)
t=2ms SSH_MSG_KEXINIT ← server (server algorithm list)
t=2ms SSH_MSG_KEX_ECDH_INIT → server (client's ephemeral public key)
t=3ms SSH_MSG_KEX_ECDH_REPLY ← server (server host key + ephemeral key + signature)
[client verifies host key against known_hosts]
[client verifies signature]
[both sides derive session keys]
t=3ms SSH_MSG_NEWKEYS → server
t=3ms SSH_MSG_NEWKEYS ← server
*** All subsequent packets are encrypted ***
t=3ms SSH_MSG_SERVICE_REQUEST("ssh-userauth") → server
t=4ms SSH_MSG_SERVICE_ACCEPT ← server
t=4ms SSH_MSG_USERAUTH_REQUEST(publickey, ed25519, pubkey) → server
t=4ms SSH_MSG_USERAUTH_PK_OK ← server (yes, I know that key)
t=4ms SSH_MSG_USERAUTH_REQUEST(publickey, ed25519, pubkey, signature) → server
t=5ms SSH_MSG_USERAUTH_SUCCESS ← server
t=5ms SSH_MSG_CHANNEL_OPEN("session") → server
t=5ms SSH_MSG_CHANNEL_OPEN_CONFIRMATION ← server
t=5ms SSH_MSG_CHANNEL_REQUEST("pty-req") → server
t=5ms SSH_MSG_CHANNEL_SUCCESS ← server
t=5ms SSH_MSG_CHANNEL_REQUEST("shell") → server
t=5ms SSH_MSG_CHANNEL_SUCCESS ← server
*** Interactive shell is live ***
Total elapsed time: ~5–20ms on a typical LAN.
Conclusion
SSH is one of the most elegantly engineered protocols in existence. Its layered architecture cleanly separates transport security, identity verification, and application multiplexing. The cryptographic foundations — ephemeral key exchange, asymmetric authentication, symmetric encryption with integrity protection — work in concert to provide confidentiality, authentication, and integrity across an untrusted network.
Every time you type ssh user@host, this entire machinery executes in under a second. Understanding it not only demystifies a tool you use daily, but gives you the knowledge to configure it securely, debug it when things go wrong, and reason clearly about what guarantees it actually provides — and where those guarantees end.
Top comments (0)