If you’re still typing ssh -i ~/.ssh/my-key.pem -p 2222 admin@192.168.1.50 every time you connect, you’re doing it wrong. The SSH config file is one of the most underutilized productivity tools in a developer’s arsenal.

The Basics: ~/.ssh/config

Create or edit ~/.ssh/config:

1
2
3
4
5
Host dev
    HostName dev.example.com
    User deploy
    IdentityFile ~/.ssh/deploy_key
    Port 22

Now you just type ssh dev. That’s it.

Host Patterns

Wildcards let you apply settings to multiple hosts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# All production servers
Host prod-*
    User deploy
    IdentityFile ~/.ssh/prod_key
    StrictHostKeyChecking yes

# Specific production hosts inherit above
Host prod-web
    HostName 10.0.1.10

Host prod-db
    HostName 10.0.1.20

Jump Hosts (ProxyJump)

Access internal servers through a bastion:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Bastion/jump host
Host bastion
    HostName bastion.example.com
    User jump
    IdentityFile ~/.ssh/bastion_key

# Internal server via bastion
Host internal-db
    HostName 10.0.0.50
    User admin
    ProxyJump bastion

Now ssh internal-db automatically tunnels through the bastion. No manual hopping.

For older SSH versions, use ProxyCommand:

1
2
3
4
Host internal-db
    HostName 10.0.0.50
    User admin
    ProxyCommand ssh -W %h:%p bastion

Connection Multiplexing

Reuse connections to avoid repeated authentication:

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

Create the socket directory:

1
mkdir -p ~/.ssh/sockets

First connection authenticates. Subsequent connections to the same host are instant—they reuse the existing socket. The connection persists for 10 minutes (600 seconds) after the last session closes.

Keep Connections Alive

Prevent timeout disconnects:

1
2
3
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3

Sends a keepalive packet every 60 seconds, gives up after 3 missed responses.

Per-Host Settings

Different environments, different rules:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Development - fast and loose
Host dev-*
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null
    LogLevel ERROR

# Production - locked down
Host prod-*
    StrictHostKeyChecking yes
    PasswordAuthentication no
    PubkeyAuthentication yes
    IdentitiesOnly yes

The IdentitiesOnly yes prevents SSH from trying every key in your agent—it uses only the specified IdentityFile.

Dynamic Port Forwarding (SOCKS Proxy)

Route browser traffic through a remote host:

1
2
3
4
Host proxy
    HostName secure.example.com
    User tunnel
    DynamicForward 1080

Connect with ssh -N proxy, then configure your browser to use SOCKS5 proxy at localhost:1080.

Local Port Forwarding

Access remote services locally:

1
2
3
4
5
Host db-tunnel
    HostName bastion.example.com
    User admin
    LocalForward 5432 internal-db:5432
    LocalForward 6379 internal-redis:6379

After connecting, localhost:5432 reaches the internal database.

Remote Port Forwarding

Expose local services to remote networks:

1
2
3
4
Host expose-local
    HostName remote.example.com
    User deploy
    RemoteForward 8080 localhost:3000

Your local port 3000 becomes accessible as port 8080 on the remote host.

Agent Forwarding (Use Carefully)

Forward your SSH agent to use local keys on remote hosts:

1
2
3
Host trusted-jump
    HostName jump.example.com
    ForwardAgent yes

Security warning: Only enable this for trusted hosts. A compromised remote host could use your forwarded agent to access other systems.

Safer alternative—use ProxyJump instead of agent forwarding when possible.

Include Directive

Split configs across files:

1
2
3
4
5
6
# ~/.ssh/config
Include config.d/*

Host *
    AddKeysToAgent yes
    IdentitiesOnly yes
1
2
3
4
# ~/.ssh/config.d/work
Host work-*
    User corp_username
    IdentityFile ~/.ssh/work_key
1
2
3
4
# ~/.ssh/config.d/personal
Host home-*
    User pi
    IdentityFile ~/.ssh/personal_key

Match Blocks

Conditional configuration based on various criteria:

1
2
3
4
5
6
7
# Use different key when on corporate network
Match Host *.corp.example.com exec "ip route | grep -q 10.0.0.0/8"
    IdentityFile ~/.ssh/corp_key

# Use proxy for all connections when on public wifi
Match exec "nmcli -t -f NAME connection show --active | grep -q 'Public WiFi'"
    ProxyCommand nc -X 5 -x localhost:1080 %h %p

Security Hardening

Global defaults for all connections:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Host *
    # Prefer strong ciphers
    Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
    
    # Strong key exchange
    KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org
    
    # Strong MACs
    MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
    
    # Verify host keys
    StrictHostKeyChecking ask
    
    # Don't hash known_hosts (makes debugging easier)
    HashKnownHosts no
    
    # Visual host key verification
    VisualHostKey yes

Practical Example: Complete Config

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# ~/.ssh/config

# Global defaults
Host *
    AddKeysToAgent yes
    IdentitiesOnly yes
    ServerAliveInterval 60
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 600

# Work infrastructure
Host work-bastion
    HostName bastion.work.com
    User ops
    IdentityFile ~/.ssh/work

Host work-*
    User deploy
    IdentityFile ~/.ssh/work
    ProxyJump work-bastion

Host work-web
    HostName 10.0.1.10

Host work-api
    HostName 10.0.1.20

Host work-db
    HostName 10.0.2.10
    LocalForward 5432 localhost:5432

# Home lab
Host pi
    HostName 192.168.1.100
    User pi
    IdentityFile ~/.ssh/homelab

Host nas
    HostName 192.168.1.50
    User admin
    IdentityFile ~/.ssh/homelab

# GitHub (useful for multiple accounts)
Host github-personal
    HostName github.com
    User git
    IdentityFile ~/.ssh/github_personal

Host github-work
    HostName github.com
    User git
    IdentityFile ~/.ssh/github_work

File Permissions

SSH is picky about permissions:

1
2
3
4
5
chmod 700 ~/.ssh
chmod 600 ~/.ssh/config
chmod 600 ~/.ssh/id_*
chmod 644 ~/.ssh/*.pub
chmod 700 ~/.ssh/sockets

If permissions are wrong, SSH will refuse to use the files.

Debugging

When things don’t work:

1
2
3
4
5
6
7
8
# Verbose output
ssh -v dev

# Very verbose
ssh -vv dev

# Maximum verbosity
ssh -vvv dev

Check which config file options are being applied:

1
ssh -G dev | grep -i identity

A well-organized SSH config is infrastructure documentation that actually works. Every host you add makes the next connection faster. Every pattern you define reduces cognitive load.

Start simple. Add hosts as you need them. In a month, you’ll wonder how you ever worked without it.