Darwvin

The SSH config file: a practical guide to Host, ProxyJump, IdentityFile and more

The SSH config file explained: Host and Match precedence, ProxyJump bastions, IdentityFile, ControlMaster multiplexing, Include, and a full example.

Published 12 min read

The SSH client reads ~/.ssh/config before each connection, so one file can turn ssh -p 2222 -i ~/.ssh/work_ed25519 deploy@203.0.113.10 into ssh web-prod. The rule that catches most people is precedence: for most keywords, the first value the client finds wins, so the order of your Host blocks matters as much as their contents. This guide covers the keywords you will use every day, then gives a complete annotated file you can adapt.

How ssh chooses its settings

ssh takes settings from three sources, in this order: command-line options, your user file at ~/.ssh/config, and the system-wide file at /etc/ssh/ssh_config (ssh_config(5)). For each keyword, the first value found is used. A command-line option beats the file, and a line in your user file beats the same line in the system file.

Two commands show what ssh will actually do:

ssh -G web-prod      # print the effective settings, without connecting
ssh -v web-prod      # debug lines name the config file and the blocks applied

ssh -G has been available since OpenSSH 6.8. It prints the result after evaluating Host and Match blocks, so it settles arguments about which value wins. To check a single keyword, filter the output. For the db-01 alias in the complete example at the end of this guide, it looks like this:

$ ssh -G db-01 | grep -E '^(user|hostname|proxyjump) '
user deploy
hostname db-01.corp.example
proxyjump bastion

-F selects a different file. With -F, the system-wide file is skipped, and -F none reads no configuration at all (ssh(1)).

Format rules

  • Keywords are case-insensitive, and arguments are case-sensitive.
  • A keyword and its value are separated by whitespace, or by exactly one = that may have spaces around it, so Port 2222 and Port=2222 mean the same thing.
  • Quote values that contain spaces, as in IdentityFile "~/.ssh/work key".
  • Empty lines and lines starting with # are comments.

Host blocks and patterns

A Host line starts a block that runs until the next Host or Match line. The pattern is matched against the name you type, because ssh_config(5) says the host is usually the hostname argument given on the command line. Patterns can use * for zero or more characters and ? for exactly one character. A pattern prefixed with ! negates the match. If a negated pattern matches, the Host line is ignored, even when another pattern on that line matches.

Host *.corp.example !legacy.corp.example
    User deploy

ssh app.corp.example gets User deploy. ssh legacy.corp.example skips the block and uses your local user name. A Host * block at the end of the file supplies defaults for any keyword that no earlier block set. Use an alias for each machine you type often, and reserve wildcards for groups that share settings.

Precedence: the first value wins

ssh_config(5) says that, unless noted otherwise, the first specified value for each directive is used. It recommends host-specific declarations near the beginning of the file and general defaults at the end. The mistake this causes is common:

# Wrong: the catch-all comes first, so it wins for every host
Host *
    User deploy

Host db-01
    User dba

ssh -G db-01 reports user deploy, and the db-01 block never applies to that keyword. Moving the catch-all to the bottom fixes it:

Host db-01
    User dba

Host *
    User deploy

Now ssh -G db-01 reports user dba, and every other host still gets deploy.

Include follows the same rule because it is read where it appears. An Include at the top of the file beats a later Host block for the same keyword, so shared defaults belong at the bottom and specific overrides above them.

Match blocks

Match is the conditional form of Host. It applies the lines below it only when its conditions are met, and the block ends at the next Host or Match line. ssh_config(5) lists criteria including host, user, localuser, localnetwork, tagged, exec and canonical. Most criteria take an argument, and any criterion can be negated with !.

Match host *.corp.example user deploy
    ProxyJump bastion

Every condition on the line has to hold. ssh -l deploy db.corp.example applies the block, and ssh -l other db.corp.example does not.

HostName, User and Port

An alias is where the file pays off:

Host web-prod
    HostName 203.0.113.10
    User deploy
    Port 2222

Now ssh web-prod connects to 203.0.113.10 on port 2222 as deploy. That address comes from a range reserved for documentation (RFC 5737), and the host names in these examples use the reserved .example domain (RFC 2606). Replace them with your own.

%h expands to the name you typed. It saves a line when several aliases share a naming pattern:

Host db-01 app-01
    HostName %h.corp.example

ssh app-01 now connects to app-01.corp.example.

IdentityFile and IdentitiesOnly

IdentityFile names a private key that ssh offers to the host, and the path can start with ~. Most keywords keep the first value, but multiple IdentityFile lines add to the list of identities that ssh tries (ssh_config(5)). A Host * block with an IdentityFile therefore adds that key to every connection instead of replacing the host's own key.

When ssh-agent holds many keys, ssh offers them in turn. The server limits authentication attempts per connection, and MaxAuthTries defaults to 6 (sshd_config(5)), so the right key can come too late. IdentitiesOnly yes tells ssh to use only the identities you configured, even when the agent offers more.

Host git.example.com
    User git
    IdentityFile ~/.ssh/id_ed25519_git
    IdentitiesOnly yes

If a key is rejected outright, the publickey troubleshooting guide covers the usual causes.

ProxyJump and bastions

ProxyJump connects through one or more intermediate hosts. It was added in OpenSSH 7.3. Its value takes [user@]host[:port] or an ssh URI, and several hops are separated by commas. ssh(1) describes -J as a shortcut for this keyword. ProxyJump none turns it off, which is useful when a broad Host block sets it. ssh_config(5) notes that ProxyJump and ProxyCommand compete, and whichever is specified first takes effect, so set one or the other for a host.

Host bastion
    HostName bastion.example
    User jumpuser
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes

Host db-01
    HostName db-01.corp.example
    User dba
    ProxyJump bastion
    IdentityFile ~/.ssh/id_ed25519_db
    IdentitiesOnly yes

ssh db-01 connects to the bastion first, then opens a TCP connection from there to db-01.corp.example. The bastion has its own block, so its user and key are set separately. ssh(1) notes that command-line directives generally apply to the destination host, not the jump hosts, so keep jump-host settings in the file. The one-off equivalent is ssh -J bastion db-01.

Connection sharing: ControlMaster, ControlPath and ControlPersist

Every new SSH connection repeats the handshake and key exchange. Connection sharing lets later sessions reuse one connection, and it stays off unless you turn it on:

Host *
    ControlMaster auto
    ControlPath ~/.ssh/cm-%C
    ControlPersist 10m

ControlMaster auto makes ssh reuse a master connection if one exists, and create one if it does not (ssh_config(5)). ControlPath names the socket. %C expands to a hash of the local host name, the remote host, the port, the remote user and the ProxyJump value, so each connection gets its own socket name without a long path. ControlPersist was added in OpenSSH 5.6. The value 10m keeps the master running until it has been idle for ten minutes, and ssh -G reports that value as 600 seconds.

Control commands work on a running master (ssh(1)):

ssh -O check web-prod
ssh -O exit web-prod
ssh -O forward -L 15433:db-01.corp.example:5433 bastion
ssh -O cancel -L 15433:db-01.corp.example:5433 bastion

check reports whether the master is running, and exit closes it. forward adds a port forward to an existing connection with no new login, and cancel removes one. Pass the same -L specification to cancel that you used to add the forward.

Two cautions apply. A running master keeps the settings it started with, so after you change ProxyJump, IdentityFile or a similar keyword, run ssh -O exit on that host before you test. The control socket gives access to a live connection, so keep ControlPath in your home directory, as the example does, rather than in a shared temporary directory.

Keepalives and timeouts

Host *
    ServerAliveInterval 30
    ServerAliveCountMax 3
    ConnectTimeout 10

ServerAliveInterval defaults to 0, which means ssh sends no keepalive probes, so a silent connection can hang until something gives up. The two ServerAlive keywords detect dead connections, and the port forwarding guide shows how they keep tunnels up. ConnectTimeout limits how long ssh waits for the server, in seconds, instead of the system TCP timeout (ssh_config(5)). If a firewall or NAT device drops idle connections after a fixed period, choose a ServerAliveInterval shorter than that period so the probes keep the connection active.

Include: splitting the file

When the file grows, move host groups into fragments:

# ~/.ssh/config
Include config.d/*.conf

Relative paths in your user config resolve from ~/.ssh, so this line reads ~/.ssh/config.d/*.conf. Wildcards expand in lexical order. Include can also appear inside a Host or Match block. The included lines then apply only when that block matches, which ssh_config(5) calls conditional inclusion. The Include directive was added in OpenSSH 7.3.

Forwards in the config

LocalForward, RemoteForward and DynamicForward can live in a Host block, so one short command opens the tunnel. You can list several forwards, and forwards given on the command line add to the list (ssh_config(5)).

Host db-tunnel
    HostName bastion.example
    User deploy
    LocalForward 15432 db-01.corp.example:5432
    ControlPath none
    ExitOnForwardFailure yes

ssh -N db-tunnel runs the tunnel. ControlPath none disables connection sharing for this host, so the tunnel neither joins nor depends on a master connection (ssh_config(5)). The port forwarding guide covers the flags, server settings and restart options.

A complete annotated example

This file uses only the keywords covered above. Each alias in it resolves cleanly with ssh -G.

# ~/.ssh/config
# First value wins for each keyword: specific blocks first, defaults last.

# Jump host, reached directly
Host bastion
    HostName bastion.example
    User jumpuser
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes

# Private hosts, reached through the bastion. %h expands to the alias.
Host db-01 app-01
    HostName %h.corp.example
    User deploy
    ProxyJump bastion
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes

# Database tunnel: run "ssh -N db-tunnel", then connect to 127.0.0.1:15432
Host db-tunnel
    HostName bastion.example
    User jumpuser
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes
    LocalForward 15432 db-01.corp.example:5432
    ControlPath none
    ExitOnForwardFailure yes

# Git server with its own key
Host git.example.com
    User git
    IdentityFile ~/.ssh/id_ed25519_git
    IdentitiesOnly yes

# Defaults for every other host. Keep this block last.
Host *
    ServerAliveInterval 30
    ServerAliveCountMax 3
    ConnectTimeout 10
    ControlMaster auto
    ControlPath ~/.ssh/cm-%C
    ControlPersist 10m

Check the effective values for each alias before you rely on them:

for h in bastion db-01 db-tunnel git.example.com; do ssh -G "$h" | grep -E '^(hostname|user|proxyjump) '; done

To add a host, copy one of the blocks above the Host * line, change its HostName, User and IdentityFile, and run ssh -G newhost to confirm the result. Keep Host * last, because if it sits above a block, its values win for any keyword both set.

Common mistakes

SymptomLikely causeFix
Wrong user or portA Host * block above the specific blockMove Host * to the end of the file
Too many authentication failuresThe agent offers its keys before the right one, and the server's MaxAuthTries limit is reachedSet IdentityFile and IdentitiesOnly yes
Settings unchanged after an editA running control master keeps its old settingsRun ssh -O exit HOST, then connect again
Path with a space failsThe value is not quotedQuote it: IdentityFile "~/.ssh/work key"
Included file ignoredA relative Include path resolves under ~/.ssh, so the file is not where Include looksPut the file under ~/.ssh/ or use an absolute path

If a setting still surprises you, start with ssh -G for the value that won, then run ssh -v to see which blocks matched.

FAQ

Do I need to restart anything after editing ~/.ssh/config?

No. Each new ssh command reads the file. A running control master keeps its old settings, so close it with ssh -O exit HOST before you test a change.

Can I store a password in this file?

No. ssh_config has no keyword that holds a password. The file only names key files by path, so keep each private key in its own file, protect it with a passphrase, and load it into ssh-agent.

What is the difference between ProxyJump and ProxyCommand?

ProxyJump connects through SSH hosts, which makes it the simpler choice for a bastion. ProxyCommand runs a command that provides the connection, which helps when the hop is not a plain SSH host. ssh_config(5) says they compete, and the one specified first takes effect, so use one per host.

Can my SSH client import this file?

RemoteOpsX can import an OpenSSH config file, so the hosts defined here can come into the app. Review the imported list afterwards, because aliases, jump hosts and key paths are only as correct as the file they came from.

Why does ssh keep offering the wrong key?

Without IdentitiesOnly yes, ssh also offers the keys held by the agent, even when you configured a specific key for the host. Set IdentityFile for the host together with IdentitiesOnly yes, then confirm with ssh -G host. The key setup guide covers creating the right key first.

References

Get product news by email

We use your address only for these emails. The unsubscribe link is in every email. Read our privacy policy.