A working example
Include ~/.ssh/config.d/*
Host web
HostName 203.0.113.10
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
Host *.example.com
User admin
Host *
ServerAliveInterval 60
User user
Now ssh web means ssh -p 2222 -i ~/.ssh/id_ed25519_work deploy@203.0.113.10, and it also sends a keepalive every 60 seconds. The same names work for scp, sftp and rsync.
The file is ~/.ssh/config. It must not be writable by others, or ssh stops with Bad owner or permissions. Run chmod 600 ~/.ssh/config. System-wide settings are read after it, from /etc/ssh/ssh_config.
With dozens of hosts, the file is only part of the answer. See how to manage many SSH servers for names, groups and keys that stay tidy.
The first value wins
This is the rule that surprises people. Every Host block that matches applies, not only the first one. For each option, ssh keeps the first value it reads and ignores later ones. In the example:
ssh webgetsUser deploy, because its block comes beforeHost *.ssh db.example.comgetsUser admin.ssh othergetsUser user, and all three getServerAliveInterval 60.
So put specific hosts at the top and defaults at the end. If you move Host * to the top, its User wins everywhere. Options on the command line come first of all, so ssh -l other web, ssh other@web and ssh -p 3000 web override the file.
There is one exception worth knowing. IdentityFile adds up: a value in Host web-* and another in Host * are both tried, in order. Add IdentitiesOnly yes if you want to avoid keys from the agent, and leave IdentityFile out of Host * if you want one key per host.
Host patterns
| Pattern | Matches |
|---|---|
Host web db | Either name. |
Host *.example.com | Any name ending in .example.com. |
Host web-? | web- and one character. |
Host web-* !web-old | All web- names except web-old. A negation alone matches nothing: it only excludes from a positive match. |
The pattern is compared with what you type, not with the address it resolves to. %h stands for that name, which makes short names possible:
Host web-*
HostName %h.example.com
ssh web-1 now connects to web-1.example.com.
Include
Include ~/.ssh/config.d/*
This reads other files as if they were pasted in that place, which lets you keep one file per project. Relative paths start from ~/.ssh. It needs OpenSSH 7.3 or later.
Put it at the top of the file. A Host block continues until the next one, and an Include placed after it is part of that block. We tested this: with Include below a Host a block, the settings in the included file did nothing for any other host. You can use this on purpose, to include a file only for some hosts.
Match
Match is a more powerful form of Host, with conditions:
Match host "*.internal" user deploy
Port 2200
Match exec "test -f /tmp/at-office"
ProxyJump bastion
The first applies only when both conditions are true. The second runs a shell command and applies if it exits with 0, which is how you change settings by network or place. Note that this command runs each time the config is read, even with ssh -G.
The useful conditions are host, originalhost, user, localuser, exec, tagged, final and all. host checks the name after HostName is applied, and originalhost checks what you typed. final makes ssh read the configuration a second time, which lets a block see the result of the first pass:
Host *.corp
HostName %h.example.com
Match final host *.example.com
User admin
In OpenSSH 9.4 and later, you can also tag hosts, and select them with Match tagged or ssh -P:
Host web
Tag prod
Match tagged prod
User deploy
ProxyJump has its own page: how to use ProxyJump and a jump host in SSH.
See the result with ssh -G
ssh -G web
This prints every setting ssh would use for that host, with all files and rules applied, and exits without connecting. It is the fastest way to answer "why does it use that user?":
ssh -G web | grep -E '^(hostname|user|port|identityfile) '
To find out whether your config is the cause of a problem, ignore it for one command. -F none skips both the user file and the system file:
ssh -F none user@203.0.113.10
For the connection itself, ssh -v web prints which config lines were applied.
Common mistakes
- A
Host *at the top that overrides everything below it. - A global option at the end of the file. Options outside a block must come before the first
Host. At the end, they belong to the last block. - A typo in an option. ssh stops with
Bad configuration optionand connects to nothing, on any host. For an option that only some versions know, putIgnoreUnknownand the option name above it. - Secrets in the file. It holds paths to keys, not keys, and it has no password option. That is how it should stay.
Questions
Where is the SSH config file?
On Linux and macOS it is ~/.ssh/config in your home folder. On Windows it is C:\Users\name\.ssh\config. It does not exist until you create it. System-wide settings in /etc/ssh/ssh_config are read after it.
Why does ssh ignore a setting in my config?
Another matching block set the same option earlier, and the first value wins. Run ssh -G followed by the host name to see the value ssh will use, and move specific hosts above Host *.
Does the SSH config file work with scp, sftp and rsync?
Yes. They use ssh to connect, so the host names and options in ~/.ssh/config apply to them too, for example scp file.txt web:/tmp/.
Sources
- ssh_config(5), OpenBSD manual
- ssh(1), OpenBSD manual
- OpenSSH 7.3 release notes, Include
- OpenSSH 9.4 release notes, tags