The Unix Way ■ Episode 07
You type ssh -i ~/.ssh/prod_key -p 2222 deploy@192.168.50.12
fourteen times a day. You have done this for years. You have
committed the flags to muscle memory the way pianists commit
scales. It works. It is also entirely unnecessary.
There is a plain text file that reduces this to
ssh prod. It has existed since
1999.
~/.ssh/config. SSH reads it automatically on
every connection. No flag, no import, no reload. One file.
No GUI. No tool. No subscription.
The Basics
Host prod
HostName 192.168.50.12
User deploy
Port 2222
IdentityFile ~/.ssh/prod_key
Now: ssh prod. That is it.
scp prod:logs.tar.gz . works too.
rsync, git pull, VS Code Remote:
they all read this file. You configure once, everything
else follows.
Wildcards make it better:
Host dev-*
User developer
IdentityFile ~/.ssh/dev_key
Every host matching dev-* inherits the same
user and key. dev-api, dev-frontend,
dev-db: all covered. One pattern, not twelve
entries.
The Multiplexer
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 600
Your first connection opens TCP, negotiates keys, authenticates. Every subsequent connection to the same host reuses that socket. No handshake. No key exchange. Instant.
Ten terminals to the same server: one connection. The other nine are free. Rather changes the economics of SSH, that.
The Jump
Host internal
HostName 10.0.0.50
ProxyJump bastion
User admin
One line: ProxyJump bastion. SSH connects to
the bastion first, then tunnels through to the internal host.
No
agent forwarding,
which exposes your private keys on the bastion. No two-step
manual process. One command: ssh internal.
Before
OpenSSH 7.3
(2016), this required ProxyCommand with
netcat. Now it is one word.
The Keep-Alive
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
Your connection drops after five idle minutes. Not because the server closed it: because a NAT gateway, a firewall, or a load balancer decided the session was stale. This sends a heartbeat every 60 seconds. Three missed heartbeats: disconnect. No more frozen terminals where you stare at a cursor that will never move again.
The Security Defaults
The first comment material from the LinkedIn post is worth including here, because it turns a convenience file into a security posture.
Host *
IdentitiesOnly yes
AddKeysToAgent yes
HashKnownHosts yes
StrictHostKeyChecking ask
VisualHostKey yes
IdentitiesOnly tells SSH to try only the key
you specified, not every key loaded in the agent. When you
have five keys and the server allows three attempts before
locking you out, this matters.
AddKeysToAgent loads the key into
ssh-agent
after first use, so you do not run ssh-add
after every reboot. VisualHostKey prints a
randomart fingerprint on every connection. You will notice
when a host key changes. Rather more reliable than reading
hex strings at eight in the morning.
The Match Order
SSH config uses
first match wins.
When SSH encounters a directive for a matching host, it takes
the first value it finds and ignores later ones for the same
directive. The consequence: specific host blocks belong at the
top of the file, Host * defaults at the bottom.
This is not a convenience suggestion. It is a security
consideration. If your Host * block sets
StrictHostKeyChecking ask and a specific host
needs StrictHostKeyChecking no for automated
deployment, the specific block must appear first. Reverse the
order and the global default wins silently. SSH will not warn
you.
Multiple Identities, Same Host
You have a personal GitHub account and a work account. Both
connect to github.com. Both use
git@github.com. SSH has no way to distinguish
them by hostname alone.
The solution is HostKeyAlias combined with a
non-DNS hostname in the Host stanza:
Host github.private
HostName github.com
User git
IdentityFile ~/.ssh/private_key
HostKeyAlias github.com
Host github.work
HostName github.com
User git
IdentityFile ~/.ssh/work_key
HostKeyAlias github.com
Git remotes then use github.private or
github.work as the hostname. SSH resolves the
right identity transparently. HostKeyAlias
ensures both stanzas share the same known_hosts entry for
github.com, so you are not asked to verify the
host key twice.
For existing repositories, url.*.insteadOf in
~/.gitconfig rewrites remotes without touching
each clone:
[url "git@github.work:company/"]
insteadOf = git@github.com:company/
Every git clone git@github.com:company/repo.git
now silently routes through the work identity. No manual
remote editing. No wrapper scripts.
The Complete Config
For the impatient, the entire recommended baseline in one block:
# Production
Host prod
HostName 192.168.50.12
User deploy
Port 2222
IdentityFile ~/.ssh/prod_key
# Development wildcard
Host dev-*
User developer
IdentityFile ~/.ssh/dev_key
# Internal via bastion
Host internal
HostName 10.0.0.50
ProxyJump bastion
User admin
# Global defaults (last: first match wins)
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist 600
ServerAliveInterval 60
ServerAliveCountMax 3
IdentitiesOnly yes
AddKeysToAgent yes
HashKnownHosts yes
StrictHostKeyChecking ask
VisualHostKey yes
Before first use:
mkdir -p ~/.ssh/sockets && chmod 700 ~/.ssh
That is the entire setup. No package manager involved.
The Point
~/.ssh/config is a plain text file. It works
on FreeBSD, Linux, macOS, and anything running
OpenSSH.
It has not changed its syntax in 25 years. It requires no
package, no daemon, no update cycle.
One file replaces connection managers, credential GUIs, bastion scripts, SSH wrapper tools, and the muscle memory of typing the same 80-character command fourteen times a day.
The full reference is one command away:
man ssh_config. Then open the file. You will
wonder why you waited.