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

  • Password authentication disabled
  • Root login disabled
  • Key-based auth only
  • Strong keys (Ed25519 or RSA 4096)
  • Fail2ban installed
  • MaxAuthTries limited
  • AllowUsers/AllowGroups configured
  • Firewall configured
  • Logging enabled (VERBOSE)
  • Unused forwarding disabled

Start Here

  1. Today: Disable password auth and root login
  2. This week: Install fail2ban
  3. This month: Implement jump host for internal servers
  4. 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.