Linux & Networking12 min

SSH — Key Management, Config & Tunnelling Cheat Sheet

Complete SSH reference — key generation, config file, port forwarding, tunnels, jump hosts, SCP, rsync, and hardening tips.

Connect & Basic Usage

# Connect
ssh user@hostname
ssh user@192.168.1.10
ssh -p 2222 user@hostname            # Non-default port

# Run remote command (non-interactive)
ssh user@host "uptime"
ssh user@host "df -h && free -m"

# Run remote command with sudo (requires TTY)
ssh -t user@host "sudo systemctl restart nginx"

# Exit
exit
# or Ctrl+D

Key Generation

# Ed25519 (recommended — fast, secure, small key)
ssh-keygen -t ed25519 -C "your@email.com"

# RSA 4096 (more compatible)
ssh-keygen -t rsa -b 4096 -C "your@email.com"

# Custom filename
ssh-keygen -t ed25519 -f ~/.ssh/work_key -C "work laptop"

# Keys are stored at:
ls ~/.ssh/
# id_ed25519      ← private key (600, never share!)
# id_ed25519.pub  ← public key (safe to copy anywhere)

Copy Public Key to Server

# Easiest way (recommended)
ssh-copy-id user@hostname
ssh-copy-id -i ~/.ssh/my_key.pub user@hostname

# Manual (if ssh-copy-id not available)
cat ~/.ssh/id_ed25519.pub | ssh user@host "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"

# Or on server:
mkdir -p ~/.ssh
echo "ssh-ed25519 AAAA... your@email.com" >> ~/.ssh/authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

SSH Config File (~/.ssh/config)

# Default settings for all hosts
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
    AddKeysToAgent yes
    IdentityFile ~/.ssh/id_ed25519

# Work server
Host work
    HostName 203.0.113.10
    User alice
    Port 2222
    IdentityFile ~/.ssh/work_key

# Bastion / jump host
Host bastion
    HostName bastion.example.com
    User ops
    IdentityFile ~/.ssh/bastion_key

# Internal server via bastion
Host internal
    HostName 10.0.1.20
    User ops
    ProxyJump bastion
# Now connect with simple alias
ssh work
ssh internal           # automatically hops through bastion

Jump Hosts (Bastion)

# Ad-hoc jump (OpenSSH 7.3+)
ssh -J user@bastion user@internal-host

# Multiple hops
ssh -J user@bastion1,user@bastion2 user@final-host

# Config-based (preferred)
# Add to ~/.ssh/config:
# Host internal
#     ProxyJump bastion

# Legacy ProxyCommand (older OpenSSH)
ssh -o ProxyCommand="ssh -W %h:%p bastion" user@internal-host

Port Forwarding & Tunnels

# Local forward: access remote-host:5432 via localhost:5432
# (connect a DB on a remote private network)
ssh -L 5432:remote-db:5432 user@bastion
ssh -L 8080:internal-api:80 user@gateway

# Usage after forwarding:
psql -h localhost -p 5432 -U admin mydb
curl http://localhost:8080/api

# Remote forward: expose local port 8080 on remote server's port 80
# (create a temporary public URL for local dev)
ssh -R 80:localhost:8080 user@public-server

# Dynamic (SOCKS5 proxy — route all browser traffic through SSH)
ssh -D 1080 user@host
# Then configure browser to use SOCKS5 proxy at localhost:1080

# Background tunnel (don't open shell)
ssh -N -L 5432:db:5432 -f user@bastion   # -N: no command, -f: background

SCP — Secure Copy

# Copy local file to remote
scp file.txt user@host:/remote/path/

# Copy remote file to local
scp user@host:/remote/file.txt ./local/

# Copy directory recursively
scp -r ./local-dir/ user@host:/remote/path/

# Via non-default port
scp -P 2222 file.txt user@host:/path/

# Via jump host
scp -J user@bastion file.txt user@internal:/path/

# Preserve timestamps
scp -p file.txt user@host:/path/

rsync — Efficient Sync

# Sync local dir to remote (only changed files)
rsync -av ./local-dir/ user@host:/remote/dir/

# Dry run (see what would change)
rsync -avn ./local-dir/ user@host:/remote/dir/

# Delete files on remote not in local
rsync -av --delete ./local-dir/ user@host:/remote/dir/

# Exclude patterns
rsync -av --exclude='*.log' --exclude='.git/' ./src/ user@host:/deploy/

# Progress bar
rsync -av --progress large-file.tar.gz user@host:/path/

# Over non-default SSH port
rsync -av -e "ssh -p 2222" ./dir/ user@host:/path/

SSH Agent

# Start agent
eval "$(ssh-agent -s)"

# Add key to agent (prompted for passphrase once)
ssh-add ~/.ssh/id_ed25519
ssh-add ~/.ssh/work_key

# List loaded keys
ssh-add -l

# Remove all keys
ssh-add -D

# Agent forwarding (use local keys on remote host)
ssh -A user@host           # -A: forward agent
# Or in ~/.ssh/config: ForwardAgent yes

Server Hardening (/etc/ssh/sshd_config)

# Disable root login
PermitRootLogin no

# Disable password authentication (key-only)
PasswordAuthentication no
ChallengeResponseAuthentication no

# Allow only specific users/groups
AllowUsers alice bob
AllowGroups ops

# Change default port (obscurity, not security)
Port 2222

# Limit auth attempts
MaxAuthTries 3
LoginGraceTime 20

# Disable empty passwords
PermitEmptyPasswords no

# Disable X11 forwarding if not needed
X11Forwarding no

# Restrict to IPv4 only
AddressFamily inet
# Reload after changes
sudo systemctl reload sshd

# Test config syntax before reloading
sudo sshd -t

Key Management

# View public key fingerprint
ssh-keygen -lf ~/.ssh/id_ed25519.pub
ssh-keygen -lf ~/.ssh/id_ed25519.pub -E md5

# Convert OpenSSH to PEM
ssh-keygen -p -m PEM -f ~/.ssh/id_rsa

# Remove a host from known_hosts
ssh-keygen -R hostname
ssh-keygen -R 192.168.1.10

# Verify remote host key fingerprint
ssh-keyscan -H hostname >> ~/.ssh/known_hosts
ssh-keyscan -H hostname | ssh-keygen -lf -

# Change passphrase on existing key
ssh-keygen -p -f ~/.ssh/id_ed25519

Escape Sequences (Inside SSH Session)

SequenceAction
~.Disconnect (when stuck)
~COpen command line (for port forwarding)
~&Background the session
~?Show all escape sequences

Prefix: Enter then ~ then the character.