SSH Config Mastery: One File to Rule Them All

  1. SSH Config Mastery: One File to Rule Them All Or: How I Learned to Stop Typing IP Addresses and Love the Config File

You’re sitting at your terminal, about to SSH into a server. You type:

ssh -i ~/.ssh/my_special_key_for_production_server_3.pem -p 2222 admin@192.168.47.123

And you think: “There has to be a better way.”

Spoiler alert: There is. It’s called ~/.ssh/config, and it’s about to change your life. Or at least your daily workflow. Which, let’s be honest, is basically the same thing for most of us.


What Is SSH Config?

The phone book for servers you actually want to call

The SSH config file is a plain text file that stores connection settings for your SSH sessions. Instead of remembering IP addresses, ports, usernames, and key files, you define them once and reference them by a friendly name.

Before:

ssh -i ~/.ssh/production_key.pem -p 2222 -o StrictHostKeyChecking=no admin@10.0.0.42

After:

ssh production

That’s it. That’s the tweet.


Where Does It Live?

OS Config Location Default Permissions
macOS ~/.ssh/config 600 (rw——-)
Linux ~/.ssh/config 600 (rw——-)
Windows 11 C:\Users\[username]\.ssh\config User read/write

Pro tip: If the file doesn’t exist, create it. SSH won’t complain – it’ll just work without custom settings like a sad, unconfigured robot.


Setting Up SSH Config on macOS

The land of “it just works” (most of the time)

Step 1: Create or Edit the Config File

mkdir -p ~/.ssh
chmod 700 ~/.ssh
touch ~/.ssh/config
chmod 600 ~/.ssh/config

Step 2: Add Your First Host Entry

Open the file with your favorite editor:

nano ~/.ssh/config

Add a host entry:

Host devserver
    HostName 192.168.1.100
    User developer
    Port 22
    IdentityFile ~/.ssh/dev_key

Step 3: Connect Like a Boss

ssh devserver

That’s it. No more typing IP addresses like it’s 1995.


Setting Up SSH Config on Linux

Same procedure as every year, James

Good news: It’s identical to macOS. Linux and macOS both use OpenSSH, so the config format is the same.

mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/config
chmod 600 ~/.ssh/config

The only Linux-specific consideration is SELinux (if you’re on RHEL/CentOS/Fedora):

1. If SELinux is being difficult
restorecon -Rv ~/.ssh

Setting Up SSH Config on Windows 11

Yes, Windows has native SSH now. I know, I was surprised too.

Windows 11 (and Windows 10 1803+) includes OpenSSH as a built-in feature. No more PuTTY! Unless you want PuTTY. Nobody’s judging.

Step 1: Verify OpenSSH Is Installed

Open PowerShell as Administrator:

Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Client*'

If it shows State: NotPresent, install it:

Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0

Step 2: Create the Config File

1. Create .ssh directory if it doesn't exist
New-Item -ItemType Directory -Path "$env:USERPROFILE\.ssh" -Force

1. Create config file
New-Item -ItemType File -Path "$env:USERPROFILE\.ssh\config" -Force

1. Open in Notepad
notepad "$env:USERPROFILE\.ssh\config"

Step 3: Add Host Entries

Same format as macOS/Linux:

Host devserver
    HostName 192.168.1.100
    User developer
    Port 22
    IdentityFile C:\Users\YourName\.ssh\dev_key

Windows-specific gotcha: Use forward slashes OR escaped backslashes in paths:

  • C:/Users/YourName/.ssh/key
  • C:\\Users\\YourName\\.ssh\\key
  • C:\Users\YourName\.ssh\key (single backslashes = escape sequences = pain)

SSH Config Syntax Deep Dive

Time to learn the ancient runes

Basic Structure

Host [alias]
    Option1 value1
    Option2 value2

Indentation is optional but highly recommended for your sanity.

Essential Options

Option Description Example
Host Alias name for this connection Host production
HostName Actual IP or FQDN HostName 10.0.0.42
User Login username User admin
Port SSH port (default: 22) Port 2222
IdentityFile Path to private key IdentityFile ~/.ssh/my_key
IdentitiesOnly Only use specified key IdentitiesOnly yes
ForwardAgent Forward SSH agent ForwardAgent yes
ProxyJump Jump through bastion host ProxyJump bastion
LocalForward Local port forwarding LocalForward 8080 localhost:80
RemoteForward Remote port forwarding RemoteForward 9090 localhost:3000
ServerAliveInterval Keep connection alive ServerAliveInterval 60
ServerAliveCountMax Max keepalive attempts ServerAliveCountMax 3
StrictHostKeyChecking Host key verification StrictHostKeyChecking accept-new
UserKnownHostsFile Known hosts file location UserKnownHostsFile ~/.ssh/known_hosts
Compression Enable compression Compression yes
AddKeysToAgent Auto-add keys to agent AddKeysToAgent yes

