5 min read

Hardening a Linux VPS: SSH Keys, UFW, and Fail2ban

#Security#DevOps#Linux#Self-Hosting

Deploying self-hosted tools—whether Caddy with 9Router, Dokploy, or Telegram AI assistants—exposes ports to the public internet. Within minutes of provisioning a public IP, automated vulnerability scanners will begin probing your SSH port. Run lastb on any newly spun up machine, and you will see thousands of unauthorized login attempts.

Before running more production workloads, lock down the fundamentals. This guide covers creating a non-root sudo user, enforcing SSH key-only authentication, setting up an uncomplicated firewall (ufw), and configuring fail2ban. These straightforward steps elevate server security from vulnerable to resilient in under fifteen minutes.

Assumes an Ubuntu or Debian server where you currently connect as root via password. If your hosting provider already provisions SSH keys upon deployment, you can skip key generation.

1. Creating a Dedicated Non-Root User

Operating as root for day-to-day administration is hazardous: a single typographic error can be catastrophic, and automated attack bots target the predictable root username first.

Log in to the VPS:

ssh root@your-vps-ip

Create a new user account and add it to the sudo administrative group:

adduser lucky
usermod -aG sudo lucky

Verify that the new user can invoke administrative commands:

su - lucky
sudo whoami   # Should output: root

Do not close your initial root SSH terminal yet. Keep it open as a safeguard while confirming remote access for the new account.

2. Transitioning to SSH Key Authentication

Passwords are vulnerable to brute-force dictionaries; cryptographic keys are not. Perform this step from your local machine, not the remote server.

If you don't already have an Ed25519 key pair, generate one:

ssh-keygen -t ed25519 -C "lucky@laptop"

Accept the default storage path (~/.ssh/id_ed25519) and supply a secure passphrase. Next, copy your public key to the remote user's authorized keys list:

ssh-copy-id lucky@your-vps-ip

Open a new terminal window on your local machine and test logging in:

ssh lucky@your-vps-ip

If you authenticate cleanly without being prompted for the Linux account password, your key is configured properly.

3. Disabling Root Login and Password Authentication

With public-key authentication confirmed, eliminate the two most common brute-force vectors: direct root logins and password-based SSH authentication.

Edit the SSH daemon configuration:

sudo nano /etc/ssh/sshd_config

Verify or update the following directives:

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes

On newer Ubuntu releases, drop-in configuration snippets in /etc/ssh/sshd_config.d/ can override the main file. Double-check for conflicting directives:

sudo grep -r PasswordAuthentication /etc/ssh/sshd_config.d/

If any file contains PasswordAuthentication yes, change it to no. Restart the SSH service to apply changes:

sudo systemctl restart ssh

Crucial Rule: Keep your active session open! Open a separate terminal and test ssh lucky@your-vps-ip. Only close the existing session after confirming that your key authenticates successfully.

4. Configuring the UFW Firewall

Restrict inbound network traffic to only essential ports: SSH, HTTP, and HTTPS. ufw (Uncomplicated Firewall) provides a clean interface over iptables:

sudo apt update && sudo apt install ufw -y

Establish default traffic policies: deny all unsolicited incoming traffic, allow outbound connections, and explicitly permit necessary ports:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

Enable the firewall and check status:

sudo ufw enable
sudo ufw status verbose

Ensure sudo ufw allow OpenSSH is executed before enabling UFW. Failing to do so will sever your active SSH connection and lock you out.

Note for Docker users: Docker automatically bypasses UFW by injecting rules directly into iptables chains. If binding container ports to the host, bind them specifically to loopback (e.g., 127.0.0.1:3000:3000) and route public requests through a reverse proxy such as Caddy or Traefik.

5. Mitigating Brute-Force Attacks with Fail2ban

While UFW blocks unneeded ports, port 22 must remain reachable. fail2ban inspects system authentication logs and dynamically bans IP addresses exhibiting repetitive authentication failures.

Install fail2ban:

sudo apt install fail2ban -y

Create a local override file to preserve custom rules across package upgrades:

sudo nano /etc/fail2ban/jail.local

Define a protective jail for SSH:

[sshd]
enabled = true
maxretry = 3
findtime = 10m
bantime = 1h

This configuration triggers a 1-hour ban if an IP generates 3 failed login attempts within a 10-minute window. Apply the rules by restarting the daemon:

sudo systemctl restart fail2ban

Inspect active bans at any time:

sudo fail2ban-client status sshd

6. Enabling Automated Security Updates

Keep core packages patched against zero-day exploits by enabling unattended security updates:

sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades

Select Yes when prompted. The operating system will now download and apply critical security patches in the background automatically.

Summary

In less than fifteen minutes, your VPS has transitioned from an exposed target to a hardened server:

  • Root login is disabled.
  • Password authentication is eliminated in favor of Ed25519 keys.
  • Unnecessary ports are closed behind UFW.
  • Automated brute-force scanners are neutralized by Fail2ban.
  • Security patches apply automatically.

A hardened foundation allows you to deploy applications with peace of mind.