The Complete SSH Guide for 2026: Keys, Config, Tunneling, and Security

The Complete SSH Guide for 2026: Keys, Config, Tunneling, and Security

SSH has been the backbone of remote system administration since Tatu Ylönen created it in 1995, but the ecosystem has evolved enormously since then. Key types have changed, configuration options have multiplied, and entire new workflows have emerged around tunneling, multiplexing, and bastion hosts.

This guide consolidates what you need to know about SSH in 2026, modernizing tips from our earlier articles on using rsync over SSH and SSH keep-alive.

What Changed Since 2007

If you learned SSH a decade or two ago, here’s what’s different:

  • Ed25519 is the default key type. DSA keys have been removed entirely from OpenSSH. RSA still works but Ed25519 is faster, more secure, and produces much smaller keys.
  • ProxyJump replaced complex ProxyCommand chains. Since OpenSSH 7.3 (2016), you can hop through bastion hosts with a simple -J flag instead of nested SSH commands.
  • Connection multiplexing is built in. ControlMaster lets you reuse a single TCP connection across multiple SSH sessions — no more re-authenticating for every terminal tab.
  • Post-quantum key exchange is arriving. OpenSSH 9.0+ includes hybrid key exchange using the sntrup761x25519-sha512 algorithm to resist future quantum attacks.
  • SSH certificates are gaining adoption. Short-lived certificates signed by a CA are replacing authorized_keys management at scale.

For the full changelog, see the OpenSSH release notes.

SSH Key Management

Ed25519 vs RSA

Ed25519RSA 4096
Key size256-bit (68 chars)4096-bit (~ 750 chars)
Security level~128-bit equivalent~140-bit equivalent
PerformanceFaster signing/verificationSlower
CompatibilityOpenSSH 6.5+ (2014)Universal
Quantum resistanceNone (same as RSA)None
RecommendationDefault choice for new keysLegacy compatibility only

Generating Keys

# Ed25519 (recommended)
ssh-keygen -t ed25519 -C "you@example.com"

# RSA 4096 (only if you need legacy compatibility)
ssh-keygen -t rsa -b 4096 -C "you@example.com"

The -C comment flag is optional but helps identify keys when you have several.

Managing Keys with ssh-agent

The ssh-agent is a helper program that runs in the background to securely manage and store private keys for SSH public key authentication, eliminating the need to enter a passphrase for each connection. Add this to your ~/.ssh/config to automatically load keys on first use:

Host *
    AddKeysToAgent yes
    IdentitiesOnly yes

On macOS, you can also add UseKeychain yes to store passphrases in the system keychain.

Hardware Keys (FIDO2/YubiKey)

OpenSSH 8.2+ supports FIDO2 security keys directly. This means your private key never leaves the hardware token:

ssh-keygen -t ed25519-sk -C "you@example.com"

The -sk suffix generates a key that requires a physical touch on the security key to authenticate. See the Yubico SSH documentation for setup details.

SSH Certificates

For organizations managing access across many servers, SSH certificates eliminate the need to distribute authorized_keys files. A certificate authority (CA) signs short-lived certificates that servers trust automatically. This is a significant operational improvement over key-based access at scale. Smallstep has an excellent walkthrough of setting this up.

SSH Config File Essentials

Your ~/.ssh/config file is the most powerful and underused SSH feature. It saves you from typing long commands and lets you define per-host settings.

Config Hierarchy

SSH reads configuration in this order (first match wins):

  1. Command-line options (-o, -p, etc.)
  2. User config (~/.ssh/config)
  3. System config (/etc/ssh/ssh_config)

Host Blocks and Wildcards

# Specific host
Host webserver
    HostName 203.0.113.10
    User deploy
    Port 2222
    IdentityFile ~/.ssh/deploy_ed25519

# Wildcard — applies to all hosts in a domain
Host *.internal.example.com
    User admin
    ProxyJump bastion.example.com

# Default settings for all connections
Host *
    AddKeysToAgent yes
    ServerAliveInterval 60
    ServerAliveCountMax 3

With the config above, ssh webserver expands to the full connection details automatically.

Include Directive

Split your config into manageable pieces:

