SSH Cheatsheet
Quick reference for SSH — key management, port forwarding, tunneling, jump hosts, sshd hardening, and certificate authentication.
Quick Reference
Basic Connection
# Basic connections
ssh user@hostname
ssh [email protected]
ssh -p 2222 user@host # Non-standard port
ssh -i ~/.ssh/mykey user@host # Specific key file
# Connection modes
ssh -v user@host # Verbose (debug level 1)
ssh -vv user@host # More verbose
ssh -vvv user@host # Maximum debug output
ssh -q user@host # Quiet mode
ssh -T [email protected] # Test connection, no shell
ssh -N user@host # No remote command (for tunnels)
ssh -f user@host -N # Background after auth
# Remote command execution
ssh user@host 'uptime'
ssh user@host 'ls -la /var/log'
ssh user@host 'sudo systemctl status nginx'
Key Management
# Generate keys (prefer ed25519)
ssh-keygen -t ed25519 -C "[email protected]"
ssh-keygen -t ed25519 -f ~/.ssh/mykey -C "deploy key"
ssh-keygen -t rsa -b 4096 -C "legacy system key"
ssh-keygen -t ecdsa -b 521 -C "ecdsa key"
# Copy public key to remote host
ssh-copy-id user@host
ssh-copy-id -i ~/.ssh/mykey.pub user@host
ssh-copy-id -p 2222 user@host
# Manual authorized_keys entry format
# options keytype base64-key comment
# Example:
# from="10.0.0.*",no-port-forwarding ssh-ed25519 AAAA... user@host
# command="/usr/local/bin/backup.sh" ssh-ed25519 AAAA... backup-key
# View public key fingerprint
ssh-keygen -lf ~/.ssh/id_ed25519.pub
ssh-keygen -lf ~/.ssh/id_ed25519.pub -E md5 # MD5 format
# Remove a host from known_hosts
ssh-keygen -R hostname
ssh-keygen -R [hostname]:2222
SSH Agent
# Start ssh-agent
eval "$(ssh-agent -s)"
# Add keys to agent
ssh-add ~/.ssh/id_ed25519
ssh-add ~/.ssh/mykey
ssh-add -t 3600 ~/.ssh/id_ed25519 # Expire in 1 hour
# List loaded keys
ssh-add -l
ssh-add -L # Full public keys
# Remove keys from agent
ssh-add -d ~/.ssh/mykey # Remove specific key
ssh-add -D # Remove all keys
# Persistent agent (add to ~/.bashrc or ~/.zshrc)
if [ -z "$SSH_AUTH_SOCK" ]; then
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
fi
Port Forwarding
# Local port forwarding: -L local:remote
# Access remote service on local port
ssh -L 8080:localhost:80 user@remotehost
# Now: http://localhost:8080 -> remotehost:80
# Forward to a 3rd host (via SSH server)
ssh -L 5432:db.internal:5432 user@bastion
# Now: localhost:5432 -> db.internal:5432 (via bastion)
# Remote port forwarding: -R remote:local
# Expose local service on remote host
ssh -R 9090:localhost:3000 user@remotehost
# Now: remotehost:9090 -> local:3000
# Dynamic port forwarding: -D (SOCKS proxy)
ssh -D 1080 -N -q -f user@remotehost
# Configure browser to use SOCKS5 proxy at localhost:1080
# Keep tunnel alive
ssh -L 5432:db:5432 \
-N -f \
-o ServerAliveInterval=60 \
-o ServerAliveCountMax=3 \
user@bastion
File Transfer
# scp (simple copy)
scp file.txt user@host:/remote/path/
scp -r ./localdir user@host:/remote/
scp -P 2222 file.txt user@host:/tmp/
scp user@host:/remote/file.txt .
# sftp (interactive)
sftp user@host
sftp> ls
sftp> cd /var/www
sftp> put localfile.txt
sftp> get remotefile.txt
sftp> mkdir newdir
sftp> exit
# rsync over SSH (preferred for large transfers)
rsync -avz -e ssh ./dir/ user@host:/dest/
rsync -avz -e "ssh -p 2222" ./dir/ user@host:/dest/
rsync -avz --delete ./dir/ user@host:/dest/
rsync -avz --exclude="*.log" ./dir/ user@host:/dest/
rsync -n ./dir/ user@host:/dest/ # Dry run
# sshfs (mount remote filesystem)
sshfs user@host:/remote/path /mnt/remote
sshfs -p 2222 user@host:/remote /mnt/remote
fusermount -u /mnt/remote # Unmount
Multiplexing (Persistent Connections)
# In ~/.ssh/config — share one TCP connection
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h:%p
ControlPersist 10m
# Create sockets directory
mkdir -p ~/.ssh/sockets
chmod 700 ~/.ssh/sockets
# Manual multiplexing
ssh -M -S ~/.ssh/sockets/myhost user@host # Master
ssh -S ~/.ssh/sockets/myhost user@host # Slave (reuses conn)
# Check multiplex status
ssh -S ~/.ssh/sockets/myhost -O check user@host
# Close master connection
ssh -S ~/.ssh/sockets/myhost -O exit user@host
# Benefit: subsequent connections to same host are instant
# (no new TCP handshake / key exchange)
Jump Hosts (Bastion)
# Single jump host
ssh -J bastion.example.com user@internal-server
# Specify port for jump host
ssh -J [email protected]:2222 user@internal
# Multi-hop chain
ssh -J bastion1,bastion2 user@final-server
# ProxyJump in ~/.ssh/config (cleaner)
Host internal-server
HostName 10.0.1.50
User ubuntu
ProxyJump bastion
# ProxyCommand (older alternative, still works)
Host internal-old
HostName 10.0.1.60
User ubuntu
ProxyCommand ssh -W %h:%p bastion.example.com
# Run command on internal via jump
ssh -J bastion user@internal 'df -h'
# Port forward through jump host
ssh -J bastion \
-L 5432:db.internal:5432 \
user@internal -N
~/.ssh/config Full Example
# ~/.ssh/config — SSH client configuration
# Global defaults
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 600
AddKeysToAgent yes
IdentitiesOnly yes
StrictHostKeyChecking accept-new
# Bastion / Jump server
Host bastion bastion.example.com
HostName bastion.example.com
User ubuntu
Port 22
IdentityFile ~/.ssh/bastion_ed25519
ForwardAgent no # Do NOT forward agent to untrusted hosts
# Internal servers (accessed via bastion)
Host app-server
HostName 10.0.1.10
User deploy
IdentityFile ~/.ssh/deploy_ed25519
ProxyJump bastion
Host db-server
HostName 10.0.2.20
User ubuntu
IdentityFile ~/.ssh/db_ed25519
ProxyJump bastion
# Auto-tunnel for local DB access
LocalForward 15432 localhost:5432
# GitHub
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/github_ed25519
AddKeysToAgent yes
# Staging environment
Host staging-*
User ubuntu
IdentityFile ~/.ssh/staging_ed25519
ProxyJump bastion-staging
Host bastion-staging
HostName staging-bastion.example.com
User ubuntu
IdentityFile ~/.ssh/staging_ed25519
# Legacy server with old algorithms
Host legacy-server
HostName 192.168.100.50
User admin
IdentityFile ~/.ssh/id_rsa
HostKeyAlgorithms +ssh-rsa
PubkeyAcceptedKeyTypes +ssh-rsa
SSH Tunneling Use Cases
Access Internal Web App
# Make internal app available at localhost:8080
ssh -L 8080:internal-app.company.internal:80 \
-N -f -q \
[email protected]
# Access: http://localhost:8080
MySQL / PostgreSQL Tunnel
# Tunnel to RDS or internal DB
ssh -L 13306:mydb.rds.amazonaws.com:3306 \
-N -f -q \
[email protected]
# Then connect locally:
mysql -h 127.0.0.1 -P 13306 -u dbuser -p
psql -h 127.0.0.1 -p 15432 -U dbuser mydb
SOCKS5 Proxy for Browser
# Create SOCKS5 proxy on localhost:1080
ssh -D 1080 -N -f -q user@remotehost
# Configure browser:
# Firefox: Settings -> Network -> Manual proxy -> SOCKS Host: 127.0.0.1 Port: 1080
# Use with curl
curl --socks5 127.0.0.1:1080 https://internal-site.company.com
SSH Agent Forwarding
Security Warning: Agent forwarding (
ForwardAgent yes) allows anyone with root access on the intermediate server to use your SSH keys. Prefer ProxyJump instead — it connects directly without exposing your agent.
# Agent forwarding (use with caution)
ssh -A user@bastion # Forward agent to bastion
# From bastion, can SSH to internal servers using local keys
# Check what keys are forwarded
ssh-add -l
# Safe alternative: ProxyJump (no agent exposure)
ssh -J bastion user@internal # Direct connection, keys stay local
# In ~/.ssh/config:
# AVOID: ForwardAgent yes (unless you fully trust the host)
# PREFER: ProxyJump bastion
# If you must use ForwardAgent, restrict it to trusted hosts:
Host trusted-bastion
HostName bastion.example.com
ForwardAgent yes # Only for this specific host
sshd_config Hardening
# /etc/ssh/sshd_config — Server hardening
# Port and protocol
Port 2222 # Change from default 22
ListenAddress 0.0.0.0 # Restrict listen interface
Protocol 2
# Authentication
PermitRootLogin no # Never allow root login
PasswordAuthentication no # Keys only
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
ChallengeResponseAuthentication no
UsePAM yes
# Access control
AllowUsers deploy admin ci-runner # Whitelist users
# AllowGroups sshusers # OR restrict by group
MaxAuthTries 3 # Max attempts before disconnect
MaxSessions 5
# Session limits
LoginGraceTime 30 # Time to authenticate
ClientAliveInterval 300 # Send keepalive every 5min
ClientAliveCountMax 2 # Disconnect after 2 missed keepalives
# Features to disable
X11Forwarding no
AllowTcpForwarding no # Disable if tunnels not needed
AllowStreamLocalForwarding no
GatewayPorts no
PermitTunnel no
PermitUserEnvironment no
# Banner and logging
Banner /etc/issue.net
PrintLastLog yes
LogLevel VERBOSE # Logs key fingerprints
# Algorithms (restrict to strong only)
KexAlgorithms curve25519-sha256,ecdh-sha2-nistp521
Ciphers [email protected],[email protected]
MACs [email protected],[email protected]
HostKeyAlgorithms ssh-ed25519,ecdsa-sha2-nistp521
# Apply changes
sshd -t # Test config syntax
systemctl restart sshd
SSH Certificate Authentication
Tip: Certificate-based auth is superior to key-based for large teams. Instead of distributing public keys to every server, servers trust your CA. When a user leaves, revoke their cert without touching any server.
# === SSH CA Setup (one-time) ===
# Generate CA key pair (keep private key offline/in HSM)
ssh-keygen -t ed25519 -f ssh_ca -C "SSH Certificate Authority 2026"
# === Sign a user key ===
# User generates their own key
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519
# Admin signs the public key
ssh-keygen -s ssh_ca \
-I "[email protected]" \ # Certificate identity
-n ubuntu,ec2-user,deploy \ # Allowed principals (usernames)
-V +52w \ # Valid for 52 weeks
~/.ssh/id_ed25519.pub
# This creates ~/.ssh/id_ed25519-cert.pub
# View the certificate
ssh-keygen -L -f ~/.ssh/id_ed25519-cert.pub
# === Configure servers to trust the CA ===
# Copy CA public key to servers
scp ssh_ca.pub user@server:/etc/ssh/
# /etc/ssh/sshd_config on each server:
TrustedUserCAKeys /etc/ssh/ssh_ca.pub
# === Known_hosts with CA (client-side) ===
# ~/.ssh/known_hosts
@cert-authority *.example.com ssh-ed25519 AAAA...ca-public-key...
# This trusts any server cert signed by the CA for *.example.com
# No more "The authenticity of host X can't be established" prompts!
Troubleshooting
Common Errors & Fixes
# Error: Host key verification failed
# Fix: Remove stale key from known_hosts
ssh-keygen -R hostname
# Or edit ~/.ssh/known_hosts and remove the line for that host
# Error: Permission denied (publickey)
# Diagnose with verbose mode
ssh -vvv user@host 2>&1 | head -50
# Fix: Check permissions (SSH is strict about this)
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
chmod 600 ~/.ssh/authorized_keys
chmod 644 ~/.ssh/config
chmod 700 ~/.ssh # Directory must be 700
# On the server:
chmod 700 ~
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
# Error: Connection refused
# Check sshd is running
systemctl status sshd
ss -tlnp | grep :22
# Check firewall allows the port
firewall-cmd --list-all | grep 22
ufw status | grep 22
# Error: Too many authentication failures
# Too many keys offered — specify which key
ssh -o IdentitiesOnly=yes -i ~/.ssh/specific_key user@host
# Debug levels
ssh -v user@host # Basic: connection, auth method
ssh -vv user@host # More: key exchange, cipher negotiation
ssh -vvv user@host # Maximum: all packet details