SSH is the front door to your infrastructure. Every server, every cloud instance, every piece of automation relies on it. A compromised SSH setup means game over.

These hardening steps dramatically reduce your attack surface.

Disable Password Authentication

Passwords can be brute-forced. Keys can’t (practically):

1
2
3
4
# /etc/ssh/sshd_config
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM no
1
2
# Restart SSH (keep your current session open!)
sudo systemctl restart sshd

Before doing this, ensure your key is in ~/.ssh/authorized_keys.

Use Strong Key Types

1
2
3
4
5
# Generate Ed25519 key (recommended)
ssh-keygen -t ed25519 -C "your_email@example.com"

# Or RSA with 4096 bits minimum
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

Ed25519 is faster, smaller, and considered more secure than RSA.

Disable Root Login

Never allow direct root SSH:

1
2
# /etc/ssh/sshd_config
PermitRootLogin no

Use a regular user and sudo instead. This adds accountability and a second authentication layer.

Limit User Access

Only allow specific users to SSH:

1
2
3
4
# /etc/ssh/sshd_config
AllowUsers alice bob deploy
# Or by group
AllowGroups sshusers admins

Everyone else is rejected before authentication even starts.

Change the Default Port

Security through obscurity isn’t security, but it reduces noise:

1
2
# /etc/ssh/sshd_config
Port 2222
1
2
3
# Don't forget to update firewall
sudo ufw allow 2222/tcp
sudo ufw delete allow 22/tcp

This eliminates 99% of automated scanning bots.

Rate Limiting with Fail2ban

Block IPs after failed attempts:

1
sudo apt install fail2ban
1
2
3
4
5
6
7
8
9
# /etc/fail2ban/jail.local
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600

Three failures in 10 minutes = banned for an hour.

SSH Config Hardening

Full hardened configuration:

 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
# /etc/ssh/sshd_config

# Protocol and port
Port 2222
Protocol 2
AddressFamily inet  # IPv4 only, or inet6 for IPv6

# Authentication
PermitRootLogin no
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
UsePAM no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys

# Limit users
AllowUsers deploy admin

# Timeouts and limits
LoginGraceTime 30
MaxAuthTries 3
MaxSessions 5
ClientAliveInterval 300
ClientAliveCountMax 2

# Disable unused features
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no

# Logging
SyslogFacility AUTH
LogLevel VERBOSE

# Cryptography (modern algorithms 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

Test before applying:

1
sudo sshd -t  # Syntax check

Key-Based Access Control

Restrict what keys can do:

1
2
3
4
5
6
# ~/.ssh/authorized_keys
# Limit to specific commands
command="/usr/local/bin/backup.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-ed25519 AAAA... backup-key

# Restrict source IP
from="192.168.1.0/24",no-port-forwarding ssh-ed25519 AAAA... office-key

Two-Factor Authentication

Add TOTP to SSH:

1
2
sudo apt install libpam-google-authenticator
google-authenticator  # Run as user, follow prompts
1
2
3
4
5
6
# /etc/pam.d/sshd
auth required pam_google_authenticator.so

# /etc/ssh/sshd_config
ChallengeResponseAuthentication yes
AuthenticationMethods publickey,keyboard-interactive

Now login requires: valid key + TOTP code.

SSH Certificates (Advanced)

For larger teams, use certificates instead of distributing keys:

1
2
3
4
5
6
7
8
# Create CA
ssh-keygen -t ed25519 -f ca_key -C "SSH CA"

# Sign a user key
ssh-keygen -s ca_key -I user_alice -n alice -V +52w alice_key.pub

# Server trusts CA
echo "TrustedUserCAKeys /etc/ssh/ca_key.pub" >> /etc/ssh/sshd_config

Revoke access by revoking certificates, not hunting down authorized_keys files.

Bastion Host Pattern

Don’t expose all servers to the internet:

InternetBastion(hardened)Internalservers
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# ~/.ssh/config on your laptop
Host bastion
    HostName bastion.example.com
    User admin
    Port 2222
    IdentityFile ~/.ssh/bastion_key

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

Host internal-web
    HostName 10.0.1.10

Host internal-db
    HostName 10.0.1.20
1
2
# Connect through bastion automatically
ssh internal-web

Audit SSH Access

Know who’s logging in:

1
2
3
4
5
6
7
8
# Recent logins
last -a | head -20

# Failed attempts
grep "Failed password" /var/log/auth.log | tail -20

# Accepted keys
grep "Accepted publickey" /var/log/auth.log | tail -20

Set up alerts for:

  • Successful logins from new IPs
  • Multiple failed attempts
  • Root login attempts (should be zero)

SSH Agent Forwarding Risks

Agent forwarding is convenient but dangerous:

1
2
3
4
5
# Dangerous: forwards your agent to remote host
ssh -A server

# Anyone with root on that server can use your agent
# to authenticate as you to other systems

Use ProxyJump instead of agent forwarding when possible.

Quick Security Checklist

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Verify your config
sudo sshd -T | grep -E "passwordauth|permitroot|port|allowusers"

# Check for weak keys
for key in /etc/ssh/ssh_host_*_key.pub; do
    ssh-keygen -l -f "$key"
done

# Review authorized keys
cat ~/.ssh/authorized_keys

# Check who's connected now
who
ss -tn | grep :22

SSH hardening is layers: strong keys, no passwords, limited users, rate limiting, monitoring. No single measure is enough; together they make compromise significantly harder.

Test changes carefully — locking yourself out of a remote server is a bad day. Always keep a session open while testing SSH config changes.