Real-World Config Examples

Because examples are worth a thousand man pages

Example 1: Simple Server Entry

Host webserver
    HostName 192.168.1.50
    User deploy
    IdentityFile ~/.ssh/webserver_key

Usage: ssh webserver

Example 2: Jump Host / Bastion Setup

1. The bastion (jump host)
Host bastion
    HostName bastion.company.com
    User admin
    IdentityFile ~/.ssh/bastion_key

1. Internal server accessible only through bastion
Host internal-db
    HostName 10.0.0.50
    User dbadmin
    ProxyJump bastion
    IdentityFile ~/.ssh/internal_key

Usage: ssh internal-db — SSH automatically jumps through bastion first!

Example 3: Multiple Servers with Wildcards

1. Settings for all production servers
Host prod-*
    User deploy
    IdentityFile ~/.ssh/production_key
    StrictHostKeyChecking yes

Host prod-web
    HostName 10.0.1.10

Host prod-api
    HostName 10.0.1.11

Host prod-db
    HostName 10.0.1.12
    Port 2222

Example 4: GitHub/GitLab with Specific Keys

Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/github_key
    IdentitiesOnly yes

Host gitlab.com
    HostName gitlab.com
    User git
    IdentityFile ~/.ssh/gitlab_key
    IdentitiesOnly yes

Now git clone git@github.com:user/repo.git uses the correct key automatically.

Example 5: Keep Connections Alive

Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
    AddKeysToAgent yes

This applies to ALL hosts and prevents those annoying “Connection reset by peer” messages.

Example 6: Local Port Forwarding (Access Remote Service Locally)

Host db-tunnel
    HostName database.internal
    User admin
    LocalForward 5432 localhost:5432
    IdentityFile ~/.ssh/db_key

Usage: ssh -N db-tunnel — Now connect to localhost:5432 to reach the remote database!

Example 7: Complete Dev Environment Entry

Host homelab
    HostName 192.168.50.10
    User admin
    Port 22
    IdentityFile ~/.ssh/homelab_key
    ServerAliveInterval 60
    ServerAliveCountMax 3

Usage: ssh homelab instead of ssh -i ~/.ssh/homelab_key admin@192.168.50.10


Security Best Practices

Because convenience shouldn’t mean compromised

1. Lock Down File Permissions

chmod 700 ~/.ssh
chmod 600 ~/.ssh/config
chmod 600 ~/.ssh/id_*        # Private keys
chmod 644 ~/.ssh/id_*.pub    # Public keys

SSH will refuse to use keys with overly permissive permissions. It’s not being difficult—it’s saving you from yourself.

2. Use IdentitiesOnly yes

Without this, SSH tries ALL your keys against every server:

Host secure-server
    HostName 10.0.0.1
    User admin
    IdentityFile ~/.ssh/specific_key
    IdentitiesOnly yes    # Only try this specific key

This prevents accidentally revealing which keys you have and speeds up connections.

3. Set Default Security Options

Host *
    HashKnownHosts yes           # Hash hostnames in known_hosts
    PasswordAuthentication no    # Force key-based auth
    StrictHostKeyChecking ask    # Verify host keys

4. Separate Keys for Different Purposes

Don’t use one key for everything. Organize by environment/purpose:

~/.ssh/
├── personal_github
├── personal_github.pub
├── work_production
├── work_production.pub
├── work_staging
├── work_staging.pub
└── homelab
└── homelab.pub

If one key is compromised, you only need to rotate that one.


Troubleshooting Common Issues

When SSH says “no” and you don’t know why

Problem: “Permission denied (publickey)”

Cause: Key not found, wrong permissions, or not offered.

Fix:

1. Verbose mode shows what's happening
ssh -vvv hostname

1. Check permissions
ls -la ~/.ssh/

1. Verify key is being offered
ssh-add -l

Problem: “Bad owner or permissions”

Cause: File permissions too open.

Fix:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/config
chmod 600 ~/.ssh/private_key

Problem: Config file seems to be ignored

Cause: Syntax error or wrong file location.

Fix:

1. Validate config syntax
ssh -G hostname

1. Check which config file is used
ssh -vvv hostname 2>&1 | grep "Reading configuration"

Problem: “Too many authentication failures”

Cause: SSH trying too many keys before finding the right one.

Fix:

Host problem-server
    IdentityFile ~/.ssh/correct_key
    IdentitiesOnly yes

Problem: Connection drops after inactivity

Fix:

Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3

Pro Tips

The good stuff

Tip 1: Use Include for Organization

Split your config into multiple files:

