# SSH, Actually Explained: Handshakes, Keys, and the Tunnel Trick

> Source: <https://dev.to/lovestaco/ssh-actually-explained-handshakes-keys-and-the-tunnel-trick-48bf>
> Published: 2026-09-07 17:17:38+00:00

*Hello, I'm Maneshwar, and I'm building LiveReview — a blast-radius aware AI code review built for your business-critical systems. [Star us](https://github.com/HexmosTech/LiveReview/) to help devs discover the project, give it a try, and share your feedback to help improve the product.*

You type `ssh ubuntu@1.2.3.4`, you get a prompt, and a server in another continent starts obeying your keyboard.

Most of us learned that incantation on day one and never looked under it again.

Which is fair. It works. It has worked for thirty years. Nothing about it demands your attention.

But SSH is doing something genuinely clever in the half second before that prompt appears, and once you have seen it, a whole category of confusing errors stops being confusing.

So let's open it up.

Before SSH there was telnet, and telnet had exactly one flaw.

It sent everything in plain text. Your username, your password, every command, every byte of output.

On a shared network that is not a subtle problem. Anyone sitting between you and the server was reading your session like a group chat they had been added to.

Telnet is a postcard. SSH is a locked briefcase.

The postman is the same postman in both cases. That is the entire point.

Telnet is still installed on your machine, by the way. It survives as a debugging tool, because `telnet host 443` is a quick way to ask "is this port even open". 

As a login protocol it is dead, and it deserved it.

Here is the problem SSH has to solve in its first few milliseconds.

Two machines that have never met need to agree on a secret encryption key, while shouting at each other across a network where everything they say is being recorded.

That sounds impossible. It is not, and the reason is the neatest trick in applied cryptography.

SSH uses two kinds of encryption, not one, and people conflate them constantly.

**Asymmetric crypto sets up the conversation.** It is slow, it involves public and private key pairs, and it is used only for the opening handshake.

**Symmetric crypto carries the conversation.** One shared key, fast, and it does all the actual work of encrypting your keystrokes.

The handshake exists purely to get both sides holding the same symmetric key.

The move is called a [Diffie-Hellman key exchange](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange), and it goes like this.

Each side generates a private value and derives a public value from it.

They swap public values in the clear, where anyone can see them.

Then each side combines its own private value with the other side's public value, and the maths works out such that both arrive at the same number.

An observer who saw both public values cannot get there. They are missing either private half, and going backwards from a public value to a private one is the hard problem the whole thing rests on.

So the session key is never transmitted. It is independently derived, twice, in two different countries.

That key is also **ephemeral**. It exists for this session and is thrown away when you disconnect. Record the traffic today, steal the server's private key next year, and you still cannot decrypt what you captured. That property is called [forward secrecy](https://en.wikipedia.org/wiki/Forward_secrecy) and it is worth knowing the name of.

Want to actually watch this happen? SSH will narrate it.

``` bash
$ ssh -vv ubuntu@example.com
debug1: SSH2_MSG_KEXINIT sent
debug1: kex: algorithm: curve25519-sha256
debug1: kex: host key algorithm: ssh-ed25519
debug1: Server host key: ssh-ed25519 SHA256:qN8Xb2...
debug1: SSH2_MSG_NEWKEYS sent
debug1: Authenticating to example.com:22 as 'ubuntu'
```

`NEWKEYS` is the moment the tunnel goes live. Notice that authentication happens on the line *after* it.

That ordering matters more than it looks.

The encryption is set up first, and only then does SSH ask who you are.

Your password, if you use one, is already inside the tunnel by the time you type it.

There is a gap in the story above.

Key exchange gives you a secure channel to *somebody*. It does not prove that somebody is the server you meant to reach.

That is what the server's host key is for, and it is why the first connection asks you this:

```
The authenticity of host 'example.com' can't be established.
ED25519 key fingerprint is SHA256:qN8Xb2fV+3Kx9...
Are you sure you want to continue connecting (yes/no)?
```

You have typed `yes` there a thousand times without reading it. I have too.

What you are being asked is whether that fingerprint belongs to the machine you think you are talking to. SSH cannot know. There is no certificate authority here, unlike the web.

So it does the next best thing. It writes the fingerprint into `~/.ssh/known_hosts` and screams if it ever changes.

That scream is the `REMOTE HOST IDENTIFICATION HAS CHANGED` block, and it is not being dramatic for fun.

Either the server was genuinely rebuilt, or someone is sitting in the middle pretending to be it.

Nine times out of ten it is a rebuilt server. The tenth time is the reason for the warning.

Once the tunnel is up, SSH still needs to know who you are.

You can use a password. You should not.

The better way is a key pair you generate once, on your own machine.

```
ssh-keygen -t ed25519 -C "laptop"
# ~/.ssh/id_ed25519       <- private. never leaves this machine.
# ~/.ssh/id_ed25519.pub   <- public. paste it anywhere.

ssh-copy-id ubuntu@example.com   # appends the .pub to the server's authorized_keys
```

Use `ed25519`, not RSA. It is smaller, faster, and has fewer ways to configure it badly. RSA is still fine at 4096 bits, but there is no reason to pick it for a new key.

The login is a challenge and response.

You tell the server which public key you are claiming. The server checks `authorized_keys`, finds it, and sends back a random chunk of data.

Your client signs that data with the private key. The server verifies the signature against the public key it already had.

Nothing secret ever crosses the network. Not on the first login, not on the thousandth.

Compare that to a password, which crosses the wire on every single login, lives in someone's head, is probably reused, and is short enough to guess. A 256-bit key is not short enough to guess. That is not a marginal improvement, it is a different category.

This is also why keys are the only sane option for automation. A CI pipeline cannot type a password, but it can hold a key.

Two places where this is already in your muscle memory.

**EC2.** When you launch an instance, AWS puts your public key into the image and hands you the `.pem`, which is the private half. That is why `ssh -i mykey.pem ec2-user@1.2.3.4` works with no password. AWS never had your private key and cannot recover it for you, which is [stated plainly in their docs](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html).

**GitHub.** `git push` over SSH is the exact same challenge and response. You put your public key in your account settings, and every push signs a challenge. GitHub [removed password authentication for Git entirely in 2021](https://github.blog/security/application-security/removing-support-for-password-authentication/), so it is keys or a token, nothing else.

A private key in a git repo is a server someone else owns now. GitHub scans public repos for exactly this, and bots scan them faster.

`chmod 600` your keys, keep them out of repos, and put a passphrase on them so a stolen laptop is not a stolen server.

Here is the SSH feature that feels like cheating the first time it works.

You need to reach a MySQL database in a private subnet. It has no public IP. It has no route from the internet, on purpose, because it is a database.

But there is a **bastion host** in front of that network. One small public machine whose entire job is to be the single door, with port 22 open and nothing else.

You can SSH into the bastion. So you can reach the database.

```
ssh -L 3306:db.internal:3306 ec2-user@bastion.example.com
```

Read the `-L` argument as three parts: the local port you want to open, then the host and port to reach *from the bastion's point of view*.

Now `mysql -h 127.0.0.1 -P 3306` on your laptop talks to that private database.

Your MySQL client believes it is connected to something local. It has no idea a tunnel exists. Every byte goes through the encrypted SSH session and comes out on the far side of the firewall.

The same trick works for an internal dashboard, a Redis instance, an admin panel that should never be public, or a staging service someone forgot to expose.

Two directions worth knowing, since the flags are easy to mix up:

`-L` brings a `-R` pushes a If you find yourself typing long tunnel commands daily, put them in `~/.ssh/config` and forget them:

```
Host db-tunnel
    HostName bastion.example.com
    User ec2-user
    IdentityFile ~/.ssh/prod.pem
    LocalForward 3306 db.internal:3306
```

Then it is just `ssh db-tunnel`. Your fingers will thank you.

`Permission denied (publickey)` is the least helpful error message in networking, because it covers about six different mistakes.

Here is the order I actually check them in:

``` php
flowchart TD
    A[Permission denied publickey] --> B{Did you pass the right key?}
    B -->|No| K[ssh -i mykey.pem or add to ~/.ssh/config]
    B -->|Yes| C{chmod 600 on the key file?}
    C -->|No| P[Fix permissions, SSH ignores world-readable keys]
    C -->|Yes| D{Public key in authorized_keys on the server?}
    D -->|No| U[ssh-copy-id, or paste it in]
    D -->|Yes| E{Right username for the image?}
    E -->|No| N[ec2-user, ubuntu, admin, not root]
    E -->|Yes| F{Home dir or .ssh perms too open?}
    F -->|Yes| H[chmod 700 ~/.ssh, 755 the home dir]
    F -->|No| L[Read the server log: journalctl -u sshd]

    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef start fill:#e9ecef,stroke:#6c757d,color:#1a1a1a
    classDef fix fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a
    classDef last fill:#9d8cff,stroke:#5b4bcc,color:#1a1a1a

    class B,C,D,E,F decision
    class A start
    class K,P,U,N,H fix
    class L last
```

The permissions one catches everybody at least once. SSH will silently ignore a private key that other users can read, which is protective and infuriating in equal measure.

The wrong-username one is the other classic. Amazon Linux wants `ec2-user`, Ubuntu images want `ubuntu`, and almost nothing wants `root` anymore.

When you have exhausted the client side, `ssh -vv` tells you which keys were offered, and `journalctl -u sshd` on the server tells you why each one was rejected. Between those two you will find it.

A short list, because most SSH hardening advice is longer than it needs to be.

Turn off password authentication once your key works. `PasswordAuthentication no` in `sshd_config` deletes the entire brute-force attack surface in one line.

Turn off direct root login. `PermitRootLogin no`, then `sudo` from a normal user, so the audit log has a name in it.

Do not bother moving off port 22. It stops log noise from untargeted scanners, and nothing else. A real attacker runs a port scan.

Test config changes in a second terminal while your first session is still open. Locking yourself out of a box by reloading a broken `sshd_config` is a rite of passage best skipped.

SSH is not one idea, it is three stacked on each other.

**A key exchange** that lets two strangers agree on a secret in public, then hands the session to fast symmetric encryption.

**An authentication step** that proves who you are by signing a challenge, so nothing worth stealing ever crosses the network.

**A tunnel** that, once you have all that, will carry whatever other protocol you point at it.

That third layer is the one most people never touch, and it is the one that turns SSH from a login tool into a general-purpose way to reach things that are deliberately unreachable.

Go generate an ed25519 key, delete a password login, and forward a port. It is a good afternoon.

Your team's attention is limited, and the deluge of AI-generated code is making it harder to keep production secure and reliable without slowing you down.

I'm building **LiveReview**, a blast-radius aware AI code review built for your business-critical systems.

Instead of presenting every diff with equal emphasis, **LiveReview scores each change by blast radius — how far its impact reaches through your call graph — so you can focus attention where it actually matters.**

Spend code review effort where business risk is highest — not spread evenly across every diff.

⭐ Star it on GitHub:

LiveReview is an AI code reviewer that scores every hunk of a diff by **blast radius**: how far a change reaches through your call graph, how much persistent state it touches, and how well-tested it is. A 3-line change to a shared auth check can outrank a 300-line UI tweak. Your team's attention goes to the highest-risk code first, not spread evenly across every diff.

*LiveReview's Blast Radius & Review Priority scoring, live in the diff viewer.*

| The exact math, not a black box | Visualize blast radius at a glance | Every factor that feeds the score | 
|---|---|---|

**Here's the goal:**

**Click below to try LiveReview with your codebase:**
