Set up SSH key authentication
Generate a local SSH key pair and install the public half on a remote host so subsequent SSH/SCP/rsync sessions authenticate without typing a password. The standard onboarding step for any developer touching remote machines.
How to set up ssh key authentication in each shell
ssh-keygen -t ed25519 -C "[email protected]" && ssh-copy-id user@hostTwo-step: (1) generate, (2) copy public key. `-t ed25519` is the modern algorithm (small key, fast verify, secure against modern attacks); use `-t rsa -b 4096` only when targeting legacy systems that pre-date OpenSSH 6.5 (rare in 2026). `-C` is a comment label (no security significance — useful for finding your key in a remote `authorized_keys` later). `ssh-copy-id` appends `~/.ssh/id_ed25519.pub` to the remote `~/.ssh/authorized_keys`, creating the `.ssh/` directory with correct `0700` perms if needed. Both commands are interactive on first run (passphrase + remote password); after that, `ssh user@host` is passwordless. To customize: `ssh-keygen -t ed25519 -f ~/.ssh/deploy_ed25519 -C "deploy-bot"` then `ssh-copy-id -i ~/.ssh/deploy_ed25519.pub user@host`.
ssh-keygen -t ed25519 -C "[email protected]" && ssh-copy-id user@hostssh-keygen -t ed25519 -C "[email protected]"; and ssh-copy-id user@hostFish 3.0+ accepts `&&`, but the idiomatic form is `; and`. Both work — `ssh-keygen … && ssh-copy-id …` runs fine in modern fish. The remote-side step still uses the system ssh-copy-id — fish does not modify its behavior.
ssh-keygen -t ed25519 -C "[email protected]"; type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh user@host "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"pwsh ships OpenSSH on Windows 10 1809+ — `ssh-keygen` works identically. CRITICAL: Windows does NOT ship `ssh-copy-id` (it is a bash script, not a binary). The portable Windows idiom is the pipe shown — `type` reads the public key, pipes over ssh, appends to remote `authorized_keys`, ensuring `.ssh/` exists with `mkdir -p`. For pwsh-native: `Get-Content $env:USERPROFILE\.ssh\id_ed25519.pub | ssh user@host "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"`. Remote permissions: after the append, run `ssh user@host "chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys"` once — Windows-generated content may inherit looser perms over ssh. To load into the Windows ssh-agent: `Get-Service ssh-agent | Set-Service -StartupType Automatic; Start-Service ssh-agent; ssh-add $env:USERPROFILE\.ssh\id_ed25519`.
ssh-keygen -t ed25519 -C "[email protected]" && type %USERPROFILE%\.ssh\id_ed25519.pub | ssh user@host "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"Same OpenSSH binary as pwsh. `type` is cmd's file-read equivalent of `cat` (slight semantic difference around binary files, irrelevant here). To load into ssh-agent on cmd: `sc.exe config ssh-agent start=auto && sc.exe start ssh-agent && ssh-add %USERPROFILE%\.ssh\id_ed25519`. Legacy PuTTY workflow: generate with `puttygen.exe` (GUI) or `puttygen -t ed25519 -o C:\keys\id.ppk`, copy public-key content via `puttygen -L C:\keys\id.ppk` to the remote `authorized_keys` manually (the OpenSSH-format public-key line, NOT the `.ppk` format). `plink` then uses `-i C:\keys\id.ppk` for the `.ppk` private key.
Equivalents listed for Bash, Zsh, Fish, PowerShell, cmd.exe.
Gotchas & notes
- **`ed25519` vs `rsa` — algorithm choice for new keys.** `ssh-keygen -t ed25519` is the 2026 default: 256-bit elliptic-curve key, ~68-char public-key line, fast generation + verification, secure against currently-known attacks (no quantum-resistance — but neither is RSA). Use `rsa -b 4096` ONLY when targeting OpenSSH < 6.5 (released 2014-01-30) — extremely rare in 2026. `ecdsa` (NIST P-256/384/521) is acceptable but ed25519 is preferred for new code (smaller, faster, fewer side-channels). `dsa` is OBSOLETE — OpenSSH 7.0 removed support, modern servers reject it. The `-a 100` flag adds 100 rounds of key-derivation when encrypting the private key with a passphrase — slows down brute-force on a stolen private-key file. For maximum protection: `ssh-keygen -t ed25519 -a 100 -C "[email protected]"`.
- **File permissions matter — ssh refuses to use insecure keys.** OpenSSH HARD-FAILS if your private key file is world-readable or your `~/.ssh` directory is too open. Required: `~/.ssh/` = `0700` (drwx------), `~/.ssh/id_ed25519` (private) = `0600` (-rw-------), `~/.ssh/id_ed25519.pub` (public) = `0644` (`pub` can be world-readable — it's designed to be). Same on the REMOTE: `~/.ssh/authorized_keys` must be `0600`. If you get "Permissions 0644 for id_ed25519 are too open" — fix with `chmod 600 ~/.ssh/id_ed25519`. Windows: use `icacls` to lock down to the owner: `icacls $env:USERPROFILE\.ssh\id_ed25519 /inheritance:r /grant:r "$env:USERNAME:F"` — Windows ssh-agent and `ssh` are picky about ACLs (will warn or refuse if a key file has inherited Everyone-readable ACLs).
- **The 3-line `authorized_keys` checklist.** Each line in remote `~/.ssh/authorized_keys` is one ALLOWED public key, in `<type> <base64> <comment>` format — `ssh-ed25519 AAAA... [email protected]`. Optional PREFIX of options restricts the key: `from="203.0.113.10",no-agent-forwarding,no-X11-forwarding,no-port-forwarding,no-pty,command="rsync --server …" ssh-ed25519 AAAA...` — restricts to one source IP, blocks forwarding, forces a single command (canonical for backup-only deploy keys). After editing, the file MUST end with a newline OR the last key is silently ignored on some OpenSSH versions. Sanity-check from your local box: `ssh -v user@host 2>&1 | grep "Offering public key"` — verbose ssh logs which keys it offered, useful when "passwordless still prompts" (the answer is almost always: file perms wrong, OR the key line in authorized_keys is malformed/wrapped).
- **`ssh-agent` and passphrase caching — load once per session.** If your private key has a passphrase (recommended), every ssh/scp invocation prompts for it. `ssh-agent` caches the unlocked key in memory for the session: `ssh-add ~/.ssh/id_ed25519` prompts once, all subsequent ssh calls use the cached key without prompting. Started automatically by most desktop environments (GNOME Keyring, macOS launchd, Windows ssh-agent service). To check what is loaded: `ssh-add -l`. To unload all keys: `ssh-add -D`. macOS extra: `ssh-add --apple-use-keychain ~/.ssh/id_ed25519` (recent macOS) integrates with the Keychain so the passphrase persists across reboots. To auto-load on shell startup: add `ssh-add -q ~/.ssh/id_ed25519 2>/dev/null` to `~/.bashrc` or `~/.zshrc`. For CI: provide the key without a passphrase, restrict it with `from=` + `command=` in `authorized_keys` as the security control.
- **Hardware-backed keys: FIDO2 / Yubikey + `-t ed25519-sk`.** Since OpenSSH 8.2 (2020-02-14), key types `ed25519-sk` and `ecdsa-sk` generate keys backed by a FIDO2 security key (Yubikey 5, SoloKey, etc.). The private key NEVER leaves the hardware — even a compromised laptop can't exfiltrate it. `ssh-keygen -t ed25519-sk -O resident -C "[email protected]"` creates a "resident" key (also stored on the device, so a new machine can reload it with `ssh-keygen -K`). The `.pub` file works exactly like a software-key `.pub` — paste into remote `authorized_keys`. Each SSH connection requires a tap on the security key (user-presence test); `-O no-touch-required` skips that for unattended servers. Trade-off: more secure, but harder to script automation around — pair a hardware key with a software key (in a different `authorized_keys` line restricted to CI IP ranges) for the best of both.
Related tasks
- Copy files over SSH— Move one or more files between your local machine and a remote host over an SSH-encrypted channel — the standard `scp` / `rsync` / `pscp` operation. The canonical "deploy this build artifact" or "pull these logs back for inspection" gesture.
- Run a remote command non-interactively— Execute a single command on a remote host over SSH without opening an interactive shell — capture its stdout/stderr/exit-code on the local side. The canonical "ssh user@host 'systemctl status nginx'" gesture used in deploy scripts, CI, and ad-hoc remote inspection.
- Forward a local port over SSH— Tunnel TCP traffic from a port on your local machine to a service reachable from the remote SSH host — bypass NAT, expose an internal database to your laptop, route browser traffic through a jump host, etc. The canonical "ssh -L 5432:localhost:5432 bastion" pattern used to reach private services without VPNing.
- Check an SSH connection without running anything— Verify that an SSH host is reachable, key auth works, and the server is responsive — without actually opening a shell or running a payload command. The canonical "is the box up + is my key configured" health-check used in deploy scripts and CI preflight.