1. ~/.ssh/config
Include config.d/*

Host *
    ServerAliveInterval 60
1. ~/.ssh/config.d/work
Host work-*
    User corporate-username
    IdentityFile ~/.ssh/work_key
1. ~/.ssh/config.d/homelab
Host lab-*
    User admin
    IdentityFile ~/.ssh/homelab_key

Tip 2: Quick Tunnels with Aliases

Host tunnel-grafana
    HostName monitoring.internal
    LocalForward 3000 localhost:3000
    RequestTTY no

Host tunnel-postgres
    HostName db.internal
    LocalForward 5432 localhost:5432
    RequestTTY no

Usage: ssh -fN tunnel-grafana — Open localhost:3000 in browser!

Tip 3: Match Blocks for Conditional Config

1. Different settings when on VPN
Match host *.internal exec "ping -c1 -W1 vpn-gateway"
    ProxyJump none

Match host *.internal
    ProxyJump bastion

Tip 4: Use ControlMaster for Connection Reuse

Host *
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 600

Create the socket directory:

mkdir -p ~/.ssh/sockets
chmod 700 ~/.ssh/sockets

Now multiple SSH sessions to the same host share one connection. Much faster!


Quick Reference Card

╔══════════════════════════════════════════════════════════════════╗
║                    SSH CONFIG CHEAT SHEET                        ║
╠══════════════════════════════════════════════════════════════════╣
║ FILE LOCATIONS                                                   ║
║   macOS/Linux:  ~/.ssh/config                                    ║
║   Windows 11:   C:\Users\[name]\.ssh\config                      ║
╠══════════════════════════════════════════════════════════════════╣
║ BASIC TEMPLATE                                                   ║
║   Host myserver                                                  ║
║       HostName 192.168.1.100                                     ║
║       User admin                                                 ║
║       Port 22                                                    ║
║       IdentityFile ~/.ssh/my_key                                 ║
╠══════════════════════════════════════════════════════════════════╣
║ JUMP HOST / BASTION                                              ║
║   Host internal                                                  ║
║       HostName 10.0.0.50                                         ║
║       ProxyJump bastion                                          ║
╠══════════════════════════════════════════════════════════════════╣
║ PORT FORWARDING                                                  ║
║   LocalForward  8080 localhost:80    (remote->local)             ║
║   RemoteForward 9090 localhost:3000  (local->remote)             ║
╠══════════════════════════════════════════════════════════════════╣
║ PERMISSIONS (REQUIRED!)                                          ║
║   ~/.ssh/           700 (drwx------)                             ║
║   ~/.ssh/config     600 (-rw-------)                             ║
║   Private keys      600 (-rw-------)                             ║
║   Public keys       644 (-rw-r--r--)                             ║
╠══════════════════════════════════════════════════════════════════╣
║ DEBUGGING                                                        ║
║   ssh -vvv hostname        Verbose output                        ║
║   ssh -G hostname          Show resolved config                  ║
║   ssh-add -l               List loaded keys                      ║
╚══════════════════════════════════════════════════════════════════╝

Appendix: Option Reference Table

Option Risk Level Description When to Use
Host 🟢 Safe Connection alias Always
HostName 🟢 Safe Actual server address Always
User 🟢 Safe Login username When not default
Port 🟢 Safe SSH port Non-standard ports
IdentityFile 🟢 Safe Private key path Key-based auth
IdentitiesOnly 🟢 Safe Only use specified key Recommended always
ProxyJump 🟢 Safe Bastion/jump host Private networks
LocalForward 🟡 Caution Local port forward Tunnel to remote
RemoteForward 🟡 Caution Remote port forward Expose local service
ForwardAgent 🟠 Risk Forward SSH agent Only when needed
StrictHostKeyChecking no 🔴 Danger Disable host verification Never in production
PasswordAuthentication yes 🔴 Danger Allow password auth Avoid if possible

Risk Level Legend:

  • 🟢 Safe: No security concerns
  • 🟡 Caution: Understand before using
  • 🟠 Risk: Can expose credentials if misused
  • 🔴 Danger: Security risk, avoid if possible

Conclusion: Your SSH Life, Simplified

You now know how to:

  1. Create and configure ~/.ssh/config on macOS, Linux, and Windows 11
  2. Define host aliases so you never type IP addresses again
  3. Set up jump hosts for accessing internal networks
  4. Configure port forwarding for tunnels and development
  5. Apply security best practices to keep your keys safe
  6. Troubleshoot common issues when things go wrong

The best part? This file follows you everywhere. Sync it across machines (minus the private keys!), and you’ll always have your server shortcuts ready.


Fun fact: The average DevOps engineer types SSH commands approximately 47,000 times per year. At 20 characters saved per command, that’s nearly a million characters – or about one medium-sized novel. You’re welcome.


Stay connected, stay curious 🔐

Leave a Reply

Your email address will not be published. Required fields are marked *