SSH config tricks I keep forgetting

The ~/.ssh/config features I always have to look up, in one place so I don't have to look them up again.

· 3 min read

I’ve written a fair amount about SSH on this site — key generation, key permissions, reverse tunnels — but the file I touch more than any other is ~/.ssh/config, and I keep looking the same things up. Writing them down so I don’t have to.

Host blocks

The basic unit. Instead of typing ssh -p 2222 -i ~/.ssh/deploy_key [email protected] every time:

Host prod
  HostName prod.example.com
  User deploy
  Port 2222
  IdentityFile ~/.ssh/deploy_key

Now ssh prod just works. So does scp file.tar.gz prod:/tmp/, and rsync, and anything else that speaks SSH.

Wildcards and defaults

Host blocks apply in order, and later directives don’t override earlier ones. So you put specific hosts first and catch-all defaults last:

Host prod staging
  User deploy
  IdentityFile ~/.ssh/work_key

Host *.internal.example.com
  User admin
  ProxyJump bastion

Host *
  ServerAliveInterval 60
  ServerAliveCountMax 3
  AddKeysToAgent yes
  UseKeychain yes          # macOS only
  IdentitiesOnly yes

IdentitiesOnly yes is the one I always forget and the one that causes the most grief. Without it, ssh offers every key in your agent to every server, which can trip rate limits on GitHub or lock you out of servers with MaxAuthTries set low.

ProxyJump (the -J flag)

The modern replacement for the old ProxyCommand ssh bastion nc %h %p incantation. To reach a host that’s only accessible through a bastion:

Host bastion
  HostName bastion.example.com
  User ops

Host app-*.internal
  User deploy
  ProxyJump bastion

Now ssh app-01.internal transparently jumps through the bastion. You can chain them: ProxyJump bastion1,bastion2.

Include

The file can include other files, which is what saved me when my config crossed 300 lines:

Include ~/.ssh/config.d/work
Include ~/.ssh/config.d/personal
Include ~/.ssh/config.d/clients/*

Each file can have its own Host blocks. Perfect for separating work from personal, or keeping per-client config in files you can gitignore.

Match (the escape hatch)

When Host isn’t enough — for example, you want different settings based on hostname pattern, username, or even the output of a command:

Match host *.gitlab.com exec "test -f ~/.ssh/gitlab_key"
  IdentityFile ~/.ssh/gitlab_key
  IdentitiesOnly yes

I use this less than I expected to, but when I need it nothing else works.

The ones I use almost every day

  • LocalForwardLocalForward 5432 db.internal:5432 turns ssh prod into “open a tunnel to the prod Postgres on localhost:5432.” Connect your local pgcli to it.
  • RemoteForward — the opposite direction, for when you need the remote host to see a local service.
  • ControlMaster auto / ControlPath ~/.ssh/cm_%h_%p_%r / ControlPersist 10m — opens one SSH connection and reuses it for subsequent ssh calls to the same host. Makes repeated ssh prod <cmd> feel instant instead of authenticating from scratch each time.
Host *
  ControlMaster auto
  ControlPath ~/.ssh/cm_%h_%p_%r
  ControlPersist 10m

That block alone probably saves me 20 seconds a day, every day.

The debugging one-liner

When something in the config doesn’t work the way you expect: ssh -vvv host dumps exactly which config files were read, which Host blocks matched, and which options ended up active. 90% of “why isn’t my key being used” questions answer themselves in the -vvv output.

#SSH #Config