# SSH Security Hardening for VPS: Stop the Brute Force Before It Starts

> A practical SSH hardening guide for VPS owners — disable password auth, change ports, configure fail2ban, and lock down sshd_config before your server gets compromised.

URL: https://www.matthewswong.com/en/blog/ssh-security-hardening/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2025-06-10
- Updated: 2026-10-17
- Category: DevOps

A fresh DigitalOcean Droplet with default SSH settings will see its first brute-force attempt within minutes of going online. Real-world data shows 145 failed SSH login attempts and 45 auto-bans in the first week for a typical internet-facing server targeted by bots scanning port 22. SSH is the front door of your VPS, and the default configuration is wide open to password-based attacks. This guide covers every hardening step worth applying before any application goes live.

## Step 1: SSH Key Authentication Only

SSH key pairs are cryptographically immune to brute-force. A 4096-bit RSA key or an Ed25519 key has more possible combinations than atoms in the observable universe — no botnet can crack it by trying combinations. Password authentication is the attack surface; eliminating it eliminates the risk. Generate a key pair on your local machine (ssh-keygen -t ed25519 -C your@email.com), copy the public key to the server (ssh-copy-id user@server), verify you can log in with the key, then disable password authentication in sshd_config.

### Critical sshd_config Settings

Edit /etc/ssh/sshd_config and set: PasswordAuthentication no, PermitRootLogin no, PubkeyAuthentication yes, MaxAuthTries 3, X11Forwarding no, AllowTcpForwarding no, PermitEmptyPasswords no, LoginGraceTime 30, ClientAliveInterval 300, ClientAliveCountMax 2. The ClientAlive settings disconnect idle sessions after 10 minutes, preventing ghost sessions from accumulating. Test the config with sshd -t before reloading — a syntax error will not prevent the existing session from working but will prevent new connections after a reload.

### Change the Default SSH Port

Moving SSH from port 22 to a non-standard port (e.g., 2222, 22222) eliminates the vast majority of automated scanning noise. Bots scan port 22 by default; very few scan non-standard ports comprehensively. This is security through obscurity and not a primary defense, but it reduces log noise by 90%+ and makes your fail2ban logs much easier to read. On DigitalOcean, always update the Cloud Firewall to allow the new port and block 22 before restarting sshd.

> When hardening VPS instances, always keep one terminal session active while testing SSH changes. The sequence I use: 1) Make the sshd_config change, 2) Run sshd -t to validate, 3) Open a second SSH session to verify the new config works, 4) Only then close the original session. This prevents being locked out. I have been locked out exactly once — on a GCP instance — and had to use the serial console to recover. It is a painful 20-minute experience that you only want to have once.

## Step 2: Fail2ban for Automated IP Banning

Even with password auth disabled, SSH scanners generate noise. Fail2ban monitors /var/log/auth.log, detects repeated failed login attempts, and adds iptables rules to ban offending IPs automatically. With default settings (5 failures in 10 minutes = 10-minute ban), fail2ban handles the automated scanning load without manual intervention. Configure a longer ban time (bantime = 1h or bantime = 24h) and lower maxretry (3) for more aggressive protection. The SSH jail is enabled by default in most fail2ban configurations.

### Cloud Firewall as the Outer Defense

Layer your defenses: the cloud provider firewall is your outermost defense and blocks traffic before it reaches your VPS. Use DigitalOcean Cloud Firewall or GCP VPC firewall rules to restrict SSH access to your known IP addresses only (your home IP, office IP, VPN IP). If your IP is dynamic, restrict SSH to a VPN-sourced IP instead. This is the most effective defense — traffic that never reaches your server cannot attempt authentication.

> **AllowUsers Gotcha**

> I learned this the hard way: when you add AllowUsers username to sshd_config and reload, ALL OTHER USERS are immediately denied SSH access — including root and any other user you did not explicitly list. I added AllowUsers matthews to a shared VPS without listing the deploy user, and our CI/CD pipeline stopped working immediately because it used a separate deploy user for deployments. Always list all SSH users in AllowUsers and test with a non-privileged session before applying changes.

## Step 3: Two-Factor Authentication

