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
-Jflag 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-sha512algorithm 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
| Ed25519 | RSA 4096 | |
|---|---|---|
| Key size | 256-bit (68 chars) | 4096-bit (~ 750 chars) |
| Security level | ~128-bit equivalent | ~140-bit equivalent |
| Performance | Faster signing/verification | Slower |
| Compatibility | OpenSSH 6.5+ (2014) | Universal |
| Quantum resistance | None (same as RSA) | None |
| Recommendation | Default choice for new keys | Legacy 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):
- Command-line options (
-o,-p, etc.) - User config (
~/.ssh/config) - 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.
%ris the remote user,%his the host,%pis 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
-Aand-Xflags to preserve access control lists and extended attributes - Checksum-based transfers —
--checksumcompares 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
- OpenSSH Release Notes — changelog for every OpenSSH version
- ssh_config man page — complete client configuration reference
- sshd_config man page — complete server configuration reference
- Mozilla OpenSSH Guidelines — security-focused configuration recommendations
- SSH.com Academy — tutorials on keys, agents, and tunneling
- Arch Wiki: SSH Keys — practical key management guide
- ssh-audit — server configuration auditing tool
- rsync documentation — official rsync documentation
- SSH — Secure Login Connections over the Internet - primary reference on SSH by Tatu Ylonen, 1996
- SOCKS proxy - allows users to route network traffic to a server on a client’s behalf

Comments