While managing servers directly via SSH is mostly an anti-pattern these days (there’s always that red-headed stepchild of a host that runs those cron entries you’re just not sure if you can delete), I still use it heavily. Here are some tips and usage patterns I’ve integrated into my workflow – and I’m sure I have barely scratched the surface!

Keep config in sync across workstations

I really like to keep my configuration in sync across devices (and backed up!). I use syncthing to accomplish this. I have all my machines (NAS, desktop, laptop{0…?}, phone, work machine) all setup to sync. This is crazy helpful to avoid losing keys and keep consistent configuration.

I recommend using the .stignore file to avoid synchronizing certain files (eg, work keys/config, known_hosts, authorized_keys). This is configured per-device!

// avoid sync conflicts with known hosts
// I prefer to directly manage which machines I can ssh into via my key
// skip synchronizing work keys/conf

Let your config files do the work

Entropy is great isn’t it? What was once a pretty simple id_rsa you setup when first cloning a git repo or configuring a raspberry pi has proliferated into a folder full of keys as you migrated between computers and setup a homelab and forgot old passphrases. How to keep track of which key to use on which servers? And forget keeping track of usernames…

Luckily, your ssh config can remember all of that!


On Linux, the global ssh config is located at /etc/ssh/ssh_config. It’s a great place to start familiaring yourself with the available options and to learn how ssh prioritizes them. You can also man ssh_config.

I keep my ssh config split up into appropriate files. This enables selective syncing and makes it much easier to manage over time.

Base config – ~/.ssh/config

I use this mostly to load in my application or environment specific configs, as well as to set some global Host settings:

# Apply to all hosts unless overridden by more explicit match
Host *
  # Only use identities (eg, keys) explicitly defined via CLI or ssh config
  # This avoids extra work trying invalid keys
  IdentitiesOnly yes

  # Check if remote server is alive, terminate session after 90s if not
  ServerAliveInterval 30
  ServerAliveCountMax 3

  # Automatically add keys to ssh-agent
  AddKeysToAgent yes

# Include my env or account specific options
Include config.d/*.conf

Home config – ~/.ssh/config.d/$HOMENETWORK.conf

This is for things like my NAS, other personal computers, etc.

Host $DESKTOP  # match exactly my desktop name
  # Specify my home username & personal ssh key
  User jesseops
  IdentityFile ~/.ssh/jesseops

  # Override IP to get around VPN issues capturing DNS lookups
  Hostname 192.168.x.x

Host *.localnet  # Match any host on my localnet
  ForwardAgent yes  # Let me jump from one host to another
  IdentityFile ~/.ssh/localnet_infra

Git config – ~/.ssh/confid./git.conf

# git specific keys!
Host github.com
  IdentityFile ~/.ssh/github

Host bitbucket.org
  IdentityFile ~/.ssh/bitbucket

Work config – ~/.ssh/config.d/work{,_git}.conf

Here I keep track of customizations for $current_job and can exclude it from synchronizing to my personal machines.

# Neat little trick in combination with my git config that allows me to override
# the keys I use to access github/bitbucket (useful for public hosted organizations)
Host github.com-work
  HostName github.com
  IdentityFile ~/.ssh/work

# Specify multiple hostname patterns
Host *.workdomain *.workdevdomain
  IdentityFile ~/.ssh/work
  User $USERID

# Add a bastion for accessing eg cloud hosted systems
Host work-bastion
  HostName $bastionip
  User ec2-user
  IdentityFile ~/.ssh/bastion-key
  StrictHostKeyChecking no  # not secure _but_ for ephemeral instances the host key changes often
  UserKnownHostsFile=/dev/null  # Lets just skip tracking host keys anyway

# Access host via bastion
Host someinternal.cloud
  HostName $internalip
  User ec2-user
  # May not be available in your ssh version, check out
  # https://superuser.com/questions/1253960/replace-proxyjump-in-ssh-config
  ProxyJump work-bastion  # exactly what it sounds like