ssh — Complete Guide: Keys, Config, Tunnels, and Agent Forwarding
SSH is the backbone of remote development and server administration, but most developers only ever use ssh user@host and call it a day. The full toolkit — key management, the client config, port forwarding, and jump hosts — can turn a clunky multi-step workflow into a single command. This guide covers all of it.
Key Generation
Prefer ed25519 for new keys — it’s faster and more secure than the legacy rsa default:
$ ssh-keygen -t ed25519 -C "your_email@example.com"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/Users/mukul/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /Users/mukul/.ssh/id_ed25519
Your public key has been saved in /Users/mukul/.ssh/id_ed25519.pub
The -C comment is just a label — it ends up in the .pub file and helps you identify the key later.
If you need RSA for compatibility with older systems, use at least 4096 bits:
$ ssh-keygen -t rsa -b 4096 -C "legacy-server"
Copying Your Public Key to a Server
$ ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server.example.com
This appends your public key to ~/.ssh/authorized_keys on the remote host. Equivalent manual step:
$ cat ~/.ssh/id_ed25519.pub | ssh user@server.example.com "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
ssh-agent: Managing Keys in Memory
ssh-agent holds decrypted private keys in memory so you don’t type your passphrase on every connection.
Start the agent and add your key:
$ eval "$(ssh-agent -s)"
Agent pid 12345
$ ssh-add ~/.ssh/id_ed25519
Enter passphrase for /Users/mukul/.ssh/id_ed25519:
Identity added: /Users/mukul/.ssh/id_ed25519 (your_email@example.com)
List currently loaded keys:
$ ssh-add -l
256 SHA256:abc123... your_email@example.com (ED25519)
On macOS, add this to ~/.ssh/config to have the agent auto-start and remember keys across reboots:
Host *
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_ed25519
The SSH Client Config: ~/.ssh/config
The config file is where SSH becomes genuinely powerful. Instead of typing long flags, you define named hosts:
Host myserver
HostName 192.168.1.100
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519
Host bastion
HostName bastion.prod.example.com
User ec2-user
IdentityFile ~/.ssh/prod_key
With this config, ssh myserver is equivalent to:
$ ssh -i ~/.ssh/id_ed25519 -p 2222 deploy@192.168.1.100
Useful Config Options
| Option | What it does |
|---|---|
HostName |
The actual hostname or IP |
User |
Default username |
Port |
Non-standard port |
IdentityFile |
Which private key to use |
ForwardAgent |
Enable agent forwarding |
ServerAliveInterval |
Send keepalive packets every N seconds |
ServerAliveCountMax |
Disconnect after N missed keepalives |
StrictHostKeyChecking no |
Skip host key verification (use sparingly) |
ProxyJump |
Route through a jump host |
A Host * block at the bottom sets defaults for all connections:
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
AddKeysToAgent yes
Port Forwarding (Tunneling)
SSH tunnels let you securely forward traffic between local and remote ports. There are three types.
Local Forwarding (-L)
Forward a local port to a remote destination. Classic use: accessing a database that’s only accessible from within a private network.
$ ssh -L 5432:db.internal:5432 user@bastion.example.com
Now localhost:5432 on your machine connects to db.internal:5432 via the bastion. Your Postgres client points to localhost and never touches the internet directly.
General form: -L local_port:destination_host:destination_port
Remote Forwarding (-R)
Forward a port on the remote server back to your local machine. Useful for exposing a local dev server through a public-facing host:
$ ssh -R 8080:localhost:3000 user@publicserver.com
Now publicserver.com:8080 routes to port 3000 on your laptop.
Dynamic Forwarding / SOCKS Proxy (-D)
Turns the SSH connection into a SOCKS5 proxy. Any application that supports SOCKS can route all traffic through it:
$ ssh -D 1080 user@jumpserver.com
Then configure your browser to use localhost:1080 as a SOCKS5 proxy.
Keeping Tunnels Open
Add -N (don’t execute a command) and -f (go to background):
$ ssh -N -f -L 5432:db.internal:5432 user@bastion.example.com
Jump Hosts (Bastion Servers)
Accessing a server that’s not directly reachable from the internet via a bastion:
$ ssh -J user@bastion.example.com user@private-host.internal
In ~/.ssh/config, this is cleaner:
Host private-host
HostName private-host.internal
User user
ProxyJump bastion
Host bastion
HostName bastion.example.com
User ec2-user
IdentityFile ~/.ssh/prod_key
Now ssh private-host transparently hops through the bastion.
For nested jumps (bastion → intermediate → target):
ProxyJump bastion,intermediate
Agent Forwarding
When you SSH to a bastion and then need to SSH onward to another host, agent forwarding lets the second host use the keys stored in your local ssh-agent — without ever copying your private key onto the bastion.
Enable per-connection:
$ ssh -A user@bastion.example.com
Or in config:
Host bastion
ForwardAgent yes
Important: only enable ForwardAgent on hosts you fully trust. A compromised server with your agent forwarded can authenticate as you to any other host your agent has access to.
Hardening the Server Side
These go in /etc/ssh/sshd_config on the server:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
MaxAuthTries 3
AllowUsers deploy ec2-user
After changing sshd_config, reload without dropping existing connections:
$ sudo systemctl reload sshd
Useful One-Liners
Copy a file from remote to local:
$ scp user@server:/var/log/app.log ./app.log
Copy a directory recursively:
$ scp -r user@server:/opt/backups/ ./backups/
Run a single command on a remote host without a shell:
$ ssh user@server "df -h && free -m"
Escape a hung session: If the connection freezes, type Enter ~ . (tilde then dot) to kill it.
Check what’s authorized on a remote server:
$ ssh user@server "cat ~/.ssh/authorized_keys"
Conclusion
Most SSH friction comes from typing the same options repeatedly and managing keys manually. A well-structured ~/.ssh/config eliminates that — you name your hosts, set their keys and ports once, and never think about it again. Tunnels and jump hosts extend that to networks that would otherwise require a VPN or a cumbersome multi-hop dance. Spend an hour setting this up and you’ll save it back within a week.