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.