For maximum security, add TOTP-based two-factor authentication via Google Authenticator PAM module (libpam-google-authenticator). This requires both your SSH key and a time-based OTP even if someone steals your private key. Set up: install the PAM module, run google-authenticator as the target user, configure /etc/pam.d/sshd to require it, and enable ChallengeResponseAuthentication in sshd_config. The trade-off is operational overhead: every SSH session requires entering an OTP. For shared team accounts, this is impractical. For root or admin accounts on high-value servers, it is worth the friction.

## Auditing Your SSH Security

After hardening, audit your setup: run ssh-audit (available at ssh-audit.com) against your server to check for weak algorithms and ciphers. Review /etc/ssh/sshd_config for any remaining weak settings. Check that fail2ban is running and has active bans (fail2ban-client status sshd). Scan your server with nmap from an external host to verify only the expected ports are open. Set up a weekly cron job to email you a summary of SSH login attempts and bans.

## Minimal Hardened sshd_config

A production-ready minimal sshd_config for Ubuntu 22.04: disable password and root login, enable key-based auth, set MaxAuthTries 3, restrict to specific users with AllowUsers, use modern key exchange algorithms only (KexAlgorithms curve25519-sha256,diffie-hellman-group16-sha512), and enable logging at VERBOSE level. The VERBOSE log level captures key fingerprints in the auth log, which helps identify which key was used for a login — critical for auditing on multi-user servers.

## Sources & Further Reading

- DigitalOcean — Hardening SSH Fail2Ban Nftables and Cloud Firewalls
- VPS.DO — VPS Security Hardening Checklist 10 Steps 2025
- MassiveGRID — Ubuntu 24.04 VPS Security Hardening Guide SSH Firewall Fail2Ban

## Source links

- https://www.digitalocean.com/community/tutorials/hardening-ssh-fail2ban
- https://vps.do/vps-security-hardening-checklist-10-steps-to-protect-your-server-in-2025/
- https://massivegrid.com/blog/ubuntu-vps-security-hardening-guide/

## FAQ

### Why is disabling password authentication the single most important SSH hardening step?

Password authentication is the attack surface that automated brute-force bots exploit — a typical internet-facing VPS can receive 145 failed SSH login attempts in its first week. SSH key pairs like Ed25519 are cryptographically immune to brute-force because they have more possible combinations than atoms in the observable universe. Eliminating password auth eliminates this entire class of attack.

### What does changing the default SSH port actually protect against, and what are its limits?

Moving SSH from port 22 to a non-standard port (e.g., 2222 or 22222) eliminates the majority of automated scanning noise because bots scan port 22 by default and very few comprehensively scan non-standard ports. This reduces log noise by 90%+ and makes fail2ban logs easier to read. However, it is security through obscurity and not a primary defense — it must be combined with key-based auth and a cloud firewall.

### What is the AllowUsers gotcha in sshd_config and why can it break CI/CD pipelines?

When you add AllowUsers to sshd_config and reload, all users not explicitly listed are immediately denied SSH access — including service accounts used by automation. In the post, adding AllowUsers matthews to a shared VPS without listing the deploy user caused the CI/CD pipeline to stop working instantly. Always list every SSH user that needs access and test with a non-privileged session before applying the change.

### How does fail2ban complement SSH key authentication if keys already block brute-force?

Even with password authentication disabled, SSH scanners still generate connection noise that clutters logs and consumes resources. Fail2ban monitors /var/log/auth.log, detects repeated failed login attempts, and automatically bans offending IPs via iptables rules. Configuring a longer ban time (bantime = 1h or 24h) and a lower maxretry (3) provides more aggressive protection against persistent scanners.

### What is the recommended safe sequence for testing SSH config changes to avoid getting locked out?

Make the sshd_config change, run sshd -t to validate syntax, open a second SSH session to verify the new config works, and only then close the original session. This sequence ensures a syntax error or misconfiguration does not sever your only active connection. The post notes that skipping this process once on a GCP instance required a 20-minute serial console recovery.

---

- HTML version: https://www.matthewswong.com/en/blog/ssh-security-hardening/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/ssh-security-hardening/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
