VPS security on Ubuntu and Debian: a hardening checklist (SSH, UFW, Fail2ban)

VPS security on Ubuntu and Debian — hardening checklist Security
A step-by-step hardening checklist for a VPS/VDS on Ubuntu 22.04–26.04 and Debian 12/13: SSH keys, no password or root login, UFW, Fail2ban, auto-updates and regaining access.

VPS security starts in the first 15 minutes after the server is created: bots start guessing SSH passwords almost immediately. This checklist covers the minimum hardening for a VPS or VDS on Ubuntu and Debian — updates, a separate user, SSH key login, no passwords and no root over SSH, the UFW firewall and Fail2ban — in an order that never locks you out of your own server.

Commands are checked on Ubuntu 22.04, 24.04, 26.04 and Debian 12, 13. Where the systems differ (SSH socket activation in newer Ubuntu, journald-only logs in Debian 12), it is called out explicitly. Updated in September 2026.

VPS security: quick checklist

  1. Update the system and enable automatic security updates.
  2. Create a sudo user instead of working as root.
  3. Set up SSH login with an ed25519 key.
  4. Test the key login in a new window — only then disable passwords and root login.
  5. Enable the UFW firewall after allowing SSH.
  6. Install Fail2ban against brute-force attempts.
  7. Optionally change the SSH port.
  8. Set up backups and check ports, logs and updates from time to time.
VPS security: hardening order on Ubuntu and Debian without getting locked out
Hardening order: key login and a test first, then restrictions and the firewall

Before you start: how not to lock yourself out

  • Keep your current SSH session open until you have tested a new login in another window. An established session survives sshd restarts and enabling the firewall — it is your safety net.
  • Find the console in your provider panel (VNC, “Console”, “Emergency console”). It bypasses SSH and the firewall, so you can undo anything from there.
  • Take a snapshot of the server in the panel if your provider supports it.
  • The commands below are run as root. If you are already a regular user, prefix them with sudo.

General security rules

VPS security is not only about server settings. Most servers get compromised through leaked passwords for the provider panel or email, not through clever exploits.

  • Do not keep your provider panel password in a text file on the desktop or in notes. Use an open-source password manager such as KeePassXC or Bitwarden. If you store passwords in the browser, set a master password.
  • Use long, unique passwords — a different one for every service.
  • Harden the email account linked to your hosting account: it can be used to recover access to everything else.
  • Enable two-factor authentication for your email and the provider panel. Prefer an authenticator app (TOTP) over SMS, which can be hijacked with a SIM swap. I use Aegis.
  • Do not save passwords in FTP/SFTP clients: malware routinely harvests them.
  • Never share the main account. If someone else needs access, create a separate user with their own SSH key and remove it when it is no longer needed.
  • Pick a reliable provider. I use Timeweb Cloud.

System updates and automatic security updates

Install all updates first, then enable unattended-upgrades so security fixes are applied daily. Ubuntu usually ships it preinstalled; on Debian you need to install it.

apt update && apt full-upgrade -y
apt install -y unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades   # answer "Yes"

# check whether a reboot is needed after a kernel update
[ -f /var/run/reboot-required ] && echo "Reboot required"

By default only security updates are installed automatically, and the server does not reboot on its own. If /var/run/reboot-required appears after a kernel update, reboot at a convenient time. See the Ubuntu documentation and the Debian Wiki.

A separate sudo user

Working as root all the time is risky: one typo can be irreversible, and root is the first login bots try. Create a regular user and grant it sudo. The examples use admin; a less obvious name is better.

apt install -y sudo          # minimal Debian may not ship sudo
adduser admin                # set a strong password: you need it for sudo and the console
usermod -aG sudo admin

# if your provider already added your key for root, copy it to the new user
rsync --archive --chown=admin:admin ~/.ssh /home/admin

Save the user password in your password manager: even after SSH password login is disabled you still need it for sudo and the provider console.

SSH key login

An SSH key is stronger than any password: it cannot be brute-forced, and the private part never leaves your computer. The modern default is an ed25519 key: shorter and faster than RSA and supported by every current client and server.

Generate a key on your computer

The key is generated on your computer, not on the server. The private key is never copied anywhere — only the public key goes to the server.

# on your own computer: Linux, macOS or PowerShell on Windows 10/11
ssh-keygen -t ed25519 -C "admin@my-vps"

Accept the default path and set a passphrase — it protects the key if the file is stolen. Two files appear in ~/.ssh:

  • id_ed25519 — the private key. Never share it or upload it to servers; keep a backup in a safe place.
  • id_ed25519.pub — the public key. This is what you add to the server.

If you use PuTTY, generate the key in PuTTYgen (type EdDSA / Ed25519) and paste the public key line into ~/.ssh/authorized_keys on the server. Many providers also let you add a public key when creating the VPS — it then ends up with root, and you can copy it to your user with the rsync command from the previous step.

Copy the key to the server

# Linux and macOS
ssh-copy-id -i ~/.ssh/id_ed25519.pub admin@SERVER_IP

# Windows (PowerShell) has no ssh-copy-id, so use this
type $env:USERPROFILE.sshid_ed25519.pub | ssh admin@SERVER_IP "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

