The ~/.ssh/config File, Explained
Stop retyping hostnames, ports and key paths. What each useful option in an SSH config does, how first-match-wins really works, and a config worth copying.
Basics · Bartłomiej Żabicki · updated 2026-07-27 · 10 min read
If you are still typing ssh -p 2222 -i ~/.ssh/prod_ed25519 deploy@web1.example.com, you are
doing work a text file should be doing for you. ~/.ssh/config turns that into ssh web1, and
it is read by everything that speaks SSH — scp, rsync, git, and any client that imports it.
Here is what actually goes in it, the rule that silently breaks most people's config, and a starting point worth stealing.
The shape of the file
The file is a list of Host blocks. Each block matches one or more aliases, and the indented
options below it apply when that alias matches.
Host web1
HostName web1.example.com
User deploy
Port 2222
IdentityFile ~/.ssh/prod_ed25519
Now ssh web1 is enough. So is scp deploy.tar.gz web1:/tmp/ — see
SFTP vs SCP for which transfer tool to reach for — and so
is git clone web1:/srv/git/app.git. They all resolve the alias through the same file.
Two housekeeping details that bite people:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/config
SSH refuses to use a config file that other users can write to, and it does so with an error that does not obviously point at permissions.
The rule that breaks configs: first match wins
This is the single most important thing on this page, and it is the opposite of what most people assume.
SSH does not use the most specific match. It uses the first value it finds for each option, in file order, and then stops looking for that option.
So this config does not do what it looks like it does:
# WRONG — the wildcard is on top
Host *
User root
Host web1
HostName web1.example.com
User deploy # never applied: User was already set to root
ssh web1 connects as root, because Host * matched first and set User. Flip it:
# RIGHT — specific blocks first, wildcards last
Host web1
HostName web1.example.com
User deploy
Host *
ServerAliveInterval 30
ServerAliveCountMax 3
The habit to build: specific hosts at the top, Host * at the very bottom, containing only
options you genuinely want everywhere.
The options worth knowing
Identity
Host web1
HostName web1.example.com
User deploy
Port 2222
IdentityFile ~/.ssh/prod_ed25519
IdentitiesOnly yes
IdentitiesOnly yes deserves a paragraph of its own. Without it, SSH offers every key it can
find — the agent's keys, then each default filename — before the one you named. Servers commonly
set MaxAuthTries 6, and each offered key counts as an attempt. With five keys loaded you can be
disconnected before your correct key is ever tried, and the error you get is
Permission denied (publickey), which sends people
hunting in entirely the wrong place. Set it on any host where you have named an IdentityFile.
Reaching hosts you cannot reach directly
Host bastion
HostName bastion.example.com
User deploy
Host db
HostName db.internal
User deploy
ProxyJump bastion
ProxyJump tunnels an end-to-end connection through the bastion, verifying both host keys and
keeping your key on your own machine — see jump hosts and ProxyJump
for why that matters more than it sounds.
Tunnels, declared once
Host prod-db
HostName bastion.example.com
User deploy
LocalForward 5432 localhost:5432
LocalForward 6379 localhost:6379
ExitOnForwardFailure yes
Note the syntax difference that catches everyone: on the command line it is
-L 5432:localhost:5432 with colons; in the config it is LocalForward 5432 localhost:5432 with
a space after the local port. ExitOnForwardFailure yes makes SSH refuse to connect at all if
a forward could not be set up, instead of connecting and leaving you to wonder why nothing is
listening. The three forwarding types are covered in
port forwarding explained.
Keeping the connection alive
Host *
ServerAliveInterval 30
ServerAliveCountMax 3
Every 30 seconds the client asks the server whether it is still there; after three unanswered probes it gives up. This keeps NAT and firewall state from quietly dropping an idle session, and makes a genuinely dead connection fail in ninety seconds rather than hanging forever.
Reusing a connection
Host *
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 10m
The first connection to a host does the full handshake; subsequent ones ride the same TCP
connection and open instantly. If you run a lot of git push or rsync against one server, this
is the single biggest speed-up available. The socket lingers for ten minutes after the last
session closes.
One caveat: some servers dislike many channels on one connection, and a stale socket can wedge.
ssh -O exit web1 clears it.
Wildcards and patterns
Host *.internal
ProxyJump bastion
Host prod-*
User deploy
IdentityFile ~/.ssh/prod_ed25519
Host !prod-legacy prod-*
StrictHostKeyChecking yes
* matches any run of characters, ? matches one, and a leading ! excludes. Patterns match
against the alias you typed, not the resolved hostname — a common source of confusion when a
pattern "does not fire".
Splitting the file up
Include ~/.ssh/config.d/*
Include pulls in other files at that point in the ordering — useful for keeping work and
personal hosts separate, or for a directory that a tool manages. Remember first-match-wins still
applies: an Include near the top can shadow everything below it.
A config worth copying
# ---- specific hosts -------------------------------------------------
Host bastion
HostName bastion.example.com
User deploy
IdentityFile ~/.ssh/prod_ed25519
IdentitiesOnly yes
Host web1 web2
HostName %h.example.com
User deploy
Port 2222
IdentityFile ~/.ssh/prod_ed25519
IdentitiesOnly yes
Host db
HostName db.internal
User deploy
ProxyJump bastion
IdentityFile ~/.ssh/prod_ed25519
IdentitiesOnly yes
LocalForward 5432 localhost:5432
ExitOnForwardFailure yes
Host pi
HostName 192.168.1.42
User bart
IdentityFile ~/.ssh/home_ed25519
IdentitiesOnly yes
# ---- defaults, last ------------------------------------------------
Host *
ServerAliveInterval 30
ServerAliveCountMax 3
AddKeysToAgent yes
HashKnownHosts yes
%h in HostName expands to the alias you typed, which is why web1 and web2 share one block.
Checking what SSH will actually do
Stop guessing and ask it:
# every option that will apply to this host, fully resolved
ssh -G web1
# just the ones you care about
ssh -G db | grep -E '^(user|hostname|port|identityfile|proxyjump)'
ssh -G prints the effective configuration after all matching and precedence. If a setting is not
what you expect, this shows you in one line — and nine times out of ten it is a Host * block
sitting too high in the file.
Taking it with you
The config is portable, which is the point. Copying it to a new laptop reproduces your whole setup, and a client that parses it — including Moray on iPhone, iPad and Mac — can import your hosts, ports, users, identity files, jump hosts and forwards in one step rather than making you retype them on a touchscreen.
If you write only one thing today, make it IdentitiesOnly yes on your named hosts, and move
Host * to the bottom.

Written by Bartłomiej Żabicki
iOS developer writing Swift since 2015, with a homelab he administers from an iPhone more often than he would like to admit. Moray is the SSH client he wanted to exist.
Frequently asked
- Where does the SSH config file live?
- Per-user settings go in ~/.ssh/config, which should be mode 600. System-wide defaults live in /etc/ssh/ssh_config. The per-user file wins for anything it sets, and both are read for every outgoing connection.
- Why is my Host * block being ignored?
- It almost certainly is not — the opposite is happening. SSH uses the first value it finds for each option, so a Host * block placed at the top of the file wins over every specific block below it. Wildcards belong at the bottom.
- What does IdentitiesOnly yes do?
- It stops SSH offering every key it knows about and limits it to the IdentityFile entries you named. Without it, a server with MaxAuthTries 3 can reject you before your correct key is ever tried, which surfaces as a confusing permission denied.
try it yourself
Moray does all of this natively
A real terminal, dual-pane SFTP, tunnels and jump hosts on iPhone, iPad and Mac. Keys stay in the Keychain, hosts sync over your own iCloud, and there is no account to make.
Keep reading
- 11 minSSH from an iPhone or iPad: The Complete GuideWhat iOS actually permits, how to get a usable keyboard, and why your session drops when you switch apps.
- 8 minSFTP vs SCP: Moving Files Over SSH Without Losing Your MindWhy SCP fell out of favour, what SFTP does better, and how to copy server-to-server directly.
- 9 mintmux: SSH Sessions That Survive AnythingThe single habit that makes SSH from a phone practical: put the shell on the server, not in the app.