VPS Hardening: SSH, Fail2ban, and UFW Configuration

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
Watch out: Test your connection before restarting SSH. If you lock yourself out by mistake, most VPS providers offer a console fallback, but it's slower. Open a second terminal and keep your current SSH session alive while testing.

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
Tip: Always allow SSH before enabling UFW, or you'll lock yourself out. The firewall applies instantly and won't warn you. If you mess up, use your provider's console to fix it without SSH access.

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