DevOps April 18, 2026 10 min read

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.

laptop ssh prod-db bastion.example.com Jump host Public IP only prod-db 10.0.1.5 (internal) prod-api 10.0.1.6 (internal) prod-worker 10.0.1.7 (internal) ProxyJump chains connections through one public bastion to reach private hosts.

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 ed25519 keys. RSA is still valid but only at 3072+ bits. DSA and ECDSA P-256 have known weaknesses.
  • Disable password authentication server-side (PasswordAuthentication no in sshd_config).
  • Don't commit private keys to repos, even private ones. Use IdentityFile to 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.