SSH config file: set up ~/.ssh/config once and connect to every server by name
The SSH config file, ~/.ssh/config, stores per-server settings for the ssh client, so a command such as ssh -J admin@bastion.example.org:2222 deploy@web1.internal.example.org shrinks to ssh web1. Each Host block gives a short alias and lists options such as HostName, User, Port and IdentityFile. One rule explains most surprises: for every option, the first value ssh finds is the one it uses, so specific blocks belong at the top and general defaults at the bottom.
The OpenSSH ssh_config(5) manual page sets the order: ssh takes command-line options first, then the user file ~/.ssh/config, then the system-wide /etc/ssh/ssh_config. Keywords are case-insensitive, arguments are case-sensitive, and lines starting with # are comments. The ssh -G outputs below come from a test run with OpenSSH 10.2p1.
Where the file lives and who may write to it
| System | Per-user file | System-wide file |
|---|---|---|
| Unix-like systems with OpenSSH | ~/.ssh/config | /etc/ssh/ssh_config |
| Windows (OpenSSH client) | %userprofile%\.ssh\config | %programdata%\ssh\ssh_config |
If the file does not exist yet, create it with strict permissions:
mkdir -p ~/.ssh && chmod 700 ~/.ssh
touch ~/.ssh/config && chmod 600 ~/.ssh/config
The ssh_config(5) manual asks for read and write access for the user and no write access for others, and ssh(1) recommends that the ~/.ssh directory is not accessible by others. In the OpenSSH source, ssh rejects a default user config file that belongs to someone other than root or the current user, or that other users can write to, and stops with Bad owner or permissions on and the path.
A working example for three servers
This file defines a jump host, two web servers and a database server that needs its own key:
Host bastion
HostName bastion.example.org
User admin
Port 2222
Host db1
IdentityFile ~/.ssh/id_db
Host web1 web2 db1
HostName %h.internal.example.org
User deploy
ProxyJump bastion
Host *
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
ServerAliveInterval 30
| Option | What it does | Detail from the manual |
|---|---|---|
Host | starts a block for the listed names or patterns, up to the next Host or Match | a single * gives defaults for all hosts |
HostName | the real name or IP address to connect to | defaults to the name typed on the command line; %h inserts that name |
User | login name on the server | saves typing user@ |
Port | port on the server | default 22 |
IdentityFile | private key to try | several lines add up |
IdentitiesOnly | use only the configured keys, even if the agent offers more | default no |
ProxyJump | reach the host through one or more jump hosts, separated by commas | none disables it |
With this file, ssh web1 connects to web1.internal.example.org as deploy through bastion, which takes its own user and port from its block. The ssh(1) manual explains why: options given on the command line generally apply to the destination and not to jump hosts, so jump host settings belong in the config file.
Which value wins: order, wildcards and one exception
Every block whose pattern matches is applied, but for each option the first value obtained is kept. A general block placed first therefore overrides everything below it:
Host *
User root
Host web1
HostName web1.example.org
User deploy
Here ssh -G web1 reports user root. With the Host * block moved to the end it reports user deploy. The exception is IdentityFile: multiple lines add to the list of identities tried, unlike other directives. In the three-server example, ssh -G db1 lists ~/.ssh/id_db first and ~/.ssh/id_ed25519 second, and IdentitiesOnly yes stops the agent from offering any further keys.
Patterns use * for any number of characters, ? for exactly one and ! to negate. If a negated entry matches, the whole Host line is ignored, which suits exceptions:
Host *.internal.example.org !bastion.internal.example.org
ProxyJump bastion.internal.example.org
In a test, app.internal.example.org received the ProxyJump line and bastion.internal.example.org did not. A command-line option still beats the file: with Port 2222 in a block, ssh -p 2200 reported port 2200.
Keep sessions alive and reuse connections
ServerAliveInterval defaults to 0, so no probes are sent. With 30 and the default ServerAliveCountMax of 3, an unresponsive server ends the session after roughly 90 seconds; the manual's own example uses 15 seconds and about 45. The probes travel inside the encrypted channel, unlike TCP keepalives, which the manual calls spoofable.
Connection sharing needs three lines, best added to the final Host * block:
ControlMaster auto
ControlPath ~/.ssh/cm-%C
ControlPersist 10m
ControlMaster auto tries an existing master connection first and falls back to a new connection if none exists. ControlPath names the socket; the manual recommends including %h, %p and %r, or %C (a hash of local host name, remote host name, port, user name and the ProxyJump setting), in a directory other users cannot write to. ControlPersist 10m keeps the master in the background until it has been idle for ten minutes. ssh -O check web1 reports whether a master is running and ssh -O exit web1 asks it to stop. Forwarded agent and X11 display are those of the master connection, as the manual notes.
Test and debug a change
ssh -G web1prints the configuration after theHostandMatchblocks are evaluated, then exits. For the example file,ssh -G web1 | grep -E '^(hostname|user|port|proxyjump) 'printeduser deploy,hostname web1.internal.example.org,port 22andproxyjump bastion.ssh -v web1adds debug messages, and each extravadds detail, up to three. The linesReading configuration dataandApplying options for web1, the latter preceded by file name and line number, show which blocks matched.ssh -F ./test-config -G web1tries an alternative file. When-Fis given, the system-wide file is ignored, so system defaults cannot interfere.Include ~/.ssh/config.d/*splits a large file. Wildcards are expanded in lexical order, and paths without a leading slash are read relative to~/.ssh.
ProxyJump and Include arrived in OpenSSH 7.3 (1 August 2016), ssh -G in 6.8 (18 March 2015) and ControlPersist in 5.6 (23 August 2010). The web copies of the manual describe the latest development release, so man ssh_config on the machine itself is the reference for the installed version.Common questions
Where is the SSH config file?
On Unix-like systems the per-user file is ~/.ssh/config and the system-wide file is /etc/ssh/ssh_config. The Windows OpenSSH client reads %userprofile%\.ssh\config and %programdata%\ssh\ssh_config.
What permissions does ~/.ssh/config need?
Read and write for the owner and no write access for others, which chmod 600 gives. ssh stops with "Bad owner or permissions" if another user owns the file or other users can write to it.
Which Host block wins when several match?
All matching blocks apply, but for each option the first value obtained is used, so put specific hosts first and Host * last. IdentityFile is the exception: every line is added to the list of keys tried.
How can I see which settings ssh will use?
ssh -G name prints the configuration after Host and Match blocks are evaluated, then exits. ssh -v name adds debug output, including the config file lines that were applied.
How do I use a jump host in the SSH config file?
Add ProxyJump bastion to the target Host block and describe the jump host in its own block. Command-line options apply to the destination, not to jump hosts, so their settings belong in the file.
- OpenSSH: Manual Pages (web copies reflect the latest development release)
- OpenSSH: Release notes
- Debian Manpages: ssh_config(5), openssh-client in Debian stable
- Debian Manpages: ssh(1), openssh-client in Debian stable
- Microsoft Learn: OpenSSH Server configuration for Windows (client config locations)
- OpenSSH Portable source: readconf.c (permission check and debug messages)
More guides

UFW firewall on Ubuntu: a safe setup over SSH, step by step
Set up a UFW firewall on Ubuntu 24.04 LTS: deny incoming by default, allow SSH first, rate-limit logins, handle IPv6 and avoid the Docker bypass.