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/config file turns long commands into short host aliases.
  • scp, sftp, rsync, and git all read the same file.
  • IdentitiesOnly yes stops the “too many authentication failures” error.
  • ssh -G hostname shows 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:

  1. Anything you pass on the command line with -o
  2. Your personal file at ~/.ssh/config
  3. 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/config

On 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_yubikey

This 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.

YubiKey 5 NFC hardware security key
A YubiKey 5 NFC - one of the most common FIDO2 devices for SSH key authentication
Image: Wikimedia Commons , CC-BY-SA 4.0

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 Host and runs until the next Host or Match line.
  • 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 yes

Now 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 failures

IdentitiesOnly 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 10m

Create 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.60

The 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.

Diagram showing how SSH resolves connection parameters by matching host alias against config blocks top-to-bottom and merging results

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 yes

Running 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 yes

Then organize by context:

~/.ssh/config.d/work
~/.ssh/config.d/homelab
~/.ssh/config.d/clients

Each 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 yes

Clone 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-work

Then 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-gateway

This 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 no

Flip it the other way on servers where you never want a password prompt to appear:

Host prod-*
    PasswordAuthentication no

For 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
OptionWhat it does
AddKeysToAgent yesAutomatically adds keys to the agent on first use
ServerAliveInterval 60Sends keepalive every 60s to prevent idle disconnects
ServerAliveCountMax 3Drops connection after 3 missed keepalives
Compression yesCompresses traffic - useful for slow links
HashKnownHosts yesHashes hostnames in known_hosts for privacy
StrictHostKeyChecking accept-newAuto-accepts new hosts, rejects changed keys
ControlMaster autoReuses one connection for every later session to that host
ControlPath ~/.ssh/sockets/%r@%h:%pWhere the shared socket file lives
ControlPersist 10mKeeps 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 with ssh -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:%p for 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_prod

This 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.

MethodKeys exposed on remote?Use when
ProxyJumpNoAccessing internal hosts through a bastion
ForwardAgent yesYes (to that host)Running git/ssh commands on a trusted remote host
ForwardAgent no (default)NoDefault - 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.

1Password developer settings showing SSH agent configuration
1Password's developer settings for enabling the built-in SSH agent
Image: 1Password Developer Docs

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-01

Look for these lines in the output:

  • Offering public key shows which keys SSH is trying, and in what order
  • Server accepts key shows which key actually worked
  • Too many authentication failures means the agent gave too many wrong keys before the right one
  • bad permissions means 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/config

The 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 hostname

Using 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"; done

Cross-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:

  1. Make a new key with ssh-keygen
  2. Push the public key to each server via ssh-copy-id or a config tool like Ansible , Puppet, or Chef. Automating with Ansible and dotfiles makes this step repeatable across machines
  3. Test access with the new key
  4. Remove the old key from authorized_keys on each server
  5. 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

TaskCommand
Generate Ed25519 keyssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_name -C "comment"
Generate FIDO2 keyssh-keygen -t ed25519-sk -O resident -O verify-required -f ~/.ssh/id_ed25519_sk
List agent keysssh-add -l
Add key with timeoutssh-add -t 3600 ~/.ssh/id_ed25519_name
Remove stale host keyssh-keygen -R hostname
Debug connectionssh -vvv hostname
Show resolved configssh -G hostname
Close a stuck shared connectionssh -O exit hostname
Fix permissionschmod 700 ~/.ssh && chmod 600 ~/.ssh/id_* && chmod 600 ~/.ssh/config
Copy key to serverssh-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.