fakessh.eu
fakessh.eu
A black patch panel in an open server rack with a single braided blue Ethernet cable plugged into one port among rows of identical empty ports.

SSH config file: set up ~/.ssh/config once and connect to every server by name

updated 5 November 2026

SUMMARYThe SSH config file, ~/.ssh/config, holds per-host settings for the ssh client. A Host block defines an alias with HostName, User, Port and IdentityFile, so ssh web1 replaces a long command. For each option the first value found wins, so specific blocks go first and Host * last.

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

SystemPer-user fileSystem-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
OptionWhat it doesDetail from the manual
Hoststarts a block for the listed names or patterns, up to the next Host or Matcha single * gives defaults for all hosts
HostNamethe real name or IP address to connect todefaults to the name typed on the command line; %h inserts that name
Userlogin name on the serversaves typing user@
Portport on the serverdefault 22
IdentityFileprivate key to tryseveral lines add up
IdentitiesOnlyuse only the configured keys, even if the agent offers moredefault no
ProxyJumpreach the host through one or more jump hosts, separated by commasnone 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

  1. ssh -G web1 prints the configuration after the Host and Match blocks are evaluated, then exits. For the example file, ssh -G web1 | grep -E '^(hostname|user|port|proxyjump) ' printed user deploy, hostname web1.internal.example.org, port 22 and proxyjump bastion.
  2. ssh -v web1 adds debug messages, and each extra v adds detail, up to three. The lines Reading configuration data and Applying options for web1, the latter preceded by file name and line number, show which blocks matched.
  3. ssh -F ./test-config -G web1 tries an alternative file. When -F is given, the system-wide file is ignored, so system defaults cannot interfere.
  4. 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.
Older servers and clients differ. According to the OpenSSH release notes, 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.

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.