These commands create ~/.ssh/authorized_keys with the correct permissions (700 for the directory, 600 for the file). With looser permissions sshd ignores the key.

Test the key login

Open a new terminal window and log in as the new user. You should not be asked for the SSH password (only for the key passphrase, if you set one):

ssh admin@SERVER_IP
sudo -v   # asks for the admin password, so sudo works

Do not move on until this login works.

SSH config: no passwords, no root login

Put your settings in a separate file under /etc/ssh/sshd_config.d/ instead of editing the main sshd_config: package updates will not overwrite it, and it is easy to roll back. One important detail: OpenSSH uses the first value it finds, and files in sshd_config.d are read alphabetically before the main config. A file prefixed with 00- therefore wins over 50-cloud-init.conf, which Ubuntu cloud images often use to set PasswordAuthentication yes.

sudo nano /etc/ssh/sshd_config.d/00-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
PermitEmptyPasswords no
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowUsers admin
  • PermitRootLogin no — root cannot log in over SSH. If you need root with a key (for Ansible or backup scripts), use prohibit-password.
  • PasswordAuthentication no and KbdInteractiveAuthentication no — keys only, no passwords.
  • MaxAuthTries 3 and LoginGraceTime 30 — fewer attempts and less time per connection.
  • AllowUsers admin — only this user may log in over SSH. Separate several names with spaces.

Validate the config and restart SSH:

# syntax check: no output means no errors
sudo sshd -t

# effective values the server will actually use
sudo sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|allowusers)'

sudo systemctl restart ssh

Then immediately test it in a new window:

# in a NEW terminal window: key login must work
ssh admin@SERVER_IP

# and password login must fail with Permission denied (publickey)
ssh -o PubkeyAuthentication=no admin@SERVER_IP

If older guides made you add PubkeyAcceptedAlgorithms +ssh-rsa or RhostsRSAAuthentication, remove them. The first re-enables the legacy RSA/SHA-1 signature, and the second was removed from OpenSSH long ago. With an ed25519 key you need neither.

UFW firewall

UFW is a simple front end for iptables and nftables. The policy: deny all incoming connections and open only what you really need. Allow SSH before running ufw enable, or the firewall will cut you off.

sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw limit 22/tcp comment 'SSH'
# only if the server hosts a website or a panel:
sudo ufw allow 80,443/tcp comment 'Web'
sudo ufw enable
sudo ufw status verbose

ufw limit works like allow but blocks an address that opens more than 6 connections in 30 seconds — extra brute-force protection. With a static IP you can allow SSH from that address only:

# SSH only from your static IP (instead of limit 22/tcp)
sudo ufw allow from 203.0.113.10 to any port 22 proto tcp comment 'SSH from office'

Docker bypasses UFW rules

Ports published by Docker (-p 8080:80 or ports: in compose) are opened by Docker’s own iptables rules, bypassing UFW, and ufw status will not show them. Publish internal ports on localhost only (127.0.0.1:8080:80) and expose them through a reverse proxy, or use ufw-docker. For a Docker-based example, see Matrix Synapse with Ansible and Docker.

Fail2ban: stopping brute-force attempts

Fail2ban watches logs and temporarily bans addresses with failed logins. With key-only SSH nobody can guess their way in anyway, but Fail2ban cuts log noise and bot load. Put your settings in a separate file under jail.d; do not edit the stock jail.conf or defaults-debian.conf, as updates overwrite them.

sudo apt install -y fail2ban
sudo nano /etc/fail2ban/jail.d/sshd.local
[DEFAULT]
bantime = 1h
bantime.increment = true
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 ::1

[sshd]
enabled = true
port = ssh
# Debian 12: uncomment if fail2ban reports
# "Have not found any log file for sshd jail"
# backend = systemd
  • bantime = 1h — how long an address stays banned; bantime.increment makes repeat offenders stay banned longer.
  • findtime = 10m and maxretry = 5 — ban after 5 failures within 10 minutes.
  • ignoreip — addresses that are never banned. Add your static IP (space-separated) so you do not ban yourself.
  • port — if you changed the SSH port, put it here (for example port = 2222).

Debian 12 has no /var/log/auth.log by default (logs go to the systemd journal), so Fail2ban may fail to start — that is what the commented backend = systemd line is for. Debian 13 already uses this backend by default, and Ubuntu works as is.

sudo systemctl enable fail2ban
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd

# unban an address if you banned yourself
sudo fail2ban-client set sshd unbanip 203.0.113.10

Changing the SSH port (optional)

A non-standard port does not stop a targeted attack, but it removes almost all automated scanning from your logs. If SSH is already key-only and UFW and Fail2ban are set up, it is optional. The key is the order: open the new port, test the login, and only then close port 22.

# 1. Open the new port in the firewall first
sudo ufw limit 2222/tcp comment 'SSH'

# 2. Set the port for sshd
echo 'Port 2222' | sudo tee /etc/ssh/sshd_config.d/10-port.conf
sudo sshd -t

# 3. Find out how SSH is started: active means ssh.socket
systemctl is-active ssh.socket

On Ubuntu 24.04 and 26.04 SSH is socket-activated: systemd listens on the port via ssh.socket, not sshd. Setting Port alone is not enough — you also need to override the socket. The empty ListenStream= line is required: it clears the default port 22.

# Ubuntu 24.04 / 26.04 (ssh.socket is active)
sudo mkdir -p /etc/systemd/system/ssh.socket.d
printf '[Socket]nListenStream=nListenStream=2222n' | sudo tee /etc/systemd/system/ssh.socket.d/port.conf
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket

# Ubuntu 22.04, Debian 12/13 (ssh.socket is not used)
sudo systemctl restart ssh
sudo ss -tlnp | grep -E ':(22|2222) '
# in a new window
ssh -p 2222 admin@SERVER_IP

# once port 2222 works, close port 22
sudo ufw delete limit 22/tcp

Remember to set the new port in Fail2ban (port = 2222) and restart it.

Backups

No setting protects you from an admin mistake, a compromised application or a provider outage. At a minimum, take regular snapshots in the provider panel. Better, also keep data backups off the server and with another provider — for example with restic or BorgBackup — and test a restore at least once.

Regular checks

Once a week or a month, take a quick look for anything unexpected:

# which services are listening
sudo ss -tulpn

# firewall rules and banned addresses
sudo ufw status numbered
sudo fail2ban-client status sshd

# who logged in over SSH and from where
sudo journalctl -u ssh -S today | grep -E 'Accepted|Failed'
last -a | head

# pending updates
apt list --upgradable

Unknown services on public addresses (not 127.0.0.1), logins from unfamiliar IPs or disabled automatic updates are worth investigating right away. For websites a control panel helps — for example Virtualmin, which puts the firewall, updates and backups in one interface.

If you lost access to the server

Open the VNC console in your provider panel and log in as admin or root with a password — SSH restrictions and the firewall do not apply to the console. Then roll back the changes and figure out what went wrong:

# in the provider's VNC console as admin or root
sudo ufw disable
sudo rm -f /etc/ssh/sshd_config.d/00-hardening.conf /etc/ssh/sshd_config.d/10-port.conf
sudo rm -f /etc/systemd/system/ssh.socket.d/port.conf && sudo systemctl daemon-reload
sudo systemctl restart ssh.socket 2>/dev/null; sudo systemctl restart ssh
sudo fail2ban-client unban --all

If the root password is not set or forgotten, boot the server into rescue mode from the provider panel, mount the disk and fix the files under /etc/ssh/.

Feedback

This checklist keeps growing — share your tips and questions in the comments.

FAQ

Where should I start with VPS security?

Start with updates and a separate sudo user, then set up SSH key login, test it in a new window, and only then disable password and root login. Next, enable UFW after allowing SSH, and install Fail2ban. The full order is in the quick checklist at the top.

Should I change the default SSH port?

It is optional. With key-only login nobody can guess their way in, and a different port mostly removes bot noise from the logs. If you change it, open the new port in UFW first, test the login, and only then close port 22.

Why does SSH still listen on port 22 after changing Port in sshd_config on Ubuntu 24.04?

Ubuntu 24.04 and 26.04 use socket activation, so systemd owns the port via ssh.socket. Create /etc/systemd/system/ssh.socket.d/port.conf with an empty u003ccodeu003eListenStream=u003c/codeu003e line and the new port, then run u003ccodeu003esystemctl daemon-reloadu003c/codeu003e and u003ccodeu003esystemctl restart ssh.socketu003c/codeu003e.

Why does PasswordAuthentication no not disable password login?

OpenSSH uses the first value it finds. On Ubuntu cloud images /etc/ssh/sshd_config.d/50-cloud-init.conf enables passwords before the main config is read. Put your settings in 00-hardening.conf in the same directory and verify with u003ccodeu003esshd -T | grep passwordauthenticationu003c/codeu003e.

Fail2ban does not start on Debian 12: Have not found any log file for sshd jail

Debian 12 has no /var/log/auth.log because logs go to the systemd journal. Add u003ccodeu003ebackend = systemdu003c/codeu003e to the [sshd] section of /etc/fail2ban/jail.d/sshd.local and restart Fail2ban. Debian 13 already does this by default.

Which SSH key type should I use: RSA or ed25519?

Ed25519: it is shorter, faster and supported by all current OpenSSH versions. Use RSA only for very old systems, and then at least 3072 bits.

Do I need Fail2ban if SSH is key-only?

Not strictly, but it helps: it bans bots, reduces log noise and load, and can also protect other services such as mail, web panels and FTP.

Docker opens ports bypassing UFW — what can I do?

Docker inserts its own iptables rules ahead of UFW. Publish ports on 127.0.0.1 only and expose them via a reverse proxy, or use ufw-docker.

What if I locked myself out of SSH?

Log in through the VNC console in your provider panel — neither SSH settings nor the firewall apply there. Disable UFW, remove your files from /etc/ssh/sshd_config.d/ and restart SSH. If you do not remember the password, use rescue mode.

Category: Security
Subscribe to new posts (RSS)

No email, no trackers — just the update feed.

Also available in Russian

Rate this article
Leave a comment