SSH Config File Examples: A Real ~/.ssh/config for 10+ Servers
Copy-ready ssh config file examples for 10+ servers: Host patterns, ProxyJump, IdentitiesOnly, keepalives and ControlMaster, for OpenSSH on Ubuntu 24.04.
The ssh config file lives at ~/.ssh/config. It holds Host blocks that turn long ssh commands into short aliases. With 10 or more servers, a good layout puts specific hosts first, then wildcard groups like prod-*, and a Host * block of defaults last. Use ProxyJump for private machines, one IdentityFile per context with IdentitiesOnly yes, and keepalives plus ControlMaster in the defaults block. The full example below is the kind of file we keep next to DockLab. Each piece is explained after it.
Quick answer
- Put the file at
~/.ssh/configand runchmod 600 ~/.ssh/config. - Write the most specific
Hostblocks first andHost *last. For each option, ssh keeps the first value it finds. - Group servers with patterns (
Host prod-*) and build the real name withHostName %h.internal.example.com. - Reach private servers with
ProxyJump bastion, and pin keys withIdentityFileplusIdentitiesOnly yes. - In
Host *, setServerAliveInterval 30,ControlMaster auto,ControlPath ~/.ssh/cm/%CandControlPersist 10m. - To check what applies to a host, run
ssh -G hostname.
The full example: one file for a real fleet
This file covers a typical mixed setup: a bastion host, production servers on a private network, staging on a non-standard port, a client's server with its own key, a home lab and GitHub. Hostnames and IPs are placeholders.
# ~/.ssh/config (chmod 600)
# 1. Extra files first, so their Host blocks win over the defaults below
Include config.d/*.conf
# 2. Jump host
Host bastion
HostName bastion.example.com
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
# 3. On the office network or VPN, reach prod directly (must come before Host prod-*)
Match originalhost prod-* localnetwork 10.20.0.0/16
ProxyJump none
# 4. Production: private addresses, only reachable through the bastion
Host prod-*
HostName %h.internal.example.com
User deploy
ProxyJump bastion
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
# 5. Staging: public, non-standard port
Host staging-*
HostName %h.example.com
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
# 6. A client server with its own key and user
Host acme-web
HostName 203.0.113.40
User ubuntu
IdentityFile ~/.ssh/id_ed25519_acme
IdentitiesOnly yes
# 7. Home lab: shared settings, then per-host addresses
Host pi nas
User admin
IdentityFile ~/.ssh/id_ed25519_home
Host pi
HostName 192.168.1.20
Host nas
HostName 192.168.1.30
# 8. GitHub over SSH
Host github.com
User git
IdentityFile ~/.ssh/id_ed25519_github
IdentitiesOnly yes
# 9. Defaults for everything, always last
Host *
ServerAliveInterval 30
ServerAliveCountMax 3
ControlMaster auto
ControlPath ~/.ssh/cm/%C
ControlPersist 10m
AddKeysToAgent yes
StrictHostKeyChecking accept-new
ForwardAgent no
With this file, ssh prod-api-3 connects as deploy to prod-api-3.internal.example.com. It goes through the bastion on port 2222 and uses the work key. If a master connection is already open, it reuses it. scp, rsync -e ssh, sftp and git read the same file, so these aliases work in all of them.
How ssh reads the file (and why order matters)
Most broken configs come from one rule. The ssh_config(5) man page says that "for each configuration directive, the first specified value will be used". ssh reads options from the command line first, then ~/.ssh/config, then /etc/ssh/ssh_config. In each file it goes top to bottom and applies every Host or Match block that matches. Once a directive has a value, later blocks cannot change it.
Two directives are exceptions: IdentityFile and CertificateFile add to a list. So every matching block contributes keys, and ssh tries them in order.
In practice:
- Specific first, general last. If
Host *sits at the top withUser root, every host getsroot. A laterUser deployhas no effect. - The
Match ... localnetworkblock comes beforeHost prod-*so thatProxyJump noneis set first. If you swap them, the bastion is always used. - Command-line flags win over everything.
ssh -o ProxyJump=none prod-api-3is a quick way to override a setting once.
Host patterns for 10+ servers
Patterns turn 30 servers into a handful of blocks. * matches any characters and ? matches exactly one. A Host line can list several space-separated patterns, and a pattern starting with ! excludes hosts:
# Every host under example.com except the bastion
Host *.example.com !bastion.example.com
User deploy
According to the man page, "a negated match will never produce a positive result by itself". A line with only !something never matches anything, so there must always be a positive pattern next to it.
The HostName %h.internal.example.com line in the production block does most of the work. %h expands to the name you typed. Adding a server then needs no config change: once DNS has prod-cache-1.internal.example.com, ssh prod-cache-1 works.
We use a naming convention that the patterns can rely on: env-role-n (prod-web-1, staging-db-1). It keeps patterns simple, and tab completion in bash and zsh picks up the aliases.
Match for conditions a Host line cannot express
Match checks things other than the name. The example uses localnetwork, which compares your machine's interface addresses with a CIDR range. On the office network or VPN, it skips the bastion. The Ubuntu 24.04 man page (OpenSSH 9.6p1) lists localnetwork among its Match criteria. Upstream also warns that addresses from DHCP are not a trustworthy signal. Use it for convenience, never for security decisions.
Use originalhost rather than host here. host matches after HostName has been substituted, so it would see prod-web-1.internal.example.com instead of prod-web-1. Newer criteria such as Match version and Match sessiontype arrived in OpenSSH 10.0, so they do not exist on Ubuntu 24.04's client.
ProxyJump: reaching private servers through a bastion
ProxyJump bastion tells ssh to connect to bastion first and then tunnel to the target. It replaces the old ProxyCommand ssh -W %h:%p bastion idiom. A few details that matter in practice:
- The bastion needs its own block. The man page says the destination's settings do not carry over to jump hosts. The
bastionblock above sets its port, user and key. - The bastion resolves the target name. Because the connection is opened from the bastion,
%h.internal.example.comonly has to resolve inside the private network, not on your laptop. - Chains are allowed.
ProxyJump bastion1,bastion2hops through both, in order. - ProxyJump and ProxyCommand conflict. Whichever one ssh reads first disables the other. If an old
ProxyCommandin/etc/ssh/ssh_configor an included file seems to be ignored, or seems to override your jump, that is the reason.
For a one-off jump without editing the file, run ssh -J [email protected]:2222 [email protected].
IdentityFile, IdentitiesOnly and "Too many authentication failures"
When ssh-agent holds many keys, ssh offers them one by one. The server's MaxAuthTries (6 by default in sshd) can run out before the right key is tried, and you get Received disconnect ... Too many authentication failures. The man page describes IdentitiesOnly yes as built for this situation, "where ssh-agent offers many different identities". With it, ssh only offers the keys configured for that host, including ones held by the agent.
Our rules:
- One key per context (work, each client, home, GitHub). If a client relationship ends, you remove one key in one place.
IdentitiesOnly yeswhereverIdentityFileis set.- No
IdentityFileinHost *. For unknown hosts, ssh keeps its default key search. AddKeysToAgent yes, so a passphrase is entered once per agent session, not on every connection.
On macOS, Apple's ssh also supports UseKeychain yes. That option is not upstream OpenSSH, and a Linux client will refuse a config that contains it. If you share one file across machines, put IgnoreUnknown UseKeychain above it.
Keepalives: ServerAliveInterval and ServerAliveCountMax
Sessions that drop after a few idle minutes are usually caused by a NAT or firewall forgetting the connection. ServerAliveInterval 30 sends an encrypted probe after 30 seconds of silence from the server. ServerAliveCountMax 3 disconnects after three unanswered probes. Together they keep idle sessions alive and close a dead connection in about 90 seconds, instead of leaving a frozen terminal. The man page's own example works the same way: an interval of 15 with the default count of 3 drops after about 45 seconds.
These probes go through the encrypted channel, unlike TCPKeepAlive, so they cannot be spoofed. They also work through a ProxyJump chain.
ControlMaster: reuse one connection for everything
Multiplexing opens one SSH connection per host and runs later sessions over it. A second ssh prod-web-1, an scp, or a git fetch then starts almost instantly, with no new handshake or key prompt. Through a bastion, the savings add up quickly.
Create a private socket directory
mkdir -p ~/.ssh/cm && chmod 700 ~/.ssh/cmThe man page says to keep sockets in a directory other users cannot write to. ssh does not create this directory, so connections fail until it exists.
Add the three directives to Host *
ControlMaster auto ControlPath ~/.ssh/cm/%C ControlPersist 10mautoreuses a master if one exists and creates one if not.%Cis a hash of local host, remote host, port, user and jump host, which gives a short, unique socket name.ControlPersist 10mkeeps the master open for 10 idle minutes after the last session closes.Check and close masters
ssh -O check prod-web-1 # "Master running (pid=...)" ssh -O exit prod-web-1 # close it
Trade-offs to know about:
- A stale master hangs new sessions after a laptop sleeps or the network changes.
ssh -O exit host(or deleting the socket) fixes it. - Later sessions skip authentication. If you change
IdentityFileor the key on the server, nothing changes until the master exits. - scp and sftp changed in OpenSSH 10.0. They now pass
ControlMaster no, so they no longer start masters themselves. They can still use a master that an interactive ssh session opened.
Host key checking without the prompt fatigue
StrictHostKeyChecking accept-new adds the key of a host you have never seen. It still refuses to connect if a known host's key changes. That removes the "Are you sure you want to continue connecting" prompt when you add servers, while still catching the case that matters. The trade-off is that the first connection is trusted without checking. If your environment publishes host key fingerprints, keep the default ask and compare them.
ForwardAgent no is already the default. We set it explicitly so nobody adds yes to Host * later. Per the man page, anyone who can get past file permissions on the remote host can use your forwarded agent to authenticate as you. If you need your GitHub key on one server, enable forwarding for that single host only, or use a deploy key.
Splitting the file with Include
Past about 20 hosts, one file gets hard to diff and review. Keep the defaults in ~/.ssh/config and move groups into ~/.ssh/config.d/:
mkdir -p ~/.ssh/config.d
chmod 700 ~/.ssh/config.d
ls ~/.ssh/config.d
# 10-work.conf 20-acme.conf 30-home.conf
Wildcards expand in lexical order, so number prefixes control which file is read first. This also makes offboarding easy: when a client contract ends, delete 20-acme.conf and the matching key.
Debugging: what ssh actually applied
Two commands answer almost every "why didn't my config apply" question:
# Effective config for a host, without connecting
ssh -G prod-api-3 | grep -Ei '^(hostname|user|port|proxyjump|identityfile|controlpath) '
# Which lines matched while connecting
ssh -v prod-api-3 2>&1 | grep -E 'Reading configuration|Applying options'
ssh -G prints the final values after all matching and token expansion. If user shows root when you expected deploy, an earlier block set it first. The -v output names the file and line of each block that matched, for example /home/you/.ssh/config line 24: Applying options for prod-*.
If ssh exits with Bad owner or permissions on /home/you/.ssh/config, the file is writable by someone else. chmod 600 ~/.ssh/config fixes it.
| Directive | Value we use | Common gotcha |
|---|---|---|
HostName | %h.internal.example.com | Plain Match host sees this expanded name, not the alias |
ProxyJump | bastion | Jump host needs its own block; conflicts with ProxyCommand |
IdentitiesOnly | yes | Without it, the agent can exhaust MaxAuthTries |
ServerAliveInterval | 30 | Default 0 sends no probes at all |
ControlPath | ~/.ssh/cm/%C | Directory must exist and be private |
ControlPersist | 10m | Stale masters after sleep: ssh -O exit |
Where DockLab fits
~/.ssh/config is still the right tool for scripts, rsync, git and anything else on the command line, and DockLab does not replace it. DockLab covers a different part of the work: interactive sessions across many servers. It puts the SSH terminal, SFTP browser and a Monaco-based remote editor in one window, with tabs and splits. Saved workspaces bring back the layout for a group of servers on the same device. They do not keep remote processes running, so use tmux for that. Credentials stay in a local AES-256-GCM encrypted vault, and SSH host key pinning warns you when a server's identity changes, which does the same job as StrictHostKeyChecking in the terminal.
If you only ever have one terminal open on one server, plain ssh with a good config is enough. If you often have a terminal, a file transfer and an editor open on several hosts at once, download DockLab and compare the plans on pricing. If you are also considering Termius, Warp or MobaXterm, our comparison page covers where each of them is stronger.
Frequently Asked Questions
Where is the ssh config file located?
The per-user file is ~/.ssh/config, and the system-wide file is /etc/ssh/ssh_config. The user file is read first, so for any option set in both, its values win. Neither file exists by default for users, so create it with touch ~/.ssh/config && chmod 600 ~/.ssh/config.
Does the order of Host blocks in ssh config matter?
Yes. For each directive, ssh keeps the first value it finds and ignores later ones, except IdentityFile and CertificateFile, which add to a list. Put specific hosts first, pattern groups next and Host * last.
Do scp, rsync and git use ~/.ssh/config?
Yes. All of them run the OpenSSH client underneath, so aliases, ports, keys and ProxyJump apply to scp prod-web-1:/etc/nginx/nginx.conf ., rsync -a ./site/ prod-web-1:/var/www/ and git clone github.com:org/repo.git. Tools that ship their own SSH library may not read the file.
How do I test an ssh config change without breaking my session?
Run ssh -G hostname to print the settings that would apply without connecting. Keep your current session open, and test the change from a new terminal with ssh -v hostname. If you use ControlMaster, run ssh -O exit hostname first, otherwise the new session reuses the old connection and skips authentication.
Is StrictHostKeyChecking no safe for a large fleet?
No. With no, ssh accepts both new and changed host keys, so it will not warn you about a possible man-in-the-middle. accept-new is the practical middle ground: new hosts are added automatically, and changed keys are still refused.
SSH, SFTP, a remote editor, a browser and AI in one desktop workspace.
DockLab runs on macOS, Windows and Linux. Start free and keep your whole server workflow in one window.