// How SSH works end-to-end — connection, auth, tunnels, and the crypto underneath
0 / 9
OVERVIEW
How SSH establishes a secure channel
SSH (Secure Shell) replaces telnet and rlogin by wrapping every byte in authenticated encryption. Before you type a single command, it completes a handshake that establishes:
① A shared secret — using ECDH key exchange (never transmitted over the wire)
② Identity verification — confirming you are talking to the right server
③ User authentication — proving who you are to the server
Press Step or Play to walk through each phase.
Port 22/TCP · Protocol: SSH-2.0 · Replaces: telnet, rsh, rlogin
Client State
CLOSED
Server State
LISTEN :22
💻
Client
your machine
🖥
Server
:22 / sshd
packet inspector
—
status
Waiting for first step…
// Why SSH replaced Telnet — what an attacker sees on the wire
⚠ Telnet (plaintext)
Captured on wire:login: aliPassword: MyS3cr3tP@ss$ cat /etc/passwdroot:x:0:0:root:/root:/bin/bashali:x:1000:1000::/home/ali:/bin/bash
The client sends your password encrypted inside the SSH tunnel — not in plaintext like Telnet. The server decrypts it and passes it to PAM (Pluggable Authentication Module) which hashes it and compares against /etc/shadow. Simple to use, but vulnerable to brute-force if the server is exposed to the internet.
Authentication flow
client
SSH_MSG_USERAUTH_REQUEST (method=none)
Client asks: "What auth methods do you accept?" Sends a probe with method=none first.
Client says: "I want to log in as ali, using this public key." It sends the key fingerprint — NOT the private key. Private key never leaves the machine.
server
Check ~/.ssh/authorized_keys for this key fingerprint
Scans authorized_keys line by line. If the key is found, server proceeds. If not found → USERAUTH_FAILURE.
server
🔒 SSH_MSG_USERAUTH_PK_OK — key accepted, now prove you own the private key
Server knows the public key is authorized. Now it needs proof that the client actually has the matching private key — the challenge-response begins.
local
Sign(session_id ‖ username ‖ "ssh-connection" ‖ public_key) using id_ed25519
Client creates a cryptographic signature over a message that includes the session ID (unique to this connection). Private key is used locally — never sent. If key has a passphrase, the user is prompted here.
Sends the signature. The signature proves: "I have the private key that matches the authorized public key." Replay attacks are prevented because the signature includes the unique session ID.
Uses the public key from authorized_keys to verify the signature. If verification passes, the client provably owns the private key.
server
🔒 SSH_MSG_USERAUTH_SUCCESS
Authenticated. The private key was never transmitted — only the signature.
Pros
Private key never leaves your machine
Immune to brute-force (no password to guess)
Multiple keys = per-device audit trail
Works with hardware security keys (YubiKey)
Can be passphrase-protected + stored in ssh-agent
Considerations
One-time setup required (keygen + copy)
Must protect private key file (chmod 600)
Scale problem: authorized_keys per server adds up
Solution for scale: use certificate auth (next tab)
Certificate Auth — scaling public keys for enterprise
Instead of copying your public key to every server's authorized_keys, a Certificate Authority (CA) signs your public key. Servers only need to trust the CA — no per-server key distribution. Certs can have expiry times (e.g. 8 hours), so a leaked cert automatically becomes useless.
01
User has a key pair
id_ed25519 + id_ed25519.pub — same as regular public key auth.
Client presents the signed certificate (not just a public key). The cert contains: the public key + CA signature + principals + expiry.
server
Verify CA signature on cert → check expiry → check principals → challenge-response
Server verifies: (1) cert was signed by the trusted CA, (2) cert is not expired, (3) principal matches the login username, (4) client actually has the private key (same challenge-response as public key auth).
server
🔒 SSH_MSG_USERAUTH_SUCCESS
Authenticated without any per-user authorized_keys file on the server.
Pros
No authorized_keys management per server
Short-lived certs auto-expire (no manual revocation)
Centralized access control via CA
Audit: every cert says WHO issued it and when
Works with HashiCorp Vault SSH Secrets Engine
Considerations
Requires a CA infrastructure to set up
CA key compromise = all certs compromised
More complex than simple public key auth
Used by: Uber, Facebook, Netflix at scale
SSH is not just a remote shell — it is a general-purpose encrypted transport layer. Once the SSH connection is established, you can route any TCP traffic through it. This is SSH port forwarding, and it is one of the most powerful and underused features in networking.
Use case: Access an internal database through a bastion host
The database server is not exposed to the internet — it only accepts connections from inside the corporate network. The bastion host is the only internet-facing server.
This command creates a listener on YOUR machine at port 5433. When your app (e.g. DBeaver, psql) connects to localhost:5433, SSH forwards that traffic through the encrypted tunnel to the bastion, which then connects to db.internal:5432.
1
You connect: psql -h localhost -p 5433 -U ali mydb
2
SSH client receives the connection on localhost:5433
3
SSH wraps it in an encrypted channel and sends it through the SSH tunnel to bastion.company.com
4
Bastion opens a plain TCP connection to db.internal:5432 (this hop is inside the firewall, not internet-exposed)
5
Postgres responds — data flows back through the same chain in reverse
Syntax breakdown:-L [bind_addr:]local_port:remote_host:remote_port
Omit bind_addr to bind to localhost only (safe). Add 0.0.0.0 as bind_addr to let others on your LAN use the tunnel too.
ssh -R 8080:localhost:3000 user@public-server.com
encrypted SSH tunnel
plain TCP (other side)
Traffic flow — Remote Port Forward
External User
browser
plain TCP
Public Server
:8080 listener
🔒 SSH tunnel
Your Machine
SSH client
local TCP
Your App
localhost:3000
Use case: Expose your local dev server to the internet (no port forwarding needed)
You are running a local Node.js app on port 3000 behind a NAT/router. A client wants to see your demo without deploying it. Remote port forwarding creates a listener on the public server — anyone hitting public-server.com:8080 gets tunneled back to your laptop's port 3000.
Think: ngrok, but built into SSH.
1
SSH creates a listener on public-server.com:8080 (the REMOTE side)
2
External user hits http://public-server.com:8080
3
Traffic travels back through the SSH tunnel (encrypted) to your machine
4
Your SSH client connects to localhost:3000 — your dev server responds
Note: Requires GatewayPorts yes in sshd_config on the public server to accept connections from non-localhost. Without it, the listener only binds to 127.0.0.1 on the server.
ssh -D 1080 user@remote-server.com
encrypted SSH tunnel
plain TCP (from server's perspective)
Traffic flow — Dynamic SOCKS5 Proxy
Your Browser
SOCKS5: localhost:1080
🔒 SSH (any destination)
SSH Server
acts as proxy
plain TCP to destination
Any Host
google.com, etc.
Use case: Route all browser traffic through a remote server (poor-man's VPN)
Unlike -L (which forwards a single port) and -R (which creates one reverse tunnel), -D creates a SOCKS5 proxy. ANY TCP connection from your browser can be routed through it — websites, APIs, everything. The SSH server becomes your exit node.
Set your browser's proxy to SOCKS5 localhost:1080. All HTTP/HTTPS traffic from the browser goes through the encrypted SSH tunnel to the remote server, which makes the outbound connection on your behalf.
1
SSH opens a SOCKS5 listener on YOUR machine at port 1080
Browser requests google.com → sent via SOCKS5 to SSH client
4
SSH wraps it, sends through encrypted tunnel to remote server
5
Remote server connects to google.com — Google sees the remote server's IP, not yours
DNS leak warning: Some browsers resolve DNS locally even when using SOCKS5. In Firefox: set network.proxy.socks_remote_dns = true in about:config to also tunnel DNS through the proxy.
Use case: SSH directly to an internal server through a bastion, one command
The internal server has no public IP. The bastion is the only SSH-accessible entry point. Without -J, you would have to SSH to the bastion first, then SSH again from there. With -J (ProxyJump), it all happens in one command — SSH establishes a tunnel to the bastion, then creates a new SSH connection inside that tunnel to the internal server.
Crucially: the SSH connection to the internal server is end-to-end encrypted from YOUR machine to the internal server. The bastion only forwards bytes — it cannot see the inner SSH session.
1
SSH connects to bastion.company.com and authenticates
2
Through the bastion, SSH opens a TCP channel to internal-server.local:22
3
A second, independent SSH handshake runs inside that channel — end-to-end from your machine
4
You get a shell on internal-server.local as if you connected directly
~/.ssh/config equivalent:
Host internal-server
HostName internal-server.local
User ali
ProxyJump bastion.company.com
After this, just type: ssh internal-server
// How ECDH Key Exchange Creates a Shared Secret Without Transmitting It
This is the magic at the heart of SSH (and TLS). Both sides need the same encryption key to communicate. But they cannot send the key over the network — an eavesdropper could intercept it. ECDH (Elliptic Curve Diffie-Hellman) lets both sides independently compute the same shared secret using only public information.
Client
Generate: ephemeral key pair (c_priv, c_pub)
Send to server: c_pub (public key only)
Receive from server: s_pub
Compute: shared = c_priv × s_pub
c_priv NEVER leaves this machine
⇄
Server
Generate: ephemeral key pair (s_priv, s_pub)
Send to client: s_pub (public key only)
Receive from client: c_pub
Compute: shared = s_priv × c_pub
s_priv NEVER leaves this machine
Due to elliptic curve math: c_priv × s_pub == s_priv × c_pub
✓ Both sides compute the SAME shared secret — without ever sending private keys
Forward Secrecy: These ephemeral keys are discarded after the session. Even if an attacker records all your SSH traffic today and later steals the server's host key, they cannot decrypt the recorded sessions — the ephemeral keys are gone. This property is called Perfect Forward Secrecy (PFS).
// Negotiated Algorithms (SSH_MSG_KEXINIT)
Both sides send a list of algorithms they support. The first algorithm in the client's list that the server also supports wins. Preference order matters.
Key Exchange (kex_algorithms)
Forward Secrecy
Notes
curve25519-sha256recommended
✓ ephemeral
Curve25519 ECDH + SHA-256 hash. Fast, small keys, strong security.
ecdh-sha2-nistp256default
✓ ephemeral
NIST P-256 curve. Widely supported, some distrust NIST curves.
diffie-hellman-group14-sha256
✓ ephemeral
Classic DH with 2048-bit group. Slower than ECDH.
diffie-hellman-group1-sha1legacy
✓ ephemeral
768-bit DH, SHA-1. Disabled by default in OpenSSH 7.0+.
Encryption (cipher_algorithms)
Type
Notes
chacha20-poly1305@openssh.comrecommended
AEAD
ChaCha20 stream cipher + Poly1305 MAC in one. No timing attacks. Default in modern OpenSSH.
aes256-gcm@openssh.comdefault
AEAD
AES-256-GCM authenticated encryption. Hardware-accelerated on most CPUs.
aes128-gcm@openssh.com
AEAD
AES-128-GCM. Slightly faster than 256, still secure.
aes256-ctr
CTR + MAC
AES-256 counter mode. Requires separate HMAC. Older but still secure.
3des-cbclegacy
CBC
Triple DES. Very slow, CBC mode has padding oracle risks. Avoid.
MAC (mac_algorithms)
Notes
hmac-sha2-256-etm@openssh.comrecommended
Encrypt-then-MAC. EtM ordering prevents padding oracle attacks. Default when using non-AEAD ciphers.
hmac-sha2-512-etm@openssh.com
Larger MAC (512-bit). EtM ordering. Slightly more overhead.
hmac-sha2-256
MAC-then-Encrypt ordering. Less preferred than EtM variants.
hmac-md5legacy
MD5 is broken for collision resistance. Disabled in OpenSSH 7.0+.
Host Key Types (server_host_key_algorithms)
Key Size
Notes
ssh-ed25519recommended
256-bit (32 bytes)
EdDSA over Curve25519. Fast, small, immune to weak RNG. Modern choice.
ecdsa-sha2-nistp256default
256-bit NIST P-256
ECDSA. Fast but NIST curves have some controversy.
rsa-sha2-512
≥2048 bits
RSA with SHA-512 signing. Widely compatible, larger keys.
ssh-rsa (SHA1)legacy
≥2048 bits
RSA with SHA-1. Disabled in OpenSSH 8.8+.
ssh-dsslegacy
1024-bit only
DSA. 1024-bit key is too small. Disabled in OpenSSH 7.0+.
// SSH Binary Packet Protocol — What Every Encrypted Packet Looks Like
After NEWKEYS, every SSH message is wrapped in this binary format (before encryption):
packet_length
4 bytes
padding_length
1 byte
payload (msg type + data)
variable
random padding
padding_length bytes
MAC / AEAD tag
0 or 16-32 bytes
packet_length — length of the rest (padding_length + payload + padding), encrypted with the payload. An attacker sees only the encrypted bytes, not the message size (with ChaCha20-Poly1305 even this is encrypted via the length key). padding — random bytes to pad the packet to a block boundary. Also prevents traffic analysis by randomizing packet sizes slightly. sequence number — implicit counter, not in the packet but included in the MAC calculation. Prevents replay attacks.
// ~/.ssh/known_hosts — How SSH Prevents Man-in-the-Middle Attacks
When you connect to a server for the first time, SSH stores its public host key fingerprint in ~/.ssh/known_hosts. On every future connection, SSH verifies that the server presents the same key. If the key changes, SSH loudly warns you — this is your protection against MITM attacks.
github.com,140.82.121.3ssh-ed25519AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GkZu|1|AbCdEf...=ssh-rsaAAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7...↑ hashed hostname (HashKnownHosts yes) ↑ key type ↑ base64 public key
1st connect
Server presents its host key. SSH has never seen it before. You see: The authenticity of host 'server.com (1.2.3.4)' can't be established. ED25519 key fingerprint is SHA256:abc123... Are you sure you want to continue connecting? (yes/no/[fingerprint])
If you type yes, the key is saved. Best practice: verify the fingerprint out-of-band before accepting.
known host
Server presents the same key as stored. SSH verifies silently and proceeds. This is the normal case — you connect, no prompts.
key changed
Server presents a DIFFERENT key than stored. SSH shows a loud warning and refuses to connect: WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! Someone could be eavesdropping on you right now (man-in-the-middle attack)!
Legitimate causes: server was reinstalled, key was rotated, or you are connecting via a different IP. Always investigate before bypassing.