SSH Tips Every Developer Should Know
TL;DR
SSH config files eliminate repetitive flags. Jump hosts chain connections through bastions. Port forwarding tunnels any TCP traffic. Use Ed25519 keys. Set ControlMaster for connection reuse.
I used SSH for three years before I discovered ~/.ssh/config. Typing ssh -i ~/.ssh/mykey -p 2222 ubuntu@long-hostname.example.com every time was just the cost of doing business, I thought.
Then I spent 20 minutes on the config file and never typed a flag again.
The SSH Config File
Everything below goes in ~/.ssh/config. No restart needed — SSH reads it every connection.
# ~/.ssh/config
Host prod
HostName 203.0.113.42
User ubuntu
IdentityFile ~/.ssh/prod_key
Port 22
Host staging
HostName 203.0.113.55
User deploy
IdentityFile ~/.ssh/staging_key
Port 2222
# Wildcard for all company servers
Host *.internal.example.com
User ec2-user
IdentityFile ~/.ssh/company_key
StrictHostKeyChecking no
Now ssh prod works. That's it.
Connection Reuse (ControlMaster)
Every SSH connection does a full handshake. If you're running scp, git push, and ssh in a loop, you're paying that cost repeatedly.
# ~/.ssh/config
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 600
mkdir -p ~/.ssh/sockets
chmod 700 ~/.ssh/sockets
First connection opens a socket. Subsequent connections to the same host reuse it — near-instant. ControlPersist 600 keeps the socket alive 10 minutes after you close the last session.
Before/after:
Without ControlMaster: git push → new SSH handshake every time (~300ms)
With ControlMaster: git push → reuses socket (~20ms)
This matters when git push is over SSH and you're doing it 50 times a day.
Jump Hosts (Bastion Servers)
Your production servers aren't publicly reachable. You SSH to a bastion first, then to the target. Old way:
ssh bastion
# now on bastion
ssh internal-server
Better way with ProxyJump:
Host bastion
HostName bastion.example.com
User ec2-user
IdentityFile ~/.ssh/company_key
Host internal-server
HostName 10.0.1.50
User ubuntu
ProxyJump bastion
Now ssh internal-server automatically goes through the bastion. No manual two-step.
Chain multiple jump hosts if needed:
Host deep-internal
HostName 10.0.2.100
ProxyJump bastion,internal-server
From the command line:
ssh -J bastion ubuntu@10.0.1.50
Port Forwarding
Local Forwarding (access remote service locally)
# Forward local port 5432 to the database behind a bastion
ssh -L 5432:db.internal:5432 bastion
# Now connect your local Postgres client to localhost:5432
psql -h localhost -p 5432 -U postgres mydb
In config:
Host db-tunnel
HostName bastion.example.com
User ec2-user
LocalForward 5432 db.internal:5432
LocalForward 6379 redis.internal:6379
# Forward multiple services at once
ssh -N db-tunnel # -N: don't open a shell, just keep tunnels open
Remote Forwarding (expose local service to remote server)
# Expose your local port 3000 on the remote server's port 8080
ssh -R 8080:localhost:3000 server
# Someone can now hit server:8080 and reach your local dev environment
Useful for demos, webhooks in development, testing on a remote device.
Dynamic Forwarding (SOCKS proxy)
ssh -D 9090 bastion
# Configure your browser to use SOCKS5 proxy at localhost:9090
# All browser traffic routes through the bastion
Instant VPN-like access to everything the bastion can reach.
Key Management
# Generate Ed25519 key (modern, fast, small)
ssh-keygen -t ed25519 -C "your@email.com"
# Old RSA is fine, but use 4096 bits minimum if you must
ssh-keygen -t rsa -b 4096 -C "your@email.com"
# Add key to agent (avoid entering passphrase repeatedly)
ssh-add ~/.ssh/id_ed25519
# Add to macOS keychain permanently
ssh-add --apple-use-keychain ~/.ssh/id_ed25519
# ~/.ssh/config — auto-add keys to agent on macOS
Host *
AddKeysToAgent yes
UseKeychain yes # macOS only
IdentityFile ~/.ssh/id_ed25519
Copy public key to server:
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server
# Or manually:
cat ~/.ssh/id_ed25519.pub | ssh user@server "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
Useful One-Liners
# Run a command on remote server without opening a shell
ssh server "df -h && uptime"
# Copy files (scp uses SSH)
scp file.txt server:/tmp/
scp -r ./dir server:/opt/
# Mount remote filesystem locally (macFUSE/sshfs on Linux)
sshfs server:/var/www /mnt/remote
# Check which key will be used for a host
ssh -v server 2>&1 | grep "Offering public key"
# Escape hatch when SSH hangs: ~. (tilde dot)
# Type it to terminate a hung connection
# Get local SSH agent socket into a sudo shell
ssh -A server # Forward agent, then sudo -i will still have agent access
Hardening (For Servers You Run)
# /etc/ssh/sshd_config
PasswordAuthentication no # Keys only
PermitRootLogin no # Never root direct
MaxAuthTries 3 # Limit brute force
ClientAliveInterval 300 # Disconnect idle sessions
ClientAliveCountMax 2
AllowUsers ubuntu deploy # Explicit allowlist
sudo systemctl reload sshd
Debugging Connections
# Verbose output — shows exactly what's happening
ssh -v server # 1x verbose
ssh -vv server # 2x verbose
ssh -vvv server # 3x verbose (full debug)
# Common issues:
# "Permission denied" → wrong key, wrong user, or key not in authorized_keys
# Connection hangs → firewall blocking port, try -v to see where it stops
# "Host key verification failed" → server fingerprint changed, edit ~/.ssh/known_hosts
The Bottom Line
SSH is a power tool that most people use as a basic drill. The config file alone eliminates 80% of repetitive typing.
The rules:
- Put everything in
~/.ssh/config— stop typing flags - Enable
ControlMasterfor connection reuse, especially on slow networks - Use
ProxyJumpinstead of two-step SSH to bastions - Port forwarding lets you securely tunnel any TCP service
- Use Ed25519 keys — smaller, faster, more modern than RSA
- Disable password auth on any server you manage
SSH is one of those tools where 2 hours of learning saves 2 hours per week forever.