MHOSTMHOST

Basic VPS security checklist

A fresh VPS is set up for convenience, not defense, by default: root logs in with a password, the firewall is off, and there are no automatic updates. This checklist closes eight basic gaps in one pass — afterwards your server will be noticeably more resistant to the automated scanning and password guessing that never stops against any public IP address on the internet.

What you'll need

  • An mHost VPS on Ubuntu 22.04/24.04, Debian 11/12, or AlmaLinux/Rocky Linux 8/9.
  • SSH access as root — usually the only option on a brand-new server.
  • Access to the VMmanager 6 panel — as a safety net in case you make a mistake with the firewall or SSH and lose remote access: the panel has a browser-based server console that doesn't depend on SSH. If you haven't logged in yet, start with "How to log in to VMmanager 6".
  • An hour or so of free time: a few steps need to be verified with a new connection before you close the old one.
Important: work through the items in the order given. Get the SSH or firewall steps out of order and you can cut off your own access to the server — each such spot below has its own warning.

The checklist at a glance

In short — all eight items, each covered in detail below:

  • Update the system — install all available package updates.
  • Create a sudo user — stop working as root all the time.
  • Set up SSH — key-based login instead of a password, root login over SSH disabled.
  • Enable a firewall — ufw (Ubuntu/Debian) or firewalld (AlmaLinux/Rocky), deny everything by default.
  • Install fail2ban — automatic IP banning for repeated failed logins.
  • Close unused services and ports — fewer listening ports means a smaller attack surface.
  • Enable automatic security updates — unattended-upgrades (Ubuntu/Debian) or dnf-automatic (AlmaLinux/Rocky).
  • Change default passwords — the temporary root password and any passwords left over from VMmanager recipes.

Step 1. Update the system

Always start here — some vulnerabilities are closed simply by updating packages, and the rest of the checklist is better done on an up-to-date system.

Ubuntu / Debian

bash
sudo apt update
sudo apt upgrade -y

Check whether a reboot is needed (usually after a kernel update):

bash
[ -f /var/run/reboot-required ] && echo "Reboot needed" || echo "No reboot needed"
Tip: the /var/run/reboot-required file is created by the update-notifier-common package — it's installed by default on Ubuntu, and can be installed separately on Debian (sudo apt install update-notifier-common). If the package isn't there, just reboot the server after an update that touched the kernel.

AlmaLinux / Rocky

bash
sudo dnf update -y

Check whether a reboot is needed:

bash
sudo dnf install -y dnf-utils
needs-restarting -r
Tip: needs-restarting -r prints nothing and exits without an error if no reboot is needed; if one is needed, it prints Reboot is required.

Reboot the server if needed:

bash
sudo reboot
Done: the update completed without errors — the kernel and packages are at the latest available version.

Step 2. Create a sudo user instead of root

Working as root day to day is a risk: any mistake in a command runs with full privileges, and compromising the one account hands over the whole server. Set up a separate user with sudo and use it for everyday work.

Ubuntu / Debian

bash
sudo adduser admin
sudo usermod -aG sudo admin

AlmaLinux / Rocky

bash
sudo useradd -m -s /bin/bash admin
sudo passwd admin
sudo usermod -aG wheel admin

Feel free to replace admin with any other name — the rest of the article uses this one.

Verify the new user actually has root privileges:

bash
su - admin
sudo whoami

sudo whoami will ask for the admin user's password (not root's) and should print root.

Done: sudo whoami returned root — the admin user works fully through sudo.

Step 3. Set up SSH: keys and hardening

A password can be brute-forced — an SSH key technically can't be. Later in this step, root login over SSH gets disabled completely, so before continuing, make sure key-based login as the admin user actually works.

Generate a key pair on your own computer (not on the server):

bash
ssh-keygen -t ed25519 -C "you@example.com"

You can just press Enter at the path and passphrase prompts — the key will be saved as ~/.ssh/id_ed25519 (private) and ~/.ssh/id_ed25519.pub (public).

Copy the public key to the server, to the admin user:

bash
ssh-copy-id -i ~/.ssh/id_ed25519.pub admin@203.0.113.10

If ssh-copy-id isn't available (for example, in Windows PowerShell without the OpenSSH utilities), do the same thing manually:

bash
cat ~/.ssh/id_ed25519.pub | ssh admin@203.0.113.10 "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

Verify it: ssh admin@203.0.113.10 should now log you in without asking for a password.

Important: don't move on to the next step until key-based login is confirmed working — password authentication gets disabled next, and without the key you'll be locked out.

Disable password authentication and root login — on the server, edit /etc/ssh/sshd_config:

bash
PasswordAuthentication no
PermitRootLogin no
Tip: if you want to keep root reachable over SSH but only with a key, no password, set PermitRootLogin prohibit-password instead of no.

Check the configuration syntax before restarting — that's cheaper than fixing the server blind after a mistake:

bash
sudo sshd -t

Restart SSH:

bash
# Ubuntu/Debian
sudo systemctl restart ssh

# AlmaLinux/Rocky
sudo systemctl restart sshd

Without closing your current session, open a new terminal window and verify key-based login as admin — only close the old session once you've confirmed everything works.

Important: if something goes wrong and SSH becomes unreachable, the VMmanager 6 server console can get you back in — it opens right in the browser and doesn't depend on SSH (see "How to log in to VMmanager 6").
Done: password login and root login over SSH are disabled, and admin logs in with a key.

For deeper SSH protection — a non-standard port, AllowUsers, limiting login attempts — see "Hardening SSH".

Step 4. Enable a firewall: ufw or firewalld

A firewall with a "deny everything not needed" policy is basic protection against any service you never meant to expose.

Ubuntu / Debian (ufw)

bash
sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw enable
sudo ufw status verbose
Important: allow SSH (ufw allow 22/tcp) before running ufw enable — otherwise the very next new connection gets blocked, including your own. If you've changed the SSH port to something non-standard (see "Hardening SSH"), open that port instead of 22.

AlmaLinux / Rocky (firewalld)

bash
sudo systemctl enable --now firewalld
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --reload
sudo firewall-cmd --list-all
Tip: on most AlmaLinux/Rocky images the ssh service is already allowed in the default zone — adding it again doesn't break anything, it just makes sure.

For the full rundown of ufw commands — opening arbitrary ports, listing and deleting rules — see "The ufw firewall: managing ports".

Done: ufw status verbose / firewall-cmd --list-all show incoming traffic denied by default, with only SSH allowed.

Step 5. Install fail2ban

ufw and firewalld decide which ports are visible from outside, but they don't stop someone from guessing passwords against an SSH port that's already open. fail2ban watches the logs and adds a temporary ban itself for any IP that fails to log in too many times.

Ubuntu / Debian

bash
sudo apt install -y fail2ban

AlmaLinux / Rocky

fail2ban isn't in the standard repositories — enable EPEL first:

bash
sudo dnf install -y epel-release
sudo dnf install -y fail2ban fail2ban-systemd fail2ban-firewalld
Tip: the fail2ban-systemd and fail2ban-firewalld packages aren't optional here. On AlmaLinux/Rocky the sshd jail reads the systemd journal by default, not a log file — that needs the Python bindings from fail2ban-systemd. And fail2ban needs to ban IPs through firewalld specifically — the default firewall on these distributions — or a ban will show up in the status without actually stopping the traffic; fail2ban-firewalld sets that up.

Enable and start the service:

bash
sudo systemctl enable --now fail2ban

Better not to touch settings in jail.conf — the file can get overwritten on a package update. Create jail.local instead:

bash
sudo nano /etc/fail2ban/jail.local
ini
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1

[sshd]
enabled  = true
bantime  = 1h
findtime = 10m
maxretry = 5
  • enabled — the sshd jail is off by default in jail.conf (except on Debian/Ubuntu, where the package turns it on already via its own defaults-debian.conf); the line above turns it on explicitly and identically on every distro.
  • bantime / findtime / maxretry — how long to ban for, over what window failed attempts are counted, and how many attempts are allowed before a ban; these values are stricter than the defaults (bantime defaults to 10m; here it's an hour).
  • ignoreip — addresses that never get banned; add your own static IP here, space-separated, if you have one — otherwise you risk banning yourself.

Apply the configuration:

bash
sudo systemctl restart fail2ban

Check that the sshd jail is active:

bash
sudo fail2ban-client status
sudo fail2ban-client status sshd

To unban an IP address early:

bash
sudo fail2ban-client set sshd unbanip 198.51.100.23
Done: fail2ban-client status sshd shows the jail enabled, and you can unban any IP with a single command whenever you need to.

For a deeper look at filters, actions, and the recidive jail for repeat offenders, see "Fail2ban: protection against brute-force".

Step 6. Close unused services and ports

A firewall blocks ports from the outside, but an unnecessary listening service on the server is a risk on its own: a mistake in a firewall rule, a second network interface, or a tunnel — and whatever was supposedly closed turns out to be reachable. Fewer running services means less that can be broken into.

Check what's actually listening:

bash
sudo ss -tulpn
  • -t / -u — TCP and UDP;
  • -l — listening sockets only;
  • -p — which process holds the port (needs sudo, otherwise the process name is hidden);
  • -n — show ports and addresses as numbers, don't resolve names.

Check which services are enabled and running:

bash
systemctl list-units --type=service --state=running
systemctl list-unit-files --type=service --state=enabled

Common candidates to disable on a bare VPS with no desktop environment, if you don't use them: avahi-daemon, cups, rpcbind. If you're on Debian/Ubuntu without your own mail server, the local MTA (exim4 or postfix) usually listens only on 127.0.0.1 and isn't exposed — but it's still worth checking with ss.

Stop a service and disable its autostart:

bash
sudo systemctl disable --now <service>

If you don't need it at all, remove the package entirely:

bash
# Ubuntu/Debian
sudo apt purge <package>

# AlmaLinux/Rocky
sudo dnf remove <package>
Important: only disable what you're actually sure about. Don't touch ssh/sshd — you'll lose access to the server without it — or any service you actually use (a web server, a database, and so on).

Check the result with ss -tulpn again — the list of listening ports should be shorter and easier to account for.

Done: ss -tulpn shows only the ports you're intentionally using.

Step 7. Enable automatic security updates

Step 1 updated the system once; this step gets security updates installed by themselves, without you, from now on.

Ubuntu / Debian

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

dpkg-reconfigure creates /etc/apt/apt.conf.d/20auto-upgrades with this content:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

The main logic lives in /etc/apt/apt.conf.d/50unattended-upgrades: by default it installs only security updates, not package version upgrades.

Check that everything is configured without waiting for the real scheduled run:

bash
sudo unattended-upgrade -v --dry-run
Tip: if you want the server to reboot itself after updates that require one, uncomment Unattended-Upgrade::Automatic-Reboot "true"; and Unattended-Upgrade::Automatic-Reboot-Time "02:00"; in /etc/apt/apt.conf.d/50unattended-upgrades. Without this the server won't reboot on its own — don't forget to check Step 1 manually.

AlmaLinux / Rocky

bash
sudo dnf install -y dnf-automatic
sudo nano /etc/dnf/automatic.conf

In the [commands] section:

ini
[commands]
upgrade_type = security
apply_updates = yes

Enable the timer:

bash
sudo systemctl enable --now dnf-automatic.timer
Done: systemctl status dnf-automatic.timer (AlmaLinux/Rocky) or apt-config dump APT::Periodic::Unattended-Upgrade (Ubuntu/Debian) show that automatic updates are enabled.

Step 8. Change default passwords

The temporary root password mHost emailed you when the server was activated is the first thing worth changing, if you haven't already done it on first login:

bash
passwd

As the admin user with sudo, you can also change another user's password, including root's:

bash
sudo passwd root
Tip: now that root login over SSH is disabled (Step 3), the root password should still be long and unique — you'll still need it to log in through the local VNC console in VMmanager 6.

If you deployed any software through a ready-made VMmanager 6 recipe, some of them leave their own default password (for example, Zabbix: Admin/zabbix, Redmine: admin/admin) — change those too, right after your first login to the application's web interface. For the full list of recipes and their quirks, see "Ready-made VMmanager 6 recipes".

Done: the root password has been replaced with a new, unique one, and so have any default passwords for installed applications.

What's next

All eight checklist items are done — the server has basic security in place. From here it's worth going deeper on each topic: "The ufw firewall: managing ports" — the full rundown of ufw commands; "Hardening SSH" — a non-standard port, restricting users and login attempts; "Fail2ban: protection against brute-force" — filters and actions for other services, not just SSH.