# ~/.ssh/config
Include config.d/*

This loads all files in ~/.ssh/config.d/, which is useful for separating work and personal configs, or machine-generated entries.

Match Blocks

For conditional configuration based on hostname, user, network, or other criteria:

# Use a specific key only when connecting from the office network
Match host *.corp.example.com exec "ip route | grep -q 10.0.0.0/8"
    IdentityFile ~/.ssh/corp_ed25519

See the ssh_config man page for the full list of Match criteria.

Practical Example Config

Here’s a realistic config combining common patterns:

# ~/.ssh/config

Include config.d/*

# Jump host
Host bastion
    HostName bastion.example.com
    User ops
    IdentityFile ~/.ssh/ops_ed25519
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 600

# Production servers (via bastion)
Host prod-*
    User deploy
    ProxyJump bastion
    IdentityFile ~/.ssh/deploy_ed25519

# Development VMs
Host dev
    HostName 192.168.1.100
    User dev
    ForwardAgent yes

# Global defaults
Host *
    AddKeysToAgent yes
    IdentitiesOnly yes
    ServerAliveInterval 60
    ServerAliveCountMax 3
    HashKnownHosts yes

Create the sockets directory if you’re using ControlPath:

mkdir -p ~/.ssh/sockets
chmod 700 ~/.ssh/sockets

Connection Management

Keep-Alive Configuration

If your SSH sessions drop after periods of inactivity, the fix is straightforward. Add to your ~/.ssh/config:

Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3

This sends a keepalive packet every 60 seconds. After 3 missed responses (3 minutes of no response), the client disconnects.

On the server side, the equivalent settings in /etc/ssh/sshd_config are:

ClientAliveInterval 60
ClientAliveCountMax 3

Back in 2008, one workaround was to run while date; do sleep 10; done to keep the connection alive by generating output. That worked, but ServerAliveInterval is the proper solution — it sends packets at the protocol level without cluttering your terminal.

Connection Multiplexing

Multiplexing reuses a single TCP connection for multiple SSH sessions. This eliminates the overhead of TCP handshake, key exchange, and authentication for subsequent connections to the same host.

Host *
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 600
  • ControlMaster auto — the first connection becomes the master; subsequent connections piggyback on it.
  • ControlPath — the Unix socket file used for multiplexing. %r is the remote user, %h is the host, %p is the port.
  • ControlPersist 600 — keep the master connection alive for 10 minutes after the last session closes.

You can check the status of a multiplexed connection:

ssh -O check webserver

And manually close it:

ssh -O exit webserver

Mosh: SSH for Unreliable Connections

Mosh (Mobile Shell) uses UDP instead of TCP, which makes it resilient to network changes. If your laptop switches from WiFi to a mobile hotspot, or you close the lid and reopen it later, Mosh picks up where you left off.

# Install (Debian/Ubuntu)
sudo apt install mosh

# Connect (requires mosh-server on the remote host)
mosh user@remote-host

Mosh doesn’t support port forwarding or agent forwarding, so it’s specifically for interactive terminal sessions.

rsync over SSH

rsync remains one of the most efficient tools for synchronizing files between systems. Back in 2007, Troy Johnson’s guide was the go-to reference for setting up rsync over SSH. A lot has improved since then.

Modern Defaults

Since rsync 2.6.0 (2004), SSH is the default transport. You no longer need -e ssh:

# These are equivalent now
rsync -avz -e ssh user@remote:/path/ /local/path/
rsync -avz user@remote:/path/ /local/path/

Essential Flags

# Basic sync with progress
rsync -avz --progress user@remote:/source/ /destination/

# Resume interrupted transfers
rsync -avz -P user@remote:/source/ /destination/
# -P is shorthand for --progress --partial

The core flags:

  • -a (archive) — preserves permissions, timestamps, symlinks, and recurses into directories
  • -v (verbose) — shows files being transferred
  • -z (compress) — compresses data during transfer
  • -P (progress + partial) — shows progress and keeps partial files for resuming

Mirroring with –delete

To make the destination an exact copy of the source, including deleting files that no longer exist on the source:

rsync -avz --delete user@remote:/source/ /destination/

Use --dry-run first to preview what would be deleted:

rsync -avz --delete --dry-run user@remote:/source/ /destination/

Exclude Patterns

rsync -avz --exclude='*.log' --exclude='.git/' user@remote:/source/ /destination/

# Or use an exclude file
rsync -avz --exclude-from='rsync-exclude.txt' user@remote:/source/ /destination/

rsync 3.x Improvements

Modern rsync (3.x) added several notable features:

  • Incremental recursion — starts transferring files before scanning the entire directory tree, significantly reducing memory usage on large transfers
  • ACL and xattr support — use -A and -X flags to preserve access control lists and extended attributes
  • Checksum-based transfers--checksum compares file checksums rather than size/timestamp, useful when timestamps are unreliable

For the full list, see the rsync documentation.

SSH Tunneling

SSH tunnels let you securely forward network traffic through an encrypted connection. This is essential for accessing services that aren’t exposed to the public internet.

Local Forwarding (-L)

Forward a port on your local machine to a remote destination through an SSH connection. This is commonly used with a bastion host (or jump host) — a hardened server that acts as the sole gateway to your internal network, so you don’t have to expose every server to the internet:

# Access a remote PostgreSQL database on localhost:5432
ssh -L 5432:db.internal:5432 user@bastion

# Access a remote web app on localhost:8080
ssh -L 8080:internal-app:80 user@bastion

The pattern is -L local_port:destination_host:destination_port. The destination is resolved from the SSH server’s perspective, so db.internal is a hostname the bastion can reach.

Remote Forwarding (-R)

Expose a local service through the remote server:

# Make your local dev server (port 3000) accessible on the remote server's port 8080
ssh -R 8080:localhost:3000 user@remote

This is useful for sharing a local development environment with colleagues on the same network as the remote server, or for webhook development.

Dynamic Forwarding (SOCKS Proxy)

Create a SOCKS proxy that routes all traffic through the SSH server:

ssh -D 1080 user@remote

Then configure your browser or application to use localhost:1080 as a SOCKS5 proxy. All traffic will be tunneled through the remote server.

Running Tunnels in the Background

Add -fN to run the tunnel without an interactive shell:

ssh -fNL 5432:db.internal:5432 user@bastion
  • -f — go to background after authentication
  • -N — don’t execute a remote command

Security Considerations

On servers, you can control which forwarding types are allowed in /etc/ssh/sshd_config:

AllowTcpForwarding yes       # or "local", "remote", "no"
GatewayPorts no              # Prevent binding forwarded ports to all interfaces
PermitTunnel no              # Disable layer-2/3 tunneling

Disable forwarding for service accounts or restricted users with Match blocks.

Bastion Hosts and ProxyJump

A bastion host (or jump host) is a hardened server that acts as a gateway to your internal network. Instead of exposing every server to the internet, you expose only the bastion.

ProxyJump (The Modern Way)

OpenSSH 7.3 (2016) introduced ProxyJump, which is the clean way to hop through bastion hosts:

# Command line
ssh -J bastion.example.com user@internal-server

# In ~/.ssh/config
Host internal-server
    HostName 10.0.1.50
    User admin
    ProxyJump bastion.example.com

Multi-Hop

Chain multiple jumps on the command line without any config:

ssh -J bastion1.example.com,10.0.1.5 admin@10.0.2.10

Or define the chain in your config so each host knows its own jump path:

Host bastion1
    HostName bastion1.example.com
    User ops

Host bastion2
    HostName 10.0.1.5
    User ops
    ProxyJump bastion1

Host target
    HostName 10.0.2.10
    User admin
    ProxyJump bastion2

With this config, ssh target is all you need — the full chain resolves automatically.

ProxyCommand (Legacy Alternative)

Before ProxyJump, you’d use ProxyCommand with a nested SSH call:

Host internal-server
    ProxyCommand ssh -W %h:%p bastion.example.com

This still works but ProxyJump is cleaner and handles multi-hop more gracefully.

Why ProxyJump Over Agent Forwarding

Agent forwarding (ForwardAgent yes) sends your SSH agent socket to the bastion, which means anyone with root access on the bastion can use your keys. ProxyJump avoids this entirely — the intermediate host never sees your keys. It just passes encrypted traffic through.

Only use ForwardAgent on hosts you fully trust, and prefer ProxyJump for bastion host configurations.

Security Hardening Checklist

These are sshd_config settings for servers. Apply them based on your threat model.

Authentication

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

# Limit authentication attempts
MaxAuthTries 3
LoginGraceTime 30

# Disable root login (use sudo instead)
PermitRootLogin no

Modern Cryptography

Restrict to modern algorithms. These options disable legacy ciphers, key exchange algorithms, and MACs:

KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256

Access Controls

# Restrict to specific groups or users
AllowGroups ssh-users
AllowUsers deploy admin

# Disable unused features
X11Forwarding no
PermitTunnel no
AllowAgentForwarding no
AllowStreamLocalForwarding no

Logging

LogLevel VERBOSE

VERBOSE logging records key fingerprints on login, which makes it possible to audit which key was used to authenticate.

Intrusion Prevention

fail2ban and sshguard monitor SSH logs and temporarily ban IPs after repeated failed login attempts. fail2ban is more flexible (supports many services beyond SSH), while sshguard is lightweight and SSH-focused.

Auditing Your Configuration

ssh-audit tests your server’s configuration and flags weak algorithms, known vulnerabilities, and configuration issues:

# Audit a remote server
ssh-audit example.com

# Audit your local sshd
ssh-audit localhost

For comprehensive server hardening guidelines, see the Mozilla OpenSSH guidelines.

Beyond Traditional SSH

Cloud providers have developed alternatives that reduce or eliminate the need to manage SSH keys and expose port 22. These are worth evaluating, especially for cloud-native infrastructure:

  • AWS Systems Manager Session Manager — Shell access to EC2 instances through the SSM agent, with no inbound ports required. Integrates with IAM for access control and CloudTrail for audit logging.

  • Google Cloud IAP TCP Forwarding — Tunnels SSH through Identity-Aware Proxy, gating access on IAM policies and device trust rather than network-level controls.

  • Tailscale SSH — Authenticates SSH sessions using WireGuard-based identity from the Tailscale mesh network. Supports ACLs and session recording without managing authorized_keys.

  • Cloudflare Tunnel — Routes SSH through Cloudflare’s network with Zero Trust access policies. No public IP or open ports needed on the origin server.

These tools layer their own authentication and authorization on top of (or instead of) SSH. But the underlying concepts — key-based auth, tunneling, secure configuration — remain the foundation. Understanding SSH well makes you more effective with any of these tools.

Additional Resources

Comments

Kevin Duane

Kevin Duane

Cloud architect and developer sharing practical solutions.