ssh is the universal remote-access tool: log in, copy files, forward ports, proxy traffic, jump through bastions. The flag surface is small, but the parts you only use occasionally (-L vs -R, ProxyJump syntax, ~/.ssh/config Host blocks) are the parts you forget. This page is the reference I keep open for the commands I do not type weekly, with the Windows OpenSSH and PuTTY equivalents where they differ.
How do I use SSH?
ssh connects to a remote machine over an encrypted channel, authenticates you (typically with a public/private keypair), and gives you a shell, a remote command execution, or a tunnel. The basic invocation is ssh user@host. Generate a keypair once with ssh-keygen -t ed25519, then install the public half on the server (ssh-copy-id user@host on Linux/macOS, or manually append ~/.ssh/authorized_keys on Windows). Once keys are in place, ssh user@host logs in without a password prompt. The same binary handles file copy (scp, sftp), port forwarding (-L for local, -R for remote, -D for SOCKS), and chained logins via bastion hosts (-J or ProxyJump). Configuration lives in ~/.ssh/config, where Host blocks let you alias frequently-used connections. Windows ships OpenSSH by default since Windows 10 1809; older systems used PuTTY (different keyfile format, different config UI).
Jump to:
- Basic connection
- Generating keys with ssh-keygen
- Installing the public key
- Port forwarding
- ProxyJump and bastion hosts
- The ~/.ssh/config file
- Copying files: scp and rsync
- ssh-agent and ssh-add
- Windows specifics: OpenSSH and PuTTY
- Common pitfalls
- What to do next
- FAQ
Basic connection
ssh user@host.example.comSpecify a non-default port:
ssh -p 2222 user@host.example.comUse a specific identity file (private key):
ssh -i ~/.ssh/id_ed25519_work user@host.example.comRun a single command and exit:
ssh user@host.example.com 'uptime; df -h'Verbose mode for debugging connection issues (add more v for more detail):
ssh -vvv user@host.example.comGenerating keys with ssh-keygen
Generate an Ed25519 keypair (the modern default; smaller, faster, and as secure as RSA-4096):
ssh-keygen -t ed25519 -C 'you@example.com'You will be prompted for a save path (default ~/.ssh/id_ed25519) and an optional passphrase. The passphrase encrypts the private key at rest; combined with ssh-agent, you type it once per session.
RSA is still fine for older servers that do not understand Ed25519:
ssh-keygen -t rsa -b 4096 -C 'you@example.com'Change or remove a passphrase on an existing key:
ssh-keygen -p -f ~/.ssh/id_ed25519Print the key's fingerprint (compare with what the server displays on first connect):
ssh-keygen -lf ~/.ssh/id_ed25519.pubInstalling the public key
On Linux and macOS, ssh-copy-id is the one-shot answer:
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host.example.comssh-copy-id is just a wrapper around the same append-to-authorized_keys logic the PowerShell variant does explicitly. macOS does not include ssh-copy-id by default in some versions, but brew install ssh-copy-id or the explicit cat ~/.ssh/id_ed25519.pub | ssh user@host 'cat >> ~/.ssh/authorized_keys' always works.
After installing the key, test:
ssh user@host.example.comNo password prompt means the key is working. If it still asks for a password, check permissions on the server: ~/.ssh must be 700, ~/.ssh/authorized_keys must be 600, and the user's home directory must not be world-writable.
Port forwarding
Three flavors, all useful, easy to mix up.
| Flag | Direction | Use case |
|---|---|---|
-L LPORT:DEST:DPORT | Local-to-remote | Access a remote service as if local |
-R RPORT:DEST:DPORT | Remote-to-local | Expose a local service through the remote host |
-D PORT | Dynamic (SOCKS) | Tunnel arbitrary TCP through SSH |
Local forward (-L) — reach a database on the remote network from your laptop:
ssh -L 5432:db.internal:5432 user@bastion.example.comConnect a local Postgres client to localhost:5432 and the traffic flows over SSH to db.internal:5432.
Remote forward (-R) — expose your local dev server to the remote host:
ssh -R 8080:localhost:3000 user@host.example.comAnyone on the remote host can hit http://localhost:8080 and reach your laptop's port 3000. Useful for sharing a local prototype with a remote teammate, or for webhook testing. For the latter, ngrok-style tools are usually easier; reverse SSH is the no-extra-account option.
Dynamic forward (-D) — a SOCKS proxy that tunnels arbitrary TCP through the SSH connection:
ssh -D 1080 user@host.example.comPoint your browser's SOCKS5 proxy at 127.0.0.1:1080 and every request flows through the remote host. Effectively a one-line VPN for HTTP traffic. Pairs with curl's --socks5 flag for command-line use.
Run forwards in the background with -fN (no remote command, fork into background):
ssh -fN -L 5432:db.internal:5432 user@bastion.example.comProxyJump and bastion hosts
-J chains SSH connections through one or more intermediate hosts. The traditional pattern was ssh -t bastion ssh target, which is fragile; ProxyJump is the modern, clean version.
ssh -J jump.example.com user@target.internalMultiple hops, comma-separated:
ssh -J jump1.example.com,jump2.example.com user@target.internalProxyJump only opens a TCP channel through the jump host; it does not require a shell on the jump. The jump host needs OpenSSH 7.3+ on its sshd. The bigger win is that scp and rsync work transparently when ProxyJump is set in ~/.ssh/config.
The ~/.ssh/config file
Stop typing the same -J, -i, -p, -L flags. Put them in a Host block.
# ~/.ssh/config
Host bastion
HostName bastion.example.com
User ishan
IdentityFile ~/.ssh/id_ed25519_work
Host prod-db
HostName db.internal
User dbadmin
Port 22
ProxyJump bastion
LocalForward 5432 localhost:5432
Host github.com
User git
IdentityFile ~/.ssh/id_ed25519_github
AddKeysToAgent yes
UseKeychain yes # macOS only: store passphrase in Keychain
Host *.staging.example.com
User deploy
IdentityFile ~/.ssh/id_ed25519_staging
StrictHostKeyChecking accept-new
After saving:
ssh prod-db # uses ProxyJump bastion, port 22, local forward, the lotKey directives worth knowing:
| Directive | Purpose |
|---|---|
HostName | Real DNS name or IP (the Host alias is just for ssh invocation) |
User | Default username |
Port | Non-default SSH port |
IdentityFile | Which private key to use |
IdentitiesOnly yes | Use ONLY the listed IdentityFile, ignore agent |
ProxyJump | Bastion host (replaces ProxyCommand for most uses) |
LocalForward / RemoteForward | Auto-create -L / -R on connect |
ServerAliveInterval 60 | Keep idle connections from dropping |
AddKeysToAgent yes | Auto-add the key to ssh-agent on first use |
UseKeychain yes | macOS: store passphrase in Keychain |
StrictHostKeyChecking accept-new | Auto-accept new host keys but fail on changes (safer than no) |
Config matches are first-match-wins; put specific Host blocks before wildcard blocks.
Copying files: scp and rsync
scp is the simplest, ships with OpenSSH, no setup:
scp local.txt user@host:/remote/path/Download a remote file:
scp user@host:/remote/file.txt ./Recursive directory copy:
scp -r ./dist user@host:/var/www/rsync over SSH is faster for repeated syncs (delta transfers, resume, partial files):
rsync -avz --progress ./dist/ user@host:/var/www/The trailing slash on the source matters: ./dist/ copies the contents, ./dist copies the directory itself. This is the source of more bad deployments than any other rsync quirk.
rsync over a specific SSH config / port:
rsync -avz -e 'ssh -p 2222 -i ~/.ssh/id_deploy' ./dist/ user@host:/var/www/Worth noting: newer OpenSSH versions use the SFTP protocol under the hood for scp by default. If you hit "this protocol is not supported" errors, fall back to scp -O (capital O, legacy mode) or use sftp directly.
ssh-agent and ssh-add
ssh-agent keeps decrypted private keys in memory so you do not type the passphrase every connection. ssh-add loads keys into it.
Start the agent (Linux/macOS):
eval "$(ssh-agent -s)"Add a key:
ssh-add ~/.ssh/id_ed25519macOS-specific: --apple-use-keychain (modern OpenSSH on macOS) or -K (older) stores the passphrase in the Keychain so it persists across logins.
List loaded keys:
ssh-add -lRemove all keys from the agent:
ssh-add -DWindows specifics: OpenSSH and PuTTY
Two worlds: native Windows OpenSSH (Windows 10 1809+, Windows Server 2019+) and PuTTY (the historical default).
| Concern | Windows OpenSSH | PuTTY |
|---|---|---|
ssh command | Available in PowerShell and cmd | plink.exe (CLI) and putty.exe (GUI) |
| Private key format | OpenSSH (id_ed25519) | PuTTY's own .ppk format |
| Key conversion | ssh-keygen -e -f and -i -f | puttygen (Load OpenSSH, Save .ppk) |
| Config file | %USERPROFILE%\.ssh\config | Saved sessions in registry |
| Agent | ssh-agent service (built-in) | Pageant |
ssh-copy-id | Not available | Manual via PuTTY's Pageant |
If you are coming from PuTTY and moving to native OpenSSH, see how to export and import PuTTY settings for migrating saved sessions and key conversion.
Install OpenSSH on Windows (if not already present):
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0The client is on by default on Windows 11 and recent Windows 10. The server (sshd) is optional.
Common pitfalls
1. Permissions on ~/.ssh are too loose. sshd refuses to use a key if the file is group- or world-readable. Fix: chmod 700 ~/.ssh, chmod 600 ~/.ssh/id_*, chmod 644 ~/.ssh/*.pub. The ~ home directory itself must not be group-writable either.
2. Host key changed. "WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!" usually means the server was rebuilt and presents a new key. If you trust the rebuild, remove the stale entry: ssh-keygen -R hostname. If you do not know why it changed, do NOT just delete it.
3. Forgetting -N with -f forwards a port AND opens an unwanted shell. Use -fN for background forwards: -f forks to background, -N says "do not run a remote command", together they keep just the forward alive.
4. ProxyJump on the wrong sshd version. -J requires OpenSSH 7.3 or later on the jump host. Older servers need the legacy ProxyCommand ssh -W %h:%p jump pattern.
5. ssh-add cleared on every reboot. macOS Keychain integration requires UseKeychain yes in ~/.ssh/config AND --apple-use-keychain (or legacy -K) when adding. Linux needs AddKeysToAgent yes in config plus a session-managed agent (gnome-keyring, KDE Wallet, etc.).
6. SSH timeouts on idle connections. NAT routers and load balancers drop idle TCP after a few minutes. Set ServerAliveInterval 60 and ServerAliveCountMax 3 in ~/.ssh/config to send keepalives.
7. scp with newer OpenSSH errors out. Recent versions deprecate the legacy SCP protocol. Fall back with scp -O or switch to sftp / rsync.
8. Windows OpenSSH does not support -f (background). Run forwards in a separate PowerShell window, or use Start-Process to background the whole ssh invocation.
What to do next
- grep cheat sheet — for searching log files on remote hosts.
- find command cheat sheet — for batch operations over SSH.
- curl cheat sheet — when you want to test HTTP through an SSH-tunneled SOCKS proxy.
- How to export and import PuTTY settings — migrating saved sessions when moving from PuTTY to native OpenSSH on Windows.
- Bash for loop — looping over a host list to run the same command across a fleet.
- Bash while loop — wait-for-SSH-ready polling patterns useful in deploy scripts.
- How to increase Google Cloud VM disk size without rebooting — uses SSH for the live resize sequence.
- External: OpenBSD ssh(1) manual, ssh_config(5), Microsoft OpenSSH for Windows.





