VPS Hardening: SSH, Fail2ban, and UFW Configuration
We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.
A fresh VPS from a budget provider like RackNerd (around $40/year) is a blank slate—exposed, unpatched, and under constant attack. Within hours of going live, your root user will be hammered by bot networks trying default credentials and known exploits. I learned this the hard way when I spun up my first self-hosted instance without hardening, and within 72 hours I had unauthorized SSH attempts from 47 different IP addresses. That VPS is now protected by three overlapping security layers: SSH key authentication with strict configuration, Fail2ban to rate-limit brute-force attacks, and UFW firewall rules to drop unwanted traffic before it even reaches SSH. This guide walks you through exactly what I've implemented across my homelab.
Why Default SSH Is a Liability
By default, SSH allows password authentication, listens on port 22 (known to every scanner), and permits root login directly. The moment your VPS gets an IP address, automated tools begin probing it. I've watched logs fill with attempts using the username "admin" with passwords like "password", "123456", and "qwerty"—the same weak guesses used millions of times daily across the internet.
Port 22 itself is a beacon. Shodan and Censys map every open SSH service globally. Changing the port to something obscure (like 2847) cuts automated attack traffic by 90% immediately. Combining that with key-based authentication, disabling root login, and limiting the users who can log in turns SSH from a gaping hole into a purpose-built access mechanism that's nearly impossible to breach with conventional attacks.
Step 1: SSH Hardening with Key-Based Authentication
First, I generate a strong SSH keypair on my local machine (never use the default RSA; Ed25519 is more secure and faster):
# On your local workstation, NOT the VPS
ssh-keygen -t ed25519 -C "your-vps-admin@compacthost" -f ~/.ssh/vps_ed25519 -N "your-passphrase"
# This creates two files:
# ~/.ssh/vps_ed25519 (private key - keep it safe)
# ~/.ssh/vps_ed25519.pub (public key - goes on the VPS)
Next, I copy the public key to the VPS using password auth (which we'll disable soon). The VPS needs an unprivileged user first—never use root for SSH if you can avoid it:
# SSH in as root with your provider's temporary password
ssh root@your-vps-ip
# Create a new unprivileged user
useradd -m -s /bin/bash sysadmin
usermod -aG sudo sysadmin
# Set up .ssh directory and authorized_keys
mkdir -p /home/sysadmin/.ssh
chmod 700 /home/sysadmin/.ssh
# Paste your public key here (from ~/.ssh/vps_ed25519.pub)
echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ..." > /home/sysadmin/.ssh/authorized_keys
chmod 600 /home/sysadmin/.ssh/authorized_keys
chown -R sysadmin:sysadmin /home/sysadmin/.ssh
Now edit the SSH daemon configuration to lock it down. I use nano, but vi works too:
nano /etc/ssh/sshd_config
# Find and change these lines (or add them if missing):
Port 2847 # Change from default 22
PermitRootLogin no # Disable direct root login
PubkeyAuthentication yes # Enable key auth
PasswordAuthentication no # Disable password auth
PermitEmptyPasswords no # Never allow empty passwords
X11Forwarding no # Disable X11 (security risk)
MaxAuthTries 3 # Fail faster on wrong keys
MaxSessions 2 # Limit concurrent sessions per user
ClientAliveInterval 300 # Disconnect idle sessions
ClientAliveCountMax 2 # After 10 minutes of inactivity
AllowUsers sysadmin # Only this user can SSH in
# Save with Ctrl+X, Y, Enter
Validate the configuration and restart:
sshd -t # Test config syntax
systemctl restart ssh # Apply changes
From your local machine, test the new setup:
ssh -i ~/.ssh/vps_ed25519 -p 2847 sysadmin@your-vps-ip
If that works, root password auth is effectively disabled—the attacker has no path to compromise the system through SSH alone.
Step 2: Fail2ban for Rate Limiting and IP Blocking
Fail2ban monitors log files in real time and blocks IPs that exceed failed login thresholds. Even with SSH hardened, there are other services you'll run (Nginx, Docker apps, etc.), and Fail2ban protects all of them:
# Install on Ubuntu/Debian
apt update && apt install -y fail2ban
# Start and enable it
systemctl enable fail2ban
systemctl start fail2ban
The default configuration is conservative. I customize it slightly for SSH on our non-standard port. Create a local override file:
nano /etc/fail2ban/jail.local
[DEFAULT]
bantime = 3600 # Ban for 1 hour
findtime = 600 # Check last 10 minutes
maxretry = 4 # Ban after 4 failures
destemail = [email protected] # For alerts (optional)
sendername = Fail2ban
action = %(action_mwl)s # Ban + email + logs
[sshd]
enabled = true
port = 2847 # Our custom SSH port
logpath = /var/log/auth.log
maxretry = 3 # SSH is stricter: 3 failures
bantime = 7200 # SSH ban is longer: 2 hours
# Save and exit
Reload Fail2ban to apply the rules:
systemctl restart fail2ban
# Check that the jail is active
fail2ban-client status sshd
# You should see output like:
# Status for the jail sshd:
# |- Filter misuse
# | |- Currently failed: 0
# | |- Total failed: 0
# | `- File list: /var/log/auth.log
# |- Actions
# | |- Currently banned: 0
# | |- Total banned: 0
Fail2ban automatically blocks IPs that trigger its thresholds. You can manually check and unban IPs if needed:
# See currently banned IPs
fail2ban-client status sshd
# Manually unban an IP (if a legitimate user got locked out)
fail2ban-client set sshd unbanip 192.0.2.50
Step 3: UFW Firewall Configuration
UFW (Uncomplicated Firewall) is iptables made simple. It's the first line of defense—dropping traffic before it reaches any service. Most VPS providers have hardware firewalls too, but UFW gives you fine-grained control:
# Install and enable UFW
apt install -y ufw
ufw --version
# Set default policies: deny incoming, allow outgoing
ufw default deny incoming
ufw default allow outgoing
# Allow SSH (CRITICAL: do this before enabling UFW!)
ufw allow 2847/tcp
# If running services on the VPS, allow them too
# For example, if you have Nginx:
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS
# Optionally: allow access from only trusted IP ranges
# ufw allow from 203.0.113.0/24 to any port 2847 proto tcp
# Enable UFW (will prompt for confirmation)
ufw enable
# Check the ruleset
ufw status verbose
A typical output looks like:
Status: active
To Action From
-- ------ ----
2847/tcp ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
2847/tcp (v6) ALLOW Anywhere (v6)
80/tcp (v6) ALLOW Anywhere (v6)
443/tcp (v6) ALLOW Anywhere (v6)
If you need to add more rules later (for example, if you add a service on port 8080), use:
ufw allow 8080/tcp
ufw reload
Testing Your Hardening in Action
After all three layers are in place, you can see them work. Try logging in with the wrong key repeatedly from your local machine:
# This will fail 3 times, then Fail2ban bans your IP for 2 hours
ssh -i ~/.ssh/wrong_key -p 2847 sysadmin@your-vps-ip
ssh -i ~/.ssh/wrong_key -p 2847 sysadmin@your-vps-ip
ssh -i ~/.ssh/wrong_key -p 2847 sysadmin@your-vps-ip
ssh -i ~/.ssh/wrong_key -p 2847 sysadmin@your-vps-ip # Connection refused
On the VPS, check the logs to confirm:
tail -n 20 /var/log/auth.log | grep sshd
fail2ban-client status sshd
You'll see entries like "Attempt 1 from [IP]" followed by "Ban" after the threshold is exceeded. The UFW rules silently drop any traffic not explicitly allowed. SSH only listens on your custom port. The combination creates security through layers—an attacker would need to bypass the firewall, guess your non-standard port, break your Ed25519 key, survive Fail2ban's blocking, and somehow escalate from a non-root user.
Maintenance and Monitoring
Security isn't one-time setup. Review logs weekly and update your VPS regularly:
# Check for repeated attack patterns
grep "Failed password" /var/log/auth.log | wc -l
# See top attacking IPs
grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -10
# Keep the system patched
apt update && apt upgrade -y
apt autoremove -y
I add a monthly cron job to email me a summary of Fail2ban activity, and I rotate my SSH keys every 90 days. These small habits catch problems early.
Next Steps
Once SSH, Fail2ban, and UFW are locked down, your VPS is a defensible foundation. The next layer is securing the services you actually run on it—whether that's Docker containers, a reverse proxy, or a web application. Consider adding WireGuard VPN for additional access control, or deploying Authelia in front of web services for centralized authentication. With a hardened VPS as your base, you can run self-hosted applications with confidence that the infrastructure isn't compromised before your app even starts.
Discussion