Every minute your VPS is online, automated bots are probing port 22, trying common usernames and passwords. SSH is your server’s front door — and if it’s configured with defaults, that door might as well have a “welcome” mat.

We manage hundreds of servers, and the single most common vulnerability we see on new VPS setups is default SSH configuration. The defaults are designed for maximum compatibility out of the box, not maximum security. The good news? Hardening SSH takes about 20 minutes and eliminates the vast majority of unauthorized access attempts.

This guide walks through every SSH hardening technique we use on our own infrastructure — from essential steps that block 99% of automated attacks, to advanced configurations for compliance-heavy environments.

What You Will Need

  • A Linux VPS with root or sudo access (Ubuntu 22.04/24.04, Debian 12, or CentOS/Rocky Linux 9)
  • SSH client installed on your local machine (OpenSSH client on Linux/macOS, or PowerShell SSH on Windows 10+)
  • At least 15-20 minutes of uninterrupted time — lockout risk means no rushing

For this guide, we recommend a Cloud VPS with at least 1 vCPU and 2 GB RAM. Canadian Web Hosting’s Cloud VPS gives you full root access to apply these hardening techniques, with Canadian data centres and 24/7 support if you get stuck.

1. Key-Based Authentication (Disable Password Auth)

This is the single most impactful SSH hardening step. Password authentication is vulnerable to brute-force attacks — bots can try thousands of combinations per minute. SSH key pairs use public-key cryptography, making them computationally infeasible to brute force.

Generate an Ed25519 Key Pair

Ed25519 keys are the current best practice: smaller than RSA, faster to generate, and equally secure. OpenSSH 6.5+ (released 2014) supports them, so virtually every modern server does.

# On your local machine
ssh-keygen -t ed25519 -a 100 -f ~/.ssh/id_ed25519

The -a 100 flag sets the number of KDF rounds — higher values slow down brute-force attempts if the private key is stolen. This adds a fraction of a second to your login but significantly increases attack cost.

Copy the Public Key to Your Server

ssh-copy-id -i ~/.ssh/id_ed25519.pub user@your-server-ip

If ssh-copy-id is not available (some minimal systems), append the key manually:

cat ~/.ssh/id_ed25519.pub | ssh user@your-server-ip "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"

Disable Password Authentication

Once you’ve verified key-based login works (test in a SEPARATE terminal session before closing the current one), edit /etc/ssh/sshd_config:

# In /etc/ssh/sshd_config:
PasswordAuthentication no
KbdInteractiveAuthentication no
AuthenticationMethods publickey

On newer Ubuntu installations (22.04+), password authentication may be configured in a drop-in file under /etc/ssh/sshd_config.d/. Check both locations:

grep -r "PasswordAuthentication" /etc/ssh/

If you find it in a .conf file under sshd_config.d/, edit it there instead. Drop-in files override the main config.

Warning: Do NOT close your current SSH session until you have confirmed key-based authentication works from a second terminal. If something goes wrong, the open session is your lifeline.

2. Disable Root Login

“root” is a known username on every Linux server. Brute-force bots always try it first. Disabling root login removes this attack surface entirely — legitimate administrative access can use sudo after logging in as a regular user.

# In /etc/ssh/sshd_config:
PermitRootLogin no

If you absolutely need root SSH access (some legacy automation scripts require it), use prohibit-password instead of no:

PermitRootLogin prohibit-password

This allows root login only with key-based authentication, never passwords.

3. Configure Fail2ban for SSH

Fail2ban monitors authentication logs and temporarily bans IP addresses that show repeated failure patterns. It’s your second line of defence — even if password auth were somehow re-enabled, fail2ban would block the attacker after a few failed attempts.

# Install fail2ban
sudo apt update && sudo apt install fail2ban -y

# Create a local SSH jail override. This avoids editing jail.conf directly.
sudo tee /etc/fail2ban/jail.d/sshd.local >/dev/null <<'EOF'
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = %(sshd_log)s
maxretry = 5
bantime = 3600
findtime = 600
EOF

sudo systemctl restart fail2ban

What these settings mean:

  • maxretry = 5: Allow 5 failed attempts (accidental typos happen)
  • findtime = 600: Count retries within a 10-minute sliding window
  • bantime = 3600: Ban for 1 hour (86400 for 24 hours)

For persistent offenders, enable the recidive jail which applies increasingly long bans to repeat offenders:

sudo tee /etc/fail2ban/jail.d/recidive.local >/dev/null <<'EOF'
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
maxretry = 3
bantime = 604800
EOF

sudo systemctl restart fail2ban

Check fail2ban status:

sudo fail2ban-client status sshd

Expected output shows the number of currently banned IPs and total failed attempts since the service started.

4. Set Connection Timeouts and Session Limits

Idle SSH sessions are a security risk — a forgotten terminal left open on a shared workstation could let anyone walk by and take control. Configure your server to disconnect idle sessions automatically:

# In /etc/ssh/sshd_config:
ClientAliveInterval 300
ClientAliveCountMax 2
MaxAuthTries 3
MaxSessions 4
  • ClientAliveInterval 300: Send a keepalive message every 5 minutes
  • ClientAliveCountMax 2: After 2 missed keepalives (10 minutes of silence), disconnect
  • MaxAuthTries 3: Limit authentication attempts per connection to 3
  • MaxSessions 4: Limit concurrent SSH sessions from a single connection

These settings prevent session hijacking from idle terminals and limit brute-force attempts per connection.

5. Disable Unnecessary Features

SSH has several features that are useful in specific scenarios but should be disabled unless you explicitly need them:

# In /etc/ssh/sshd_config:
AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no
PermitEmptyPasswords no
PrintLastLog yes
LogLevel VERBOSE
  • Agent forwarding: Only needed for SSH jump boxes. If disabled, SSH agents on your local machine can’t be forwarded to the remote server — preventing credential theft via compromised intermediates.
  • TCP forwarding: Only needed if you’re tunneling other protocols through SSH. Leave it on if you use SSH tunnels for database access or local development.
  • X11 forwarding: Disabled. X11 forwarding allows remote graphical applications — rarely needed on servers and introduces attack surface.
  • LogLevel VERBOSE: Records additional detail (key fingerprints, failed auth methods) in auth logs for forensic analysis.

6. Restart and Verify the Configuration

Before reloading SSH, validate your configuration:

sudo sshd -t

If there are no errors (no output = success), reload SSH:

sudo systemctl reload ssh || sudo systemctl reload sshd

To view the effective configuration with all settings resolved (including drop-in file overrides):

sudo sshd -T | grep -E "(passwordauthentication|permitrootlogin|maxauthtries)"

7. Advanced: Change the Default SSH Port (Optional)

Changing SSH from port 22 to a non-standard port is security through obscurity — it won’t stop a determined attacker. However, it will dramatically reduce log noise from automated scanners.

# In /etc/ssh/sshd_config:
Port 2222  # Choose any port above 1024 (below 1024 needs root)

# Update firewall rules
sudo ufw allow 2222/tcp

# If using SELinux (RHEL/CentOS/Rocky):
sudo semanage port -a -t ssh_port_t -p tcp 2222

Gotchas:

  • Update your SSH client config to use the new port: ssh -p 2222 user@server
  • Update monitoring tools and backup scripts that connect via SSH
  • Document the change somewhere accessible — port 22 being “wrong” is easy to forget
  • If using fail2ban with CrowdSec, update the jail port setting to match

8. Advanced: Two-Factor Authentication for SSH

For high-security environments, add TOTP-based 2FA on top of key-based authentication. This means an attacker needs BOTH your private key AND your time-based one-time password to log in.

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

# Run the setup as your user (scan the QR code with an authenticator app)
google-authenticator

# In /etc/pam.d/sshd, add at the top:
# (Before the @include common-auth line)
auth required pam_google_authenticator.so

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

Critical safety note: The AuthenticationMethods directive with publickey,keyboard-interactive means BOTH factors are required — key first, then TOTP. Test this from a session you do NOT close until you’ve confirmed it works. Keep your recovery codes stored offline.

CWH’s Managed Security service includes 2FA configuration as part of the baseline setup for SOC 2-compliant environments.

9. Advanced: IP Allowlisting with UFW

If your team connects from a fixed IP address (or a limited range), restrict SSH access to those addresses only:

# Allow SSH only from your static IP
sudo ufw allow from YOUR_STATIC_IP to any port 22

# Deny all other SSH traffic (implicit — UFW default deny handles this)
sudo ufw enable

For teams without static IPs, a more robust approach is WireGuard VPN + SSH jump box: connect to your VPN first, then SSH to the server through the VPN tunnel. This keeps SSH entirely off the public internet.

CWH’s Firewall & VPN product provides site-to-site VPN with Juniper and Palo Alto appliances for enterprise network segmentation.

10. Monitoring and Auditing

After hardening, set up basic monitoring so you know what’s happening:

# View login attempts in the last 24 hours
sudo journalctl -u sshd --since "24 hours ago"

# Count failed password attempts
sudo journalctl -u sshd --since "24 hours ago" | grep -c "Failed password"

# Check fail2ban status
sudo fail2ban-client status sshd

Integrate these checks into your existing monitoring setup. For a complete monitoring stack comparison, see our guide to essential VPS security tools.

Troubleshooting: When You Get Locked Out

Even with careful configuration, lockouts happen. Here’s how to recover:

Symptom: “Permission denied (publickey)” after disabling password auth

Cause: The SSH key was not properly copied to the server, or permissions on ~/.ssh/authorized_keys are wrong.

Fix: Use your out-of-band console (CWH provides dedicated server iDRAC/iLO access, or contact Managed Support) to check:

# Fix permissions
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
# Verify the key is in authorized_keys
cat ~/.ssh/authorized_keys

Symptom: Fail2ban banned your own IP

Cause: You mistyped your password a few times while testing.

Fix:

# Check current bans
sudo fail2ban-client status sshd
# Unban your IP
sudo fail2ban-client set sshd unbanip YOUR_IP

Symptom: Can’t connect after changing SSH port

Cause: Firewall rules weren’t updated to allow the new port.

Fix: Connect via out-of-band console and check:

sudo ufw status
# If the new port isn't listed:
sudo UFW allow 2222/tcp
sudo systemctl reload ssh || sudo systemctl reload sshd

Related Issues

SSH hardening is step one of a comprehensive server security strategy. Follow up with:

Conclusion

SSH hardening is not optional for any internet-facing Linux server. The techniques in this guide — key-based authentication, disabled root login, fail2ban, connection timeouts, and disabled unnecessary features — block the vast majority of automated attacks before they even reach your applications.

The entire process takes about 20 minutes on a fresh VPS. Compared to the cost of a breach (downtime, data loss, reputational damage), that 20 minutes is the highest-ROI security investment you can make.

For teams that prefer hands-off security management, CWH Managed Security includes SSH hardening as part of our baseline configuration, with ongoing compliance monitoring and 24/7 incident response. Our Managed Support team can also handle the initial setup and ensure SSH hardening is properly configured on your existing servers.

Sources and Further Reading