SSH Deep Dive Part 1: Keys, Clients, and Getting Connected on Windows, macOS, and Linux

I get asked some version of “how do I SSH into that box again” often enough that it’s worth writing down properly, once, rather than re-explaining it each time. This post covers the fundamentals: why a key beats a password, how to generate and install one, and what actually differs between the SSH client on Windows, macOS, and Linux. Part 2 covers the config file, agent forwarding, and connection multiplexing. Part 3 covers -L, -R, -D, and -J, the forwarding and jump-host flags that turn SSH into more than a remote shell.

Why a key beats a password

Password auth sends a secret to the server on every connection. The server has to store something derived from that secret to check it against, and that stored value is a target: if the server is ever compromised, or the password is reused somewhere that gets breached, the credential is burned. Passwords are also brute-forceable given enough attempts, and short or reused ones fall quickly, a whole password-cracking series on this site exists because of exactly that.

Key-based auth works differently. You generate a key pair: a private key that never leaves your machine, and a public key that gets copied to every server you want to reach. When you connect, the server sends a challenge, and your client signs it with the private key. The server checks that signature against the public key it already has on file. Nothing secret ever crosses the network, not even in encrypted form, because there’s nothing to send. The private key is the only thing that has to stay safe, and it never leaves your laptop.

This also makes revocation trivial. Deleting one line from authorized_keys on the server instantly cuts off that key, no password reset required, no other systems that shared the same password to worry about. And a private key can itself be protected with a passphrase, so losing the laptop doesn’t hand over the key unencrypted, something a plain password can’t offer at all.

For anything past a home lab, keys are the baseline, not the upgrade. SSH Hardening Beyond the Basics picks up from here and covers certificate authorities and bastion patterns for when a fleet of static keys stops scaling, but everything in this series assumes ordinary key pairs, which is what almost everyone is actually running.

Generating a key

ssh-keygen creates the pair. Ed25519 is the right default in 2026: it’s a modern elliptic-curve algorithm, the keys are short, and signing is fast. RSA is still fine, and still shows up on older devices that predate ed25519 support, but there’s no reason to pick it for a new key unless something you’re connecting to specifically requires it.

$ ssh-keygen -t ed25519 -C "your-email@example.com"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/you/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/you/.ssh/id_ed25519
Your public key has been saved in /home/you/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:uVVUhOCWhaHMdBtMHsFTpSjMTCoFT5/Xfp7IiAymAZE your-email@example.com

Set a passphrase unless the key lives somewhere already access-controlled and encrypted, like a company laptop with full-disk encryption you trust. An unprotected private key is only as safe as whatever else has access to that file. Part 2 covers ssh-agent, which lets you type the passphrase once per session instead of on every connection, so a passphrase doesn’t have to mean typing it constantly.

-C sets a comment, visible when you list the key later (ssh-add -l), useful for telling keys apart once you have more than one. The command above produces two files: id_ed25519 (private, never leaves this machine, permissions should be 600) and id_ed25519.pub (public, safe to copy anywhere).

Getting the public key onto a server

The server needs your public key appended to ~/.ssh/authorized_keys for the account you’re connecting as. Three ways to get it there, in order of convenience:

ssh-copy-id, if the server already accepts password auth for this one-time setup:

ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host

It connects with the password you type interactively, appends the key to authorized_keys on the remote end, and fixes permissions on the directory if needed. After this, password auth is no longer required for that account, though the server can still allow it for others.

Manual copy, if ssh-copy-id isn’t available or the server doesn’t have password auth enabled yet, for example a fresh VM whose only access is a console session:

cat ~/.ssh/id_ed25519.pub

Copy that single line, then on the server:

mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo "ssh-ed25519 AAAA... your-email@example.com" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

Provisioning-time, for cloud instances and most modern VM builds: the platform (cloud-init, a VM template, a Proxmox cloud image) takes a public key at creation time and writes authorized_keys before the box ever boots. This is the cleanest path when it’s available, since there’s no window where password auth is the only option at all.

First connection

ssh user@host

The first time you connect to any given host, you’ll see something like:

The authenticity of host '192.168.1.63 (192.168.1.63)' can't be established.
ED25519 key fingerprint is SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
Are you sure you want to continue connecting (yes/no/[fingerprint])?

That’s the server proving its own identity to you, the other direction from the key auth described above. Typing yes adds that fingerprint to ~/.ssh/known_hosts, and every future connection is silently checked against it. If the fingerprint ever changes without you rebuilding that host, SSH refuses to connect and warns loudly, that’s the protection working, not a bug to bypass. On a network you control, like a lab box you just built, this is a non-issue. On anything reachable over the internet, verifying the fingerprint out-of-band before typing yes is the theoretically correct step almost nobody actually does.

The client on each platform

The protocol is identical everywhere. What differs is what ships in the box and what the ergonomics look like.

Linux

The OpenSSH client is either preinstalled or one package away (openssh-client on Debian/Ubuntu) on essentially every distribution. ssh, ssh-keygen, ssh-agent, and ssh-copy-id are all present, and everything in this post and series was run and verified against a real OpenSSH 8.9 client:

$ ssh -V
OpenSSH_8.9p1 Ubuntu-3ubuntu0.16, OpenSSL 3.0.2 15 Mar 2022

macOS

macOS ships its own build of the OpenSSH client (Apple’s own build, not GNU/Linux’s) with no install step required, ssh, ssh-keygen, and ssh-agent are all already on PATH in Terminal. ssh-copy-id is not included by default; it’s available via Homebrew (brew install ssh-copy-id), or the manual copy method above works without it.

macOS has one genuinely useful platform-specific feature: Keychain integration for key passphrases. Instead of typing a passphrase every time ssh-agent needs it, macOS can store it in Keychain and unlock it automatically at login:

ssh-add --apple-use-keychain ~/.ssh/id_ed25519

Add UseKeychain yes to the relevant Host block in ~/.ssh/config (Part 2 covers this file properly) to make it automatic on every connection rather than a one-time flag. There have been scattered reports of this integration misbehaving across Sequoia point releases, so if a passphrase suddenly starts being asked for again after a reboot, that’s a known rough edge rather than something broken in your own setup.

Windows

This is the one people are usually surprised by: Windows has shipped a genuine OpenSSH client since Windows 10 build 1809 (October 2018) and Windows Server 2019, as an optional feature that’s enabled by default on current installs. ssh, ssh-keygen, and ssh-agent all work identically from PowerShell or cmd, same flags, same config file format, same everything, because it’s the same OpenSSH codebase, not a reimplementation:

PS> ssh -V
OpenSSH_for_Windows_9.5p1, LibreSSL 3.8.2

ssh-copy-id is the one piece missing, it’s a Bash script and Windows doesn’t ship Bash by default. The manual copy method works exactly as described above. For a one-line equivalent from PowerShell:

$key = Get-Content "$env:USERPROFILE\.ssh\id_ed25519.pub"
ssh user@host "mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '$key' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

Key generation, first connection, and known_hosts behavior are all identical to Linux and macOS. ~/.ssh on Windows resolves to %USERPROFILE%\.ssh, which PowerShell and the OpenSSH client both handle transparently, no path translation needed on your part.

Tying it back to a real example

The prompt that started this series: connecting from a Windows box to a Linux host on the local network, using an existing key.

ssh -i ~/.ssh/id_ed25519 michealg@192.168.1.63

-i names the private key explicitly, useful when you have more than one and don’t want to rely on the client trying its defaults in order. If the key was never installed on that host, ssh-copy-id -i ~/.ssh/id_ed25519.pub michealg@192.168.1.63 (or the PowerShell equivalent above) puts it there in one step.

Where this fits

Part 2 covers ~/.ssh/config, so ssh myhost replaces the full command above, plus ssh-agent and connection multiplexing for not re-authenticating on every single connection. Part 3 covers -L, -R, -D, and -J, port forwarding and jump hosts for reaching things through a host you already have access to, the same primitives covered from an offensive pivoting angle in Pivoting and Tunneling Part 1, here framed for ordinary day-to-day remote access instead.