SSH Config Tricks: Aliases, Jump Hosts, and ControlMaster
Real SSH config patterns that save 10+ minutes a day — host aliases, ProxyJump, ControlMaster multiplexing, Include directives, and per-host keys.
A colleague on a distributed team used to SSH into eight different machines daily — staging, production, two build servers, a pair of database read replicas, a customer-facing bastion, and a GPU box in a university cluster. Each one needed a different user, a different key, and some of them sat behind a jump host that required separate authentication. Every morning started with the same twenty minutes of shell friction.
Then they cleaned up their ~/.ssh/config. After, the same eight connections were single words: ssh staging, ssh gpu, ssh prod-db. Morning shell ritual dropped to under three minutes.
SSH config isn't exciting. Most developers skim a tutorial, add a few aliases, and never open the file again. The patterns below are the ones that justify a second look.
Scenario: Managing a Multi-Environment Stack
Here's the setup we'll build up to. Three environments (dev, staging, prod), each with its own API server and database. Production sits behind a bastion host. Two GitHub accounts (work and personal). A GPU box at a university cluster, reachable only through the cluster's login node.
Without a config file, SSH looks like this every time:
ssh -i ~/.ssh/work_ed25519 -o ProxyJump=bastion.example.com,jumpuser@bastion.example.com ubuntu@10.0.1.5
Retyping that ten times a day is a tax. Every misremembered flag costs 30 seconds of fiddling. The config file turns it into ssh prod-db and gets the same connection, every time.
Host Aliases: The Entry-Level Win
The simplest pattern is a named host block:
Host staging
HostName staging.example.com
User deploy
Port 22
IdentityFile ~/.ssh/work_ed25519
Now ssh staging does the right thing. The Host alias is just a nickname — it can be anything, including a short string that bears no relation to the actual hostname. User, Port, and IdentityFile override command-line defaults. Adding PreferredAuthentications publickey skips password prompts for servers where you always use keys.
Wildcard Patterns and Defaults
When you have many hosts that share settings, wildcards save repetition:
Host *.internal.example.com
User deploy
IdentityFile ~/.ssh/work_ed25519
StrictHostKeyChecking accept-new
Host *
AddKeysToAgent yes
UseKeychain yes
ServerAliveInterval 60
ServerAliveCountMax 3
Settings resolve top-to-bottom with first-match-wins semantics. More specific blocks should appear before wildcards. The final Host * block sets global defaults for everything not matched earlier.
StrictHostKeyChecking accept-new is worth calling out. It automatically adds unseen hosts to known_hosts while still rejecting mismatches on known hosts. It's the sane middle ground between "prompt on every new host" (default) and "accept anything" (insecure).
ProxyJump: The Right Way to Use Bastion Hosts
Before OpenSSH 7.3 (August 2016), chaining connections through a jump host meant ugly ProxyCommand strings using ssh ... nc %h %p. OpenSSH 7.3 added ProxyJump, which is cleaner:
Host bastion
HostName bastion.example.com
User jumpuser
IdentityFile ~/.ssh/work_ed25519
Host prod-db prod-api prod-worker
User ubuntu
IdentityFile ~/.ssh/work_ed25519
ProxyJump bastion
Host prod-db
HostName 10.0.1.5
Host prod-api
HostName 10.0.1.6
Host prod-worker
HostName 10.0.1.7
Now ssh prod-db connects through the bastion automatically. The key thing: ProxyJump bastion references another host alias, not a raw hostname. That lets you configure the bastion's own authentication once and reuse it across all the internal hosts.
ProxyJump also handles multi-hop chains. ProxyJump bastion1,bastion2,bastion3 walks through each in order. This is rare in practice but occasionally necessary for segmented networks.
ControlMaster: Connection Multiplexing
Each SSH connection starts with a TCP handshake, a TLS-like key exchange, and authentication. For a local network this takes ~300 ms. Across a continent it takes 1-2 seconds. If you're running frequent short commands (git pushes, quick file checks, scripted deploys), this adds up fast.
ControlMaster multiplexes multiple SSH sessions over a single underlying connection. Once the first session is open, subsequent sessions attach to the existing connection and skip the handshake entirely:
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 600
The directory needs to exist: mkdir -p ~/.ssh/sockets. After that, every SSH command creates (or reuses) a socket at ~/.ssh/sockets/user@host-port. ControlPersist 600 keeps the master connection open for 600 seconds after the last session closes — so rapid consecutive connections reuse instead of reconnecting.
1.2s → 30ms
Typical reconnection time with ControlMaster active on a cross-continent host. The 40× speedup is enough that git push feels instant instead of laggy.
One caveat: operations that specifically need a fresh connection (checking whether a host is alive, testing a new auth config) can be forced with ssh -o ControlPath=none. And if you need to drop the master early, ssh -O exit host closes it cleanly.
The Include Directive: Splitting Config Across Files
A single monolithic ~/.ssh/config becomes unwieldy at around 15 hosts. Worse, it's hard to separate work and personal settings, and impossible to commit work config to a private repo without leaking personal config.
The Include directive (OpenSSH 7.3+) splits config across files:
# ~/.ssh/config
Include ~/.ssh/config.d/work
Include ~/.ssh/config.d/personal
Include ~/.ssh/config.d/clients/*
Host *
AddKeysToAgent yes
ServerAliveInterval 60
Now work config lives in one file (potentially committed to a private repo), personal config in another, and client-specific config in a directory that can be expanded freely. The Host * defaults still apply globally at the end.
Per-Host Keys: Multiple GitHub Accounts
The canonical use case for per-host keys is multiple GitHub accounts. You have a work GitHub and a personal one. Both use git@github.com:... as the remote URL. GitHub only allows a given SSH key to be associated with one account at a time.
The fix is two aliased hosts mapping to the same real hostname with different keys:
Host github-work
HostName github.com
User git
IdentityFile ~/.ssh/github_work
IdentitiesOnly yes
Host github-personal
HostName github.com
User git
IdentityFile ~/.ssh/github_personal
IdentitiesOnly yes
Now work repos use git@github-work:company/repo.git as their remote URL, and personal repos use git@github-personal:username/repo.git. The key selection happens automatically based on the alias.
IdentitiesOnly yes is important. Without it, SSH may try every key in ~/.ssh/ before hitting the correct one, and GitHub will reject authentication after too many bad attempts.
Keeping Flaky Connections Alive
Corporate VPNs, hotel WiFi, and spotty cellular connections all break long-running SSH sessions. ServerAliveInterval and TCPKeepAlive help:
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
TCPKeepAlive yes
ServerAliveInterval 60 tells SSH to send an encrypted keepalive to the server every 60 seconds if no other data has been sent. ServerAliveCountMax 3 says "give up if three of these get no response." The combination catches dead connections in ~3 minutes, which is short enough to not waste your time but long enough to survive brief network hiccups.
Match Patterns for Conditional Config
The Match directive is newer and less used than Host, but it handles cases Host can't:
Match host *.example.com exec "test -f ~/.ssh/work_ed25519"
IdentityFile ~/.ssh/work_ed25519
This applies settings only if a condition holds. exec runs a shell command and matches on exit code 0. Useful for dynamic situations — different keys depending on whether you're on the company VPN, or different ProxyJump depending on which cluster is currently reachable.
A Complete Annotated Example
# ~/.ssh/config
# --- Bastions and jumps ---
Host bastion
HostName bastion.example.com
User jumpuser
IdentityFile ~/.ssh/work_ed25519
# --- Production (all behind bastion) ---
Host prod-db prod-api prod-worker
User ubuntu
IdentityFile ~/.ssh/work_ed25519
ProxyJump bastion
Host prod-db
HostName 10.0.1.5
Host prod-api
HostName 10.0.1.6
Host prod-worker
HostName 10.0.1.7
# --- Staging ---
Host staging
HostName staging.example.com
User deploy
IdentityFile ~/.ssh/work_ed25519
# --- GitHub accounts ---
Host github-work
HostName github.com
User git
IdentityFile ~/.ssh/github_work
IdentitiesOnly yes
Host github-personal
HostName github.com
User git
IdentityFile ~/.ssh/github_personal
IdentitiesOnly yes
# --- Connection multiplexing everywhere ---
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 600
ServerAliveInterval 60
ServerAliveCountMax 3
AddKeysToAgent yes
UseKeychain yes
The Security Notes Worth Remembering
None of this is a substitute for basic SSH hygiene. A few things the Mozilla OpenSSH guidelines recommend, worth spot-checking:
- Use
ed25519keys. RSA is still valid but only at 3072+ bits. DSA and ECDSA P-256 have known weaknesses. - Disable password authentication server-side (
PasswordAuthentication noinsshd_config). - Don't commit private keys to repos, even private ones. Use
IdentityFileto reference keys in~/.ssh/and keep them on the filesystem. - Keep
~/.ssh/permissions at 700 and key files at 600. SSH refuses to use keys with overly permissive modes.
For a wider productivity setup that includes this level of SSH polish, the dotfiles and shell productivity guide covers related patterns — shell aliases, git config, editor setup — that round out the toolkit. And if you're working across Kubernetes clusters on top of this, the kubecontext workflow guide is the equivalent patterns at the kubectl layer.
For the canonical reference, the ssh_config(5) man page is dense but authoritative. Every option above has its formal semantics documented there, and reading a few sections at a time is one of the better investments in practical infrastructure knowledge a developer can make.