Securing a Linux server: UFW, SSH, and Fail2Ban
0. Create a non-root user
Don’t do your day-to-day work as root. One bad command wipes the box, and every process you start runs with unlimited power. You want a normal user that borrows root’s rights only when it needs them, through sudo. Skip this step if you already have one.
Logged in as root, create the user (name it whatever you want, I’ll use user):
adduser user
adduser asks you to set a password as it runs. That password is also your sudo password, so make it a strong one. You can press Enter through the name and phone prompts.
Add the user to the sudo group so it’s allowed to run admin commands:
usermod -aG sudo user
Now log out of root and back in as the new user, so the rest of this guide runs under the account you’ll actually be using:
exit
ssh user@your_server_ip
Confirm sudo works before you go any further:
sudo whoami
Type the password you just set, and it should print root. From here on, you’re this user.
1. Generate and copy SSH keys
Password logins are the single biggest risk on a fresh server. Keys are far safer, but you have to set them up before you disable passwords, or you’ll lock yourself out of your own box.
Step 1: Generate a key pair
On your local machine (not the server), generate an Ed25519 key:
ssh-keygen -t ed25519 -C "you@example.com"
(Press Enter to accept the default location, and set a passphrase so a stolen laptop doesn’t hand over your key).
Ed25519 is the right default. The signatures are smaller, verification is faster, and the curve has no known timing side-channel weaknesses. Fall back to rsa -b 4096 only for an old system that can’t handle Ed25519.
Step 2: Copy the public key to the VPS
Send your new public key to your server:
ssh-copy-id user@your_server_ip
(Swap user with your server username and your_server_ip with the IP address. You’ll need to type your server password one last time).
Test it by running ssh user@your_server_ip. If it logs you in without asking for a password, you’re good to go.
2. Configure the firewall (UFW)
Raw iptables rules are a headache. The Uncomplicated Firewall (UFW) makes it simple to block everything except the ports you actually need: SSH (22), HTTP (80), and HTTPS (443).
sudo apt install ufw -y
sudo ufw limit 22
sudo ufw allow 80
sudo ufw allow 443
sudo ufw enable
sudo ufw status
limit on port 22 drops any IP that hits SSH more than six times in 30 seconds, which stops the dumbest brute-force scripts before Fail2Ban even gets involved.
Leave UFW logging on. It’s noisy, but it’s the only record of who’s been probing your ports, which is worth having on a box you’re trying to secure. If the volume bothers you, turn it down with sudo ufw logging low rather than off.
3. Harden the SSH daemon
Now that your keys work, turn off password logins and shut the root account out over SSH.
Warning: Double-check that your key logs you in and that your user can run
sudobefore you touch this. If you get it wrong and close your only session, you’re locked out.
Edit the SSH daemon configuration:
sudo nano /etc/ssh/sshd_config
Set these values (add the lines if they’re missing):
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
UsePAM yes
A few of these need explaining:
PermitRootLogin noblocks direct root logins. Root is the first account bots try, so this matters as much as the password setting.KbdInteractiveAuthentication nocloses the keyboard-interactive path, which can still trigger a password prompt through PAM even withPasswordAuthenticationoff. (It’s the modern name forChallengeResponseAuthentication, which OpenSSH removed in 8.7.)UsePAM yesstays on. With passwords already off, PAM accepts no password logins anyway, so disabling it gains nothing and breaks session and resource management likepam_systemdandpam_limits.
Check the config for typos before you restart anything:
sudo sshd -t
If that prints nothing, the syntax is good. Restart the service:
sudo systemctl restart ssh
(On RHEL and its relatives the service is sshd, so use sudo systemctl restart sshd there).
Don’t close your terminal yet. Open a new terminal and confirm you can still log in. If something’s wrong, your original session is still open to fix it.
4. Setup Fail2Ban
Even with keys and a firewall, bots will still hammer your SSH port. Fail2Ban watches your logs and automatically blocks IPs that fail to log in too many times.
Install Fail2Ban
sudo apt update
sudo apt install fail2ban -y
Create a local config file
Don’t edit the default jail.conf file. Package updates will overwrite it. Copy it to a .local file instead:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Customize settings
Open your new config file:
sudo nano /etc/fail2ban/jail.local
Find the [DEFAULT] section. You probably want to whitelist your own IP so you don’t accidentally ban yourself:
[DEFAULT]
# Whitelist your own IP addresses. Separate with spaces.
ignoreip = 127.0.0.1/8 ::1 192.168.1.100
# Ban time in seconds (1h = 3600)
bantime = 3600
# The time window for counting failures
findtime = 600
# Number of failures before a ban
maxretry = 5
Enable the SSH jail
Scroll down to the [sshd] section in that same file. Enable it, and tell it to read from the systemd journal:
[sshd]
enabled = true
port = ssh
backend = systemd
That backend = systemd line matters on Ubuntu 24.04, Debian 12, and anything else recent. These systems stopped writing SSH failures to /var/log/auth.log and send them to the journal instead. If you leave the old file-based backend in place, Fail2Ban starts up, finds no log file, and fails with Have not found any log file for sshd jail, so it bans nothing while looking perfectly healthy. Some distro packages already default to the journal, but setting it yourself removes the guesswork.
Save and exit.
Restart and verify
Restart Fail2Ban to apply the changes:
sudo systemctl restart fail2ban
Check that the jail is alive and reading the journal:
sudo fail2ban-client status sshd
You want to see a line like Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd. If instead you see an empty file list or a path that doesn’t exist, the backend is wrong and the jail isn’t watching anything. Go back and fix it.
5. Turn on automatic security updates
Keys, a firewall, and Fail2Ban all guard the front door. None of them help if the server is running a version of OpenSSH or OpenSSL with a public exploit. Unattended upgrades patch the security holes for you while you sleep:
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades
By default it only pulls from the security pocket, so it won’t surprise you with risky feature updates. It’s the cheapest security win on this whole list.
That’s the baseline. None of this makes you bulletproof, but it takes you from “default config a script can walk into” to “annoying enough that the bots move on to an easier target.” That’s most of what server security is in practice.