SSH config file example that scales

Most every SSH config file example you find online stops at two hosts, and this one scales to fifty. Every connection needs the right host, port, user, and sometimes a specific key, and there is no good place to write all that down outside of ~/.ssh/config. Without it you retype ssh deploy@10.0.4.17 -p 2222 -J bastion.example.com every session, forget which IP belongs to which server two weeks later, and end up with a shell history full of nearly identical commands.
Set up per-host blocks with their own keys, users, ports, and proxy jumps, and you scale from 5 servers to 50 without juggling key filenames. Instead of ssh -i ~/.ssh/id_ed25519_work -p 2222 deploy@192.168.1.50, you type ssh prod-web-01 and SSH handles the rest. scp, sftp, rsync, and git read the same file, so one alias fixes all four at once. Pair this with the SSH agent for passphrase caching, Match rules for patterns, and one key per security domain, and it feels even snappier from a low-latency terminal emulator
built for minimal input lag.
Key Takeaways
- One
~/.ssh/configfile turns long commands into short host aliases. scp,sftp,rsync, andgitall read the same file.IdentitiesOnly yesstops the “too many authentication failures” error.ssh -G hostnameshows which settings actually won.- Put specific hosts near the top and
Host *defaults at the bottom.
Where the SSH config file lives
Three files feed your connection, and they win in this order:
- Anything you pass on the command line with
-o - Your personal file at
~/.ssh/config - The system-wide file at
/etc/ssh/ssh_config
The personal file is the one you edit. It does not exist until you make it:
touch ~/.ssh/config
chmod 600 ~/.ssh/configOn Windows the path is C:\Users\<you>\.ssh\config. The built-in OpenSSH client reads it from PowerShell, CMD, and Git Bash alike, so you write the same blocks a Linux user writes. Watch out for editors that save as config.txt, and for a .ssh folder that does not exist yet.
On macOS the path matches Linux. Add UseKeychain yes to a host block and your passphrase gets stored in the login keychain. One warning about ssh-add -K on macOS: it means “store in keychain” there, while ssh-keygen -K means “download FIDO2 resident keys” from a hardware token. The letter is the same in both tools, but the jobs are opposite.
Why separate SSH keys beat one universal key
One SSH key for everything carries the same risk as one password for every account. When that key leaks from a dev laptop or a backup, every server, your GitHub account, and your homelab go down together. With one key per domain, a leak only hits the servers that trusted that key.
Audits and rotation both get easier. Each key serves a known domain (work, personal, client-A, CI/CD), so you can read authorized_keys on a server and tell at a glance which key belongs there. Swapping one universal key means touching every server you have ever logged into, while per-domain keys let you update one subset at a time.
Pick filenames that say what they do: id_ed25519_github, id_ed25519_work_prod, id_ed25519_homelab. Six months later when you clean up, you will still know what each one was for.
Generating and organizing your SSH keys
Ed25519 is the current standard for SSH keys. It makes smaller keys, signs faster, and has a cleaner crypto design than RSA. Make a new key like this:
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_github -C "github-personal-2026"The -C comment goes into the public key and helps you spot it later when you read authorized_keys on a server or run ssh-add -l.
When should you use RSA instead? Only for old systems that don’t speak Ed25519: some old RHEL 7 boxes, certain embedded devices, or aging network gear. In those cases, use 4096 bits at minimum:
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_legacy -C "legacy-device-2026"Always set a passphrase when ssh-keygen asks for one. A passphrase encrypts the private key at rest, so a stolen key file alone is useless. The SSH agent (covered below) caches the decrypted key in memory, so you’re not retyping the passphrase every few minutes.
For top-tier key safety, reach for FIDO2 hardware keys. With a YubiKey (firmware 5.2.3+) or other FIDO2 device and OpenSSH 8.2 or newer:
ssh-keygen -t ed25519-sk -O resident -O verify-required -f ~/.ssh/id_ed25519_sk_yubikeyThis makes a key tied to your hardware. The private key never leaves the token, so it can’t be copied or stolen by software. You’ll be prompted for your FIDO2 PIN and a physical touch each time the key is used. The -O resident flag stores the key handle on the device itself. You can then use it from any machine by running ssh-keygen -K to download the key refs.

Keep all keys in ~/.ssh/ and lock down the permissions, covered below under SSH config file permissions
. SSH refuses to use anything other users can read.
Audit your key list now and then with ls -la ~/.ssh/id_* and cross-check it against your config. Delete keys that no server still trusts.
SSH config file format and syntax
The file uses block syntax. Each Host entry sets options for one or more hosts. The format is simple, but it can do a lot.
The syntax rules in 60 seconds
Four rules cover the whole file:
- A block starts with
Hostand runs until the nextHostorMatchline. - Indentation is cosmetic. Four spaces reads well, zero spaces works the same.
#starts a comment. It must sit at the start of a line, not trailing after a value.- Keywords are case-insensitive, but arguments like filenames are not.
Patterns take three operators. * matches any run of characters, ? matches exactly one, and ! negates. So Host 192.168.1.? matches .1 through .9 but not .10, and Host *.example.com !secret.example.com matches the whole domain except that one box.
Basic host blocks
A typical entry looks like this:
Host prod-web-01
HostName 192.168.1.50
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519_work_prod
IdentitiesOnly yesNow ssh prod-web-01 connects to 192.168.1.50 on port 2222 as user deploy with the named key.
The IdentitiesOnly yes line is the one to keep. IdentityFile on its own only adds a key to the list of candidates, and the agent still offers everything it holds, in its own order. The server counts each offer as a failed attempt and cuts you off at MaxAuthTries, which defaults to 6 in sshd_config. Hold seven keys and the right one may never get its turn:
debug1: Offering public key: /home/you/.ssh/id_ed25519_github
debug1: Offering public key: /home/you/.ssh/id_ed25519_homelab
...
Received disconnect from 192.168.1.50 port 2222:2: Too many authentication failuresIdentitiesOnly yes tells SSH to offer only the key you named, so there is one attempt and one match. This is why config examples that work fine with a single key break the day you add a second one.
A complete SSH config file example
This file covers personal, work, bastion, and homelab domains, and the rest of the section breaks down each piece:
# ~/.ssh/config
Include ~/.ssh/config.d/*
# --- Personal GitHub ---
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_github
IdentitiesOnly yes
# --- Work Production ---
Host prod-*
User deploy
IdentityFile ~/.ssh/id_ed25519_work_prod
IdentitiesOnly yes
ServerAliveInterval 60
Host prod-web-01
HostName 192.168.1.50
Port 2222
Host prod-web-02
HostName 192.168.1.51
Port 2222
# --- Work via Bastion ---
Host work-bastion
HostName bastion.company.com
User admin
IdentityFile ~/.ssh/id_ed25519_work_bastion
IdentitiesOnly yes
Host work-internal-*
ProxyJump work-bastion
User engineer
IdentityFile ~/.ssh/id_ed25519_work_internal
IdentitiesOnly yes
Host work-internal-db
HostName 10.0.1.20
Host work-internal-app
HostName 10.0.1.30
# --- Homelab ---
Host lab-*
User pi
IdentityFile ~/.ssh/id_ed25519_homelab
IdentitiesOnly yes
Host lab-nas
HostName 192.168.50.10
Host lab-pve
HostName 192.168.50.2
# --- Defaults ---
Host *
AddKeysToAgent yes
ServerAliveInterval 60
ServerAliveCountMax 3
HashKnownHosts yes
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h:%p
ControlPersist 10mCreate the socket directory once with mkdir -p ~/.ssh/sockets, or the last three lines fail silently. The same one-key-per-domain rule fits whether you push to GitHub or run your own self-hosted Git server.
Wildcard patterns
For groups of related hosts, wildcards save repetition:
Host prod-*
User deploy
IdentityFile ~/.ssh/id_ed25519_work_prod
IdentitiesOnly yes
ServerAliveInterval 60
Host prod-web-01
HostName 192.168.1.50
Port 2222
Host prod-web-02
HostName 192.168.1.51
Port 2222
Host prod-db-01
HostName 192.168.1.60The Host prod-* block sets shared options for every host that matches that pattern. The single-host blocks below it add or change specific settings. SSH walks matching blocks top to bottom and uses the first value it finds for each option. So put your most specific blocks first and wildcards later. Or flip it: put wildcards first as defaults, then let single blocks override them.
Proxy jump for bastion hosts
If you access internal servers through a jump host, ProxyJump handles the tunneling:
Host bastion
HostName bastion.example.com
User admin
IdentityFile ~/.ssh/id_ed25519_work_bastion
IdentitiesOnly yes
Host internal-db
HostName 10.0.1.20
User dbadmin
ProxyJump bastion
IdentityFile ~/.ssh/id_ed25519_work_internal
IdentitiesOnly yesRunning ssh internal-db automatically tunnels through the bastion. For deeply nested networks, chain multiple jumps: ProxyJump bastion1,bastion2.
Note: OpenSSH 10.3 (released April 2, 2026) patched a shell injection flaw in the -J (ProxyJump) option where user and host names were not properly checked. Make sure your OpenSSH build is current.
The Include directive
As your config grows, split it into separate files:
# ~/.ssh/config
Include ~/.ssh/config.d/*
Host *
AddKeysToAgent yes
ServerAliveInterval 60
ServerAliveCountMax 3
HashKnownHosts yesThen organize by context:
~/.ssh/config.d/work
~/.ssh/config.d/homelab
~/.ssh/config.d/clientsEach file holds only the hosts for that one domain. So you can share your homelab config with a friend or shelf old client configs when a project ends.
Two GitHub accounts on one machine
GitHub ties a key to exactly one account, so a work account and a personal account need two keys and two aliases. HostName stays the real domain while Host becomes the label you type:
Host github-personal
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_github
IdentitiesOnly yes
Host github-work
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_work_github
IdentitiesOnly yesClone through the alias, not the domain: git clone git@github-work:company/repo.git. For a repo you already cloned, repoint it with git remote set-url origin git@github-work:company/repo.git.
The other half of the job is the commit author. The SSH alias picks the key, but git still stamps commits with whatever user.email is set globally, so work commits land under your personal address. Fix it by directory in ~/.gitconfig:
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-workThen put [user] email = you@company.com in ~/.gitconfig-work. Every repo under ~/work/ gets the work identity automatically.
Match directives for conditional config
The Match directive applies settings only when specific conditions are met:
Match host *.internal exec "nmcli -t -f NAME c show --active | grep -q corporate-vpn"
ProxyJump office-gatewayThis applies proxy settings only when you’re on the corporate VPN. The exec keyword runs a command and uses the block only when it returns success (exit code 0). Handy for laptops that hop between home, office, and coffee shop Wi-Fi.
Password logins in the config
The config file cannot hold a password. There is no Password directive, by design. A guide that shows one is describing some other tool.
What you can control is which method SSH tries. On a box that only accepts a password, skip the key round trip:
Host old-router
HostName 192.168.1.1
User admin
PreferredAuthentications password
PubkeyAuthentication noFlip it the other way on servers where you never want a password prompt to appear:
Host prod-*
PasswordAuthentication noFor unattended logins, use a key with an agent. sshpass exists but puts the password in your shell history and process list.
Global defaults
Put sensible defaults in a Host * block at the end of your config:
Host *
AddKeysToAgent yes
ServerAliveInterval 60
ServerAliveCountMax 3
Compression yes
HashKnownHosts yes
StrictHostKeyChecking accept-new
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h:%p
ControlPersist 10m| Option | What it does |
|---|---|
AddKeysToAgent yes | Automatically adds keys to the agent on first use |
ServerAliveInterval 60 | Sends keepalive every 60s to prevent idle disconnects |
ServerAliveCountMax 3 | Drops connection after 3 missed keepalives |
Compression yes | Compresses traffic - useful for slow links |
HashKnownHosts yes | Hashes hostnames in known_hosts for privacy |
StrictHostKeyChecking accept-new | Auto-accepts new hosts, rejects changed keys |
ControlMaster auto | Reuses one connection for every later session to that host |
ControlPath ~/.ssh/sockets/%r@%h:%p | Where the shared socket file lives |
ControlPersist 10m | Keeps the shared connection alive 10 minutes after you log out |
Connection multiplexing and what breaks
Those last three lines are the biggest speed win in the file. The first ssh to a host does the full handshake. Every later session, scp, or git push to the same host rides the socket that is already open, so it connects instantly instead of redoing key exchange and auth.
Two things go wrong with it:
- Stale sockets. Switch networks or suspend your laptop and the socket survives while the TCP connection behind it dies, so new sessions hang. Kill it with
ssh -O exit prod-web-01, or check first withssh -O check prod-web-01. - Path length. A Unix socket path caps at roughly 100 characters. Long usernames and hostnames blow past it and you get “unix_listener: too long for Unix domain socket”. Swap
%r@%h:%pfor the hashed form%C, which is always a fixed-length string.
SSH agent management and passphrase caching
The SSH agent is a daemon that holds decrypted private keys in memory. You type your passphrase once per session. The agent then handles auth for later connections without asking again. SSH clients reach the agent through the SSH_AUTH_SOCK Unix socket.
Whether the agent auto-starts depends on your setup. Most desktops (GNOME, KDE, sway) start one at login. For headless servers, tmux sessions, or minimal setups, add this to ~/.bash_profile:
eval $(ssh-agent -s)Or set up a systemd user service for ssh-agent that starts with your session and stays up across terminal restarts.
With AddKeysToAgent yes in your config (recommended), keys load into the agent on first use. No manual ssh-add needed. For extra safety, use AddKeysToAgent confirm to get a prompt before each use of a key from the agent.
You can also limit how long a key stays in the agent. Load it with a timeout:
ssh-add -t 3600 ~/.ssh/id_ed25519_work_prodThis keeps the key in the agent for one hour, then drops it. Handy for prod keys where you want to balance ease of use against the risk of an agent that runs all day with powerful keys loaded.
Agent forwarding vs. ProxyJump
Agent forwarding (ForwardAgent yes) lets a remote server use your local agent for further SSH connections. For example, you might run git pull on a server using your local GitHub key. It sounds handy, but it carries a real risk. A hacked server with agent forwarding on can use your keys for anything, against any host, for as long as you’re logged in.
Use ProxyJump over agent forwarding whenever you can. ProxyJump keeps the TCP connection on your local machine and tunnels through the bastion without exposing your agent socket on the remote host. If you do need agent forwarding, turn it on per host, never globally, and never through hosts you don’t trust.
| Method | Keys exposed on remote? | Use when |
|---|---|---|
ProxyJump | No | Accessing internal hosts through a bastion |
ForwardAgent yes | Yes (to that host) | Running git/ssh commands on a trusted remote host |
ForwardAgent no (default) | No | Default - keep it this way unless needed |
For always-on remote access without running a bastion, a self-hosted WireGuard VPN can replace jump hosts and keep your SSH keys on your machine.
Password manager SSH agent integration
If you use a password manager, it might replace the system SSH agent. 1Password
has a built-in SSH agent that stores keys in your vault. Private keys never touch the filesystem. Turn it on in 1Password Settings > Developer > Set Up SSH Agent, then point SSH_AUTH_SOCK at the 1Password agent socket.

KeePassXC takes a different tack. It hooks into the system SSH agent. When the database is unlocked, it adds your keys. When it locks, it pulls them out. Turn it on under Tools > Settings > SSH Agent. You attach your private key file to a KeePassXC entry. The passphrase stored there decrypts and loads the key.
Both options cut the number of raw private keys sitting on disk. That’s a real safety gain.
Troubleshooting, security auditing, and maintenance
SSH key management breaks in predictable ways. Here are the most common issues and how to fix them.
Debugging connections
When something isn’t working, verbose output tells you exactly what’s happening:
ssh -vvv prod-web-01Look for these lines in the output:
Offering public keyshows which keys SSH is trying, and in what orderServer accepts keyshows which key actually workedToo many authentication failuresmeans the agent gave too many wrong keys before the right onebad permissionsmeans perms on keys or config are too open
Why is my config not applying?
When a host block seems ignored, ask SSH what it decided:
ssh -G prod-web-01-G prints every setting SSH resolved for that alias and then exits without connecting. You see the final identityfile, user, port, and proxyjump after all your blocks, wildcards, and Include files have been merged. If the value there is not what you wrote, an earlier block won.
That is almost always the cause. SSH takes the first value it finds for each option, so a Host * block sitting at the top of the file locks in defaults that nothing below can override. Move it to the bottom and the specific blocks win again.
SSH config file permissions
SSH refuses to use keys or a config file that other users can read. Set the modes once:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_*
chmod 644 ~/.ssh/*.pub
chmod 600 ~/.ssh/configThe directory needs 700 and the private keys need 600. Public keys at 644 are fine, they are public. When perms are wrong, ssh -vvv says “bad permissions” outright, so check this before anything else.
Common fixes
If you see “too many authentication failures,” add IdentitiesOnly yes to that host’s block. SSH then offers only the key named by IdentityFile instead of every key in the agent.
Stale host keys are another common snag. When a server is rebuilt, its host key changes. SSH then warns you about a possible man-in-the-middle attack. If you know the change is fine:
ssh-keygen -R hostnameUsing StrictHostKeyChecking accept-new in your config auto-accepts host keys for new servers, but still rejects changed keys for known hosts. A good middle ground between safety and ease of use.
Security auditing
Periodically check what’s loaded and what’s on disk:
# List all keys currently in the agent
ssh-add -l
# List fingerprints of all local keys
for f in ~/.ssh/id_*; do ssh-keygen -lf "$f"; doneCross-check these fingerprints against authorized_keys on your servers. Any key that doesn’t fit a current use case should come off both the server and your local box. SSH key hygiene is one item in a wider routine to lock down an internet-facing host
worth running on any box that faces the public network.
Key rotation workflow
When it’s time to rotate a key:
- Make a new key with
ssh-keygen - Push the public key to each server via
ssh-copy-idor a config tool like Ansible , Puppet, or Chef. Automating with Ansible and dotfiles makes this step repeatable across machines - Test access with the new key
- Remove the old key from
authorized_keyson each server - Delete the old private key on your local box
For large fleets with dozens or hundreds of servers, look at SSH certificates instead of raw public keys. SSH certificates use a central Certificate Authority that signs short-lived keys. That cuts the need to ship public keys to every server. Meta, Uber, and Google all use SSH certs at scale. Tools like Smallstep and Infisical make this work for smaller teams too.
Quick reference
| Task | Command |
|---|---|
| Generate Ed25519 key | ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_name -C "comment" |
| Generate FIDO2 key | ssh-keygen -t ed25519-sk -O resident -O verify-required -f ~/.ssh/id_ed25519_sk |
| List agent keys | ssh-add -l |
| Add key with timeout | ssh-add -t 3600 ~/.ssh/id_ed25519_name |
| Remove stale host key | ssh-keygen -R hostname |
| Debug connection | ssh -vvv hostname |
| Show resolved config | ssh -G hostname |
| Close a stuck shared connection | ssh -O exit hostname |
| Fix permissions | chmod 700 ~/.ssh && chmod 600 ~/.ssh/id_* && chmod 600 ~/.ssh/config |
| Copy key to server | ssh-copy-id -i ~/.ssh/id_ed25519_name.pub user@host |
SSH config file questions
Where is my SSH config file?
Your personal file is ~/.ssh/config on Linux and macOS, and C:\Users\<you>\.ssh\config on Windows. The system-wide file is /etc/ssh/ssh_config. Neither personal file exists until you create it.
How to get into SSH config file?
Open it in any text editor: nano ~/.ssh/config works everywhere. If the file or the .ssh directory is missing, run mkdir -p ~/.ssh && touch ~/.ssh/config && chmod 600 ~/.ssh/config first. On Windows, make sure your editor does not append .txt to the filename.
What does the “ssh config” file do?
It stores per-host connection settings so you type ssh prod-web-01 instead of the full hostname, port, user, and key path. scp, sftp, rsync, and git read the same file, so one entry covers all of them.
How to write a config file?
Start a block with Host and an alias, then indent the settings under it. Each block runs until the next Host or Match line. SSH keeps the first value it finds for each setting, so put specific hosts near the top and any Host * defaults at the bottom.
Setting all this up takes maybe 30 minutes. After that, you ssh alias-name to any host, the right key gets used every time, and you stop locking yourself out. The safety side is just as real. When you don’t share one key across every box you own, a stolen laptop doesn’t mean changing passwords on 40 servers at 2am.
Botmonster Tech