SSH is the front door to your servers. A weak SSH config is an open invitation to attackers. Here’s how to lock it down properly without locking yourself out.
The Bare Minimum#
1
2
3
4
5
6
7
8
9
10
11
12
13
| # /etc/ssh/sshd_config
# Disable root login
PermitRootLogin no
# Disable password authentication
PasswordAuthentication no
# Enable key-based auth only
PubkeyAuthentication yes
# Disable empty passwords
PermitEmptyPasswords no
|
1
2
| # Apply changes
sudo systemctl restart sshd
|
These four settings stop 99% of automated attacks.
Key-Based Authentication#
Generate Strong Keys#
1
2
3
4
5
6
7
8
| # Ed25519 (recommended - fast, secure, short keys)
ssh-keygen -t ed25519 -C "your_email@example.com"
# RSA 4096 (if you need compatibility)
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
# With custom filename
ssh-keygen -t ed25519 -f ~/.ssh/work_key -C "work"
|
Deploy Keys#
1
2
3
4
5
6
7
8
| # Copy to server
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server
# Or manually
cat ~/.ssh/id_ed25519.pub | ssh user@server "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
# Fix permissions on server
ssh user@server "chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys"
|
Protect Your Private Key#
1
2
3
4
5
6
7
8
9
| # Add passphrase to existing key
ssh-keygen -p -f ~/.ssh/id_ed25519
# Use ssh-agent to avoid retyping
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
# macOS: Add to keychain
ssh-add --apple-use-keychain ~/.ssh/id_ed25519
|
Full Hardened 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
| # /etc/ssh/sshd_config
# Protocol and listening
Port 22
AddressFamily inet
ListenAddress 0.0.0.0
# Authentication
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
PermitEmptyPasswords no
ChallengeResponseAuthentication no
UsePAM yes
# Disable unused auth methods
KerberosAuthentication no
GSSAPIAuthentication no
HostbasedAuthentication no
# Limit users
AllowUsers deploy admin
# Or by group
AllowGroups ssh-users
# Session settings
MaxAuthTries 3
MaxSessions 5
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
# Disable forwarding (unless needed)
AllowTcpForwarding no
X11Forwarding no
AllowAgentForwarding no
# Security
StrictModes yes
IgnoreRhosts yes
# Logging
SyslogFacility AUTH
LogLevel VERBOSE
# SFTP (if needed)
Subsystem sftp /usr/lib/openssh/sftp-server -f AUTHPRIV -l INFO
# Modern crypto only
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
|
Change the Port (Security Through Obscurity)#
Not real security, but reduces log noise:
1
2
| # /etc/ssh/sshd_config
Port 2222
|
1
2
3
4
5
6
| # Update firewall
sudo ufw allow 2222/tcp
sudo ufw delete allow 22/tcp
# Connect with new port
ssh -p 2222 user@server
|
Fail2ban: Block Brute Force#
1
2
3
4
5
| # Install
sudo apt install fail2ban
# Configure
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
|
1
2
3
4
5
6
7
8
9
10
| # /etc/fail2ban/jail.local
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600
ignoreip = 127.0.0.1/8 192.168.1.0/24
|
1
2
3
4
5
6
| # Start and enable
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
# Check status
sudo fail2ban-client status sshd
|
Two-Factor Authentication#
Google Authenticator#
1
2
3
4
5
6
| # Install
sudo apt install libpam-google-authenticator
# Configure per user
google-authenticator
# Answer: y, y, y, n, y (recommended)
|
1
2
3
4
5
6
7
| # /etc/pam.d/sshd
# Add at the end
auth required pam_google_authenticator.so
# /etc/ssh/sshd_config
ChallengeResponseAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
|
Now requires both SSH key AND TOTP code.
SSH Certificates (Advanced)#
Better than authorized_keys for teams:
1
2
3
4
5
6
7
8
9
10
11
12
13
| # Create CA
ssh-keygen -t ed25519 -f ca_key -C "SSH CA"
# Sign user key
ssh-keygen -s ca_key -I "alice@company" -n alice -V +52w alice_id_ed25519.pub
# Creates alice_id_ed25519-cert.pub
# Server trusts CA
# /etc/ssh/sshd_config
TrustedUserCAKeys /etc/ssh/ca_key.pub
# Sign host keys (for known_hosts)
ssh-keygen -s ca_key -I "server.example.com" -h -V +52w /etc/ssh/ssh_host_ed25519_key.pub
|
Benefits:
- Revoke access by revoking certificates
- No managing authorized_keys on every server
- Expiration built in
Jump Hosts / Bastion#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # ~/.ssh/config
Host bastion
HostName bastion.example.com
User admin
IdentityFile ~/.ssh/bastion_key
Host internal-*
ProxyJump bastion
User deploy
IdentityFile ~/.ssh/deploy_key
Host internal-web
HostName 10.0.1.10
Host internal-db
HostName 10.0.1.20
|
1
2
3
| # Direct connection through bastion
ssh internal-web
# Equivalent to: ssh -J bastion deploy@10.0.1.10
|
Bastion Hardening#
1
2
3
4
5
| # Bastion sshd_config additions
AllowTcpForwarding yes
AllowAgentForwarding yes
PermitTTY no # No shell access
ForceCommand /bin/false # Or specific command
|
SSH Config Tips#
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
| # ~/.ssh/config
# Global defaults
Host *
AddKeysToAgent yes
IdentitiesOnly yes
ServerAliveInterval 60
ServerAliveCountMax 3
# Work servers
Host work-*
User deploy
IdentityFile ~/.ssh/work_key
Host work-prod
HostName prod.example.com
Host work-staging
HostName staging.example.com
# Personal
Host github.com
IdentityFile ~/.ssh/github_key
Host homelab
HostName 192.168.1.100
User admin
Port 2222
IdentityFile ~/.ssh/homelab_key
|
Audit and Monitor#
Check Current Sessions#
1
2
3
4
5
6
| # Who's connected
who
w
# SSH sessions specifically
ss -tnp | grep ssh
|
Auth Logs#
1
2
3
4
5
6
7
8
| # Recent auth attempts
sudo tail -f /var/log/auth.log
# Failed attempts
sudo grep "Failed password" /var/log/auth.log
# Successful logins
sudo grep "Accepted" /var/log/auth.log
|
Audit Script#
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
| #!/bin/bash
# ssh-audit.sh
echo "=== SSH Config Check ==="
# Check for password auth
if grep -q "^PasswordAuthentication yes" /etc/ssh/sshd_config; then
echo "⚠️ Password authentication is ENABLED"
else
echo "✅ Password authentication disabled"
fi
# Check for root login
if grep -q "^PermitRootLogin yes" /etc/ssh/sshd_config; then
echo "⚠️ Root login is ENABLED"
else
echo "✅ Root login disabled"
fi
# Check for empty passwords
if grep -q "^PermitEmptyPasswords yes" /etc/ssh/sshd_config; then
echo "⚠️ Empty passwords PERMITTED"
else
echo "✅ Empty passwords blocked"
fi
# List users with authorized_keys
echo -e "\n=== Users with SSH Keys ==="
for user_home in /home/*; do
user=$(basename "$user_home")
if [[ -f "$user_home/.ssh/authorized_keys" ]]; then
count=$(wc -l < "$user_home/.ssh/authorized_keys")
echo "$user: $count key(s)"
fi
done
# Check for weak keys
echo -e "\n=== Key Strength Check ==="
for keyfile in /etc/ssh/ssh_host_*_key.pub; do
bits=$(ssh-keygen -l -f "$keyfile" | awk '{print $1}')
type=$(ssh-keygen -l -f "$keyfile" | awk '{print $4}')
echo "$keyfile: $bits bits ($type)"
done
|
Firewall Integration#
UFW#
1
2
3
4
5
6
7
8
| # Basic
sudo ufw allow ssh
# Rate limiting
sudo ufw limit ssh
# Specific IPs only
sudo ufw allow from 10.0.0.0/8 to any port 22
|
iptables Rate Limiting#
1
2
3
| # Limit new connections
iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set
iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 -j DROP
|
Recovery: Don’t Lock Yourself Out#
Before Making Changes#
1
2
3
4
5
6
7
8
9
10
11
12
13
| # 1. Keep a session open
# Don't close your current SSH session until verified
# 2. Test config before restart
sudo sshd -t
# 3. Have console access ready
# Cloud: Use web console
# Physical: Have keyboard/monitor
# VM: Use hypervisor console
# 4. Set a timeout
sudo at now + 5 minutes <<< "cp /etc/ssh/sshd_config.backup /etc/ssh/sshd_config && systemctl restart sshd"
|
If Locked Out#
1
2
3
4
5
| # Boot to rescue mode
# Mount root filesystem
# Edit /etc/ssh/sshd_config
# Re-enable password auth temporarily
# Reboot and fix properly
|
The Checklist#
Start Here#
- Today: Disable password auth and root login
- This week: Install fail2ban
- This month: Implement jump host for internal servers
- This quarter: Consider SSH certificates for team
Lock the front door. Everything else depends on it.
SSH is simple until it isn’t. Spend 30 minutes hardening now, save hours of incident response later.
📬 Get the Newsletter
Weekly insights on DevOps, automation, and CLI mastery. No spam, unsubscribe anytime.