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

Back to Documents