SSH Security Hardening: Lock Down Your Servers in 15 Minutes

August 8, 2026 · 7 min read

SSH is the front door to every server you manage. If it's misconfigured, you're one leaked password or brute-force script away from a breach. The default OpenSSH install on most Linux distributions is functional but not secure — it allows password authentication, listens on a well-known port, and has no rate limiting. That's fine for a throwaway test box. For production, you need to harden it.

This guide covers the changes that actually matter: key-based authentication, disabling password login, fail2ban for brute-force protection, non-standard ports, and jump host architecture. Every config snippet is tested on Ubuntu 24.04 and works on any modern Linux distribution.

1. Generate a Strong SSH Key Pair

If you're still using RSA keys, it's time to upgrade. Ed25519 keys are shorter, faster, and considered more secure against future quantum attacks:

# Generate Ed25519 key (recommended)
ssh-keygen -t ed25519 -C "your@email.com" -f ~/.ssh/id_ed25519

# If you need RSA for compatibility (older systems)
ssh-keygen -t rsa -b 4096 -C "your@email.com" -f ~/.ssh/id_rsa

Always set a passphrase. A stolen key file without a passphrase gives an attacker instant access. With a passphrase, they need both the file and the passphrase — two-factor for SSH without buying hardware tokens.

Copy your public key to the server:

# Modern way (ships with OpenSSH 8.2+)
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server

# Manual way
cat ~/.ssh/id_ed25519.pub | ssh user@server "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys"

2. Harden sshd_config

Edit /etc/ssh/sshd_config — these are the changes that move the needle:

# Disable password authentication (key-only)
PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no

# Disable root login
PermitRootLogin no

# Disable empty passwords
PermitEmptyPasswords no

# Limit authentication attempts per connection
MaxAuthTries 3

# Idle timeout: disconnect after 5 minutes of inactivity
ClientAliveInterval 300
ClientAliveCountMax 2

# Restrict to protocol 2
Protocol 2

# Disable X11 forwarding (unless you need it)
X11Forwarding no

# Disable TCP forwarding by default (allow per-user)
AllowTcpForwarding no

# Use only strong ciphers and MACs
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
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org

Critical: Before disabling password authentication, make sure your key-based login works. Test it in a separate terminal session — don't lock yourself out. Keep one session open with the old settings while you verify.

# Test config syntax before restarting
sudo sshd -t

# Restart SSH (keeps current sessions alive)
sudo systemctl restart sshd

3. Fail2ban: Stop Brute-Force Attacks

Even with password auth disabled, bots will hammer your SSH port. Fail2ban monitors auth logs and bans IPs after repeated failures:

# Install
sudo apt install fail2ban -y

# Create local config (never edit jail.conf directly)
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Edit /etc/fail2ban/jail.local, find the [sshd] section:

[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600
ignoreip = 127.0.0.1/8 YOUR_STATIC_IP

This bans an IP for 1 hour after 3 failures within 10 minutes. Add your own static IP to ignoreip so you don't accidentally lock yourself out. Start fail2ban:

sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# Check banned IPs
sudo fail2ban-client status sshd

4. Change the Default Port

Moving SSH from port 22 to a non-standard port won't stop a targeted attacker, but it eliminates 99% of automated scanners. Most bots only scan port 22:

# In /etc/ssh/sshd_config
Port 2222

Update your firewall and SSH client config:

# ~/.ssh/config — so you don't have to type -p every time
Host production-server
    HostName 203.0.113.50
    User deploy
    Port 2222
    IdentityFile ~/.ssh/id_ed25519

Warning: If you're behind a corporate firewall that only allows outbound port 22, changing the port will break your access. Test first.

5. Jump Host Architecture

For production environments with multiple servers, don't expose SSH on every machine. Use a bastion host (jump host) as the single entry point:

# ~/.ssh/config
Host bastion
    HostName bastion.example.com
    User admin
    Port 2222
    IdentityFile ~/.ssh/id_ed25519

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

Host internal-db
    HostName 10.0.1.50

Host internal-app
    HostName 10.0.1.51

Now ssh internal-db automatically tunnels through the bastion. Internal servers only accept connections from the bastion's IP:

# On internal servers' sshd_config
AllowUsers deploy@10.0.1.1

This means an attacker needs to compromise the bastion and the internal key to reach your database. That's two layers instead of one.

6. SSH Audit Logging

Know who's connecting and what they're doing. Add these to your sshd_config:

# Log at VERBOSE level for key fingerprints
LogLevel VERBOSE

Then check the logs:

# Who logged in recently
grep "Accepted publickey" /var/log/auth.log | tail -20

# Failed attempts (should be near zero after hardening)
grep "Failed" /var/log/auth.log | tail -20

# Key fingerprints used
grep "Accepted publickey" /var/log/auth.log | grep -oP 'SHA256:\S+'

For production, ship these logs to your centralized logging stack (ELK, Loki, etc.). A sudden spike in failed SSH attempts is an early warning sign of a targeted attack.

7. Two-Factor Authentication (Optional)

For high-security environments, add TOTP-based 2FA on top of SSH keys:

# Install Google Authenticator PAM module
sudo apt install libpam-google-authenticator -y

# Set up for your user
google-authenticator

# In /etc/pam.d/sshd (add at the end)
auth required pam_google_authenticator.so

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

This requires both the SSH key and a TOTP code. Even a stolen key file without the TOTP seed is useless. The downside: you can't use ssh in scripts without interaction. Reserve this for human operators, not CI/CD pipelines.

Quick Hardening Checklist

SSH hardening isn't glamorous, but it's the foundation of server security. These changes take 15 minutes and eliminate the most common attack vectors. Do them once, document the config in your infrastructure-as-code repo, and you'll never have to think about SSH security again — until you add a new server and copy the same hardened config over.