To secure a new Linux VPS, spend its first 15 minutes on nine steps: update every package, create a sudo user, log in with an SSH key, turn off root and password logins, enable the UFW firewall, confirm automatic security updates, check the clock, close unused services and snapshot the clean result. This VPS security checklist is written for Ubuntu 24.04 LTS, uses commands from the Ubuntu Server and OpenSSH documentation, and is ordered so that every new way in is tested before the old one is removed.
It also covers three traps most guides skip: cloud-init’s 50-cloud-init.conf, which can keep password logins on; SSH socket activation, which changes how a port change works on Ubuntu 24.04; and Docker, which publishes container ports around UFW.
Key takeaways
- Never remove a way in before the new one works: test key login before turning passwords off, allow OpenSSH before enabling UFW, and keep the first session open until a second one succeeds.
- Put SSH hardening in /etc/ssh/sshd_config.d/00-hardening.conf: sshd keeps the first value it reads, so a 00- file beats cloud-init's 50-cloud-init.conf.
- On Ubuntu 24.04, unattended-upgrades already installs security updates daily, and changing the SSH port needs systemctl daemon-reload plus a restart of ssh.socket.
- Ports published by Docker bypass UFW, so bind them to 127.0.0.1 unless they must be public.
- Give your sudo user a strong password and turn on 2FA for the portal: the VNC console is your way back in after a lockout.
The VPS security checklist at a glance
Replace alex with your user name and 203.0.113.10 with your server’s IP address in every command below.
| # | Step | Main command | What it prevents | Check with |
|---|---|---|---|---|
| 1 | Update every package | sudo apt update && sudo apt upgrade | Attacks on bugs that already have a fix | ls /var/ |
| 2 | Create a sudo user | sudo adduser alex sudo | Daily work as root, where one typo is system-wide | sudo whoami |
| 3 | Give that user your SSH key | ssh-copy-id alex@ | Password guessing | Key login from a second terminal |
| 4 | Turn off root and password logins | /etc/ | Brute force against root and passwords | sudo sshd -T |
| 5 | Enable the firewall | sudo ufw allow OpenSSH then sudo ufw enable | Services you exposed by accident | sudo ufw status verbose |
| 6 | Confirm automatic security updates | unattended-upgrades (on by default) | Unpatched holes between your logins | sudo unattended-upgrade -v --dry-run |
| 7 | Check time sync | timedatectl status | TLS, 2FA and log errors from a drifting clock | “System clock synchronized: yes” |
| 8 | Close unused services | sudo ss -tulpn | Forgotten listeners on public addresses | Only ports you meant to open |
| 9 | Snapshot, then plan backups | Snapshot in the portal | A bad change you cannot undo | Snapshot listed; a backup stored elsewhere |
The rule behind the order: add the new way in (a user, a key, a firewall rule), test it from a second terminal, then remove the old one (root, passwords). If something still breaks, the VNC console is a local login that SSH and firewall settings do not touch, and the recovery section shows how to use it.
Before you start: keep a way back in
Three things make this checklist safe to run on a live server:
- An SSH key on your own computer. If you do not have one, run
ssh-keygen -t ed25519locally. Ubuntu’s OpenSSH guide recommends Ed25519 for its shorter keys and lower computational cost. Our guide to connecting to a VPS over SSH covers clients on Windows, macOS and Linux. - A console you can reach. Every HourlyVPS server has a VNC console in the portal. It works when SSH or the firewall is misconfigured, but it needs a password for a local account. That is why the sudo user in step 2 gets a strong password, even though you will never use it over SSH.
- Two-factor authentication on your portal account. Whoever controls the portal controls the console, reinstalls and snapshots, so protect that login like root.
Warning: Do not close your first SSH session until you have logged in from a second terminal as the new user with your key. An open session stays connected through an SSH reload, so it is your safety rope while you change the configuration.
Practice first: You can rehearse this checklist on a throwaway server before you touch one that matters. A Quartz Q1 (1 GB RAM, which matches Ubuntu’s documented minimum for 24.04 cloud images) costs $0.01/hour, so a one-hour run costs $0.01 on an hourly VPS. A stopped server is still billed, because its vCPU, memory, disk and IP addresses stay reserved for you; only deleting the server stops billing. The hourly billing guide explains the details. Deploy a Quartz Q1 hourly
Steps 1–3: Update, create a sudo user and add your SSH key
1. Update every package
Log in with what you chose at deploy: the SSH key you added or the initial root password. Steps 1 and 2 run as root or as the image’s first user; from step 3 on, you work as your own user. Refresh the package lists and install every pending update:
sudo apt update && sudo apt upgrade
A server image is frozen on the day it was built, so the first upgrade catches up on everything released since. On Ubuntu 24.04, needrestart restarts the services that use updated libraries automatically by default. A new kernel still needs a reboot, and Ubuntu signals that with a flag file:
ls /var/run/reboot-required
If the file exists, reboot now, while nothing important runs on the server, and log back in when it is up:
sudo reboot
2. Create a sudo user
Ubuntu’s model is a normal user who borrows root rights through sudo with their own password, which also leaves a record of who did what. Create the user and set a strong password when prompted:
sudo adduser alex
Add it to the sudo group, which Ubuntu authorizes in /etc/sudoers:
sudo adduser alex sudo
Store the password in a password manager. You need it for sudo and for the VNC console, never for SSH.
3. Give the new user your SSH key
If the server still accepts passwords, run ssh-copy-id on your own computer. It appends your public key to ~/.ssh/authorized_keys on the server:
ssh-copy-id [email protected]
If you deployed with a key and the server already refuses passwords, copy the keys of the account you are logged in with to the new user instead. Run these two commands on the server. They create the directory and the file with the owner and permissions that sshd expects, where only the user can write:
sudo install -d -m 700 -o alex -g alex /home/alex/.ssh
sudo install -m 600 -o alex -g alex ~/.ssh/authorized_keys /home/alex/.ssh/authorized_keys
Copy from ~, the account that already works for you, not from /root when you logged in as another user. On cloud images, cloud-init’s disable_root option puts a forced command in front of every key in root’s file. That command prints “Please login as the user…” and disconnects, and a copied line keeps it.
Now open a second terminal on your computer and log in as the new user. You should be asked for your key’s passphrase (if it has one), not for the account password:
ssh [email protected]
In that new session, confirm that sudo works. The command should print root:
sudo whoami
Tip: If your public key is already on GitHub or Launchpad, Ubuntu can fetch it for the current user with ssh-import-id gh:yourname (or lp:yourname).
Step 4: How do you disable root login and password authentication on Ubuntu 24.04?
Put the settings in your own drop-in file under /etc/ssh/sshd_config.d/, not at the bottom of /etc/ssh/sshd_config. Ubuntu’s main file includes that directory at the very top, and sshd keeps the first value it reads for most settings:
Because OpenSSH uses the first value set for most directives, any settings defined in files within sshd_config.d/ will override those in the main configuration file.
Ubuntu Server documentation, OpenSSH server
The sshd_config(5) manual adds the second half of the rule: files matched by the Include line are processed in lexical order. A setting in 00-hardening.conf therefore beats the same setting in 50-cloud-init.conf, and an edit at the end of sshd_config loses to both.
Why does SSH still accept passwords after you disabled them?
On cloud images, cloud-init writes SSH settings to /etc/ssh/sshd_config.d/50-cloud-init.conf. When the image or installer enables password logins, that file contains PasswordAuthentication yes, which quietly overrides a no in the main file (Launchpad bug 2088207). Some guides name their override 60- or 99- so it loads after cloud-init’s file; under sshd’s first-value rule that is backwards, because your file has to sort first.
See what is already in the directory before you add anything:
ls /etc/ssh/sshd_config.d/
sudo grep -riE 'passwordauthentication|permitrootlogin' /etc/ssh/sshd_config.d/
Create the drop-in, test it, reload
Create the file with one command. Its name must end in .conf, because the Include line only matches *.conf:
sudo tee /etc/ssh/sshd_config.d/00-hardening.conf > /dev/null <<'EOF'
# Local hardening. Files here load in lexical order; sshd keeps the first value.
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
EOF
PermitRootLogin no: OpenSSH’s default,prohibit-password, still lets root in with a key.noblocks root over SSH completely.PasswordAuthentication no: the OpenSSH default isyes.KbdInteractiveAuthentication no: Ubuntu’ssshd_configalready sets this, but cloud-init’s documentation warns that PAM keyboard-interactive logins can still take passwords when it isyes. Stating it here keeps the file self-contained.
Test the syntax, then reload. sshd -t prints nothing when the configuration is valid, and && only runs the reload if the test passed:
sudo sshd -t && sudo systemctl reload ssh
Ubuntu’s ssh.service also runs sshd -t before every reload, and your open session stays connected. Ubuntu’s documentation uses sudo systemctl restart ssh.service instead, which works as well.
Prove it from the outside
Check the effective configuration, after every file has been merged:
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication) '
All three lines should end in no. Then, from your computer, try the two logins that must now fail:
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password [email protected]
ssh [email protected]
Both should stop at Permission denied (publickey). A password prompt means another file still sets PasswordAuthentication yes first. When ssh [email protected] still works with your key, you can close the first session.
Note: Three optional clean-ups once sudo works for alex, each removing a way in that you no longer use:
- If your provider set a root password, lock it with
sudo passwd -l root. Stock Ubuntu ships root that way. - On stock Ubuntu cloud images, cloud-init creates a default user,
ubuntu, with a locked password and passwordless sudo. If you don’t need it, close its sessions and runsudo deluser --remove-home ubuntufrom youralexsession. - To allow SSH only for named accounts, follow Ubuntu’s user-management guide: create an
sshlogingroup, add your users to it, and only then addAllowGroups sshloginto your drop-in.
Step 5: Turn on the UFW firewall without locking yourself out
UFW is Ubuntu’s default firewall tool, and it ships disabled. Per the ufw manual, it starts with incoming traffic denied and outgoing traffic allowed, and it filters IPv6 too (IPV6=yes in /etc/default/ufw). The one rule you must add before enabling it is SSH:
sudo ufw allow OpenSSH
sudo ufw enable
OpenSSH is an application profile from the openssh-server package that opens 22/tcp (check with sudo ufw app info OpenSSH). UFW then warns Command may disrupt existing ssh connections and asks you to confirm; with the rule already in place, answer y. In the reverse order, new SSH connections are dropped; your open session usually survives, but if it drops, only the console is left.
sudo ufw status verbose
Look for Status: active, defaults of deny (incoming) and allow (outgoing), and an ALLOW rule for OpenSSH on IPv4 and on IPv6 (the (v6) line). Open other ports only when a service needs them. A web server needs 80/tcp and 443/tcp, as in our Caddy reverse proxy guide:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Keep IPv6: Some older guides tell you to disable it. UFW already filters IPv6 by default. Every server comes with one dedicated IPv4 address and IPv6, included in the plan price.
The DDoS mitigation on our network and a host firewall do different jobs: the first absorbs floods before they reach the server, the second decides which ports on it answer at all.
Docker publishes ports around UFW
If you plan to run containers, read this before you publish a port. Docker’s documentation is explicit:
When you publish a container’s ports using Docker, traffic to and from that container gets diverted before it goes through the ufw firewall settings.
Docker Docs, Packet filtering and firewalls
So -p 5432:5432 exposes a database to the internet even when UFW has no rule for 5432. Bind published ports to localhost, as Docker’s port-publishing docs show, and put a reverse proxy in front of anything that must be public:
docker run -p 127.0.0.1:8080:80 -p '[::1]:8080:80' nginx
Use Docker Engine 28.0 or later. The same page notes that earlier versions let hosts on the same layer-2 network segment reach ports published to localhost. Our guide to installing Docker on a VPS builds this setup step by step. To open a localhost-only port from your own computer, use SSH local port forwarding instead of publishing it.
Steps 6–9: Updates, clock, services and snapshots
6. Confirm automatic security updates
On Ubuntu 24.04 there is nothing to install: unattended-upgrades ships by default and applies security updates once a day. Confirm that it is switched on:
cat /etc/apt/apt.conf.d/20auto-upgrades
Both lines, APT::Periodic::Update-Package-Lists and APT::Periodic::Unattended-Upgrade, should be "1" (daily). A dry run shows what the next run would install; real runs log to /var/log/unattended-upgrades/:
sudo unattended-upgrade -v --dry-run
Two defaults matter later. First, only Ubuntu’s own repositories are allowed, so packages from a repository you add yourself, such as Docker’s, are not updated automatically until you add its origin. Second, automatic reboots are off. If a short nightly restart is acceptable, set these two options in /etc/apt/apt.conf.d/50unattended-upgrades so kernel updates take effect:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
The time uses the server’s local time zone.
7. Check the clock
TLS certificate checks, two-factor codes and log timelines all assume a correct clock. Ubuntu 24.04 keeps time with systemd-timesyncd. Ubuntu’s time-sync guide notes that chrony replaced it from 25.10 on. Check it:
timedatectl status
You want System clock synchronized: yes and NTP service: active. If the service is inactive, switch it on:
sudo timedatectl set-ntp true
8. Close what you don’t use
Every listening port is one more thing to patch and watch. List the sockets that accept connections and the processes behind them:
sudo ss -tulpn
Read the Local Address column: 0.0.0.0, [::] or * means every interface, reachable from the internet unless UFW blocks it, while 127.0.0.1, [::1] and the DNS stub on 127.0.0.53 are local only. On Ubuntu 24.04, port 22 may list systemd next to sshd, because with socket activation systemd opens the listening socket. Anything else on a public address needs a reason to be there.
Ubuntu’s security suggestions list removing packages you do not need as the way to shrink the attack surface. Stop and remove anything you did not install on purpose:
sudo systemctl disable --now SERVICE_NAME
sudo apt purge PACKAGE_NAME
9. Snapshot the clean state and plan real backups
Take a snapshot in the portal now. It is your rollback point for the next risky change: a bad upgrade, a broken config, a lockout you cannot fix from the console.
A snapshot is not a backup. It lives on the same platform as the server, and our security page says it plainly: keep backups outside HourlyVPS. Copy the data you care about to storage you control elsewhere, on a schedule, and test one restore; our VPS backup guide with restic sets that up with a timer and a restore drill. Before you ever delete a server, work through our checklist for deleting a VPS, because a deleted server’s disk cannot be recovered.
Do you need fail2ban, a different SSH port or more?
With password logins off, none of these is required, and each one has a cost. This is when they earn their place:
| Measure | What it does | Worth it when | Ubuntu 24.04 detail |
|---|---|---|---|
| fail2ban | Bans addresses after repeated failures in the logs | You also run services with password logins, or want quieter logs | The package enables the sshd jail, reads the systemd journal and bans with nftables |
ufw limit | Denies an address that opens 6 or more connections within 30 seconds | You want light throttling with no extra daemon | Can trip scripts that open many SSH connections at once |
| A new SSH port | Moves SSH off port 22 | You want less noise in the logs | Needs daemon-reload and a restart of ssh. |
| SSH only over a VPN | Hides SSH from the internet | You can always reach the VPN, with the console as fallback | See our WireGuard on a VPS guide |
| Ubuntu Pro Livepatch | Applies high and critical kernel fixes without an immediate reboot | Uptime matters more than a nightly reboot | Ubuntu Pro is free for personal and business use on up to 5 machines, per Ubuntu’s docs |
fail2ban on Ubuntu 24.04
Against key-only SSH, fail2ban mostly quiets the logs. If you want it anyway, Ubuntu’s package works out of the box: its defaults-debian.conf sets backend = systemd and banaction = nftables and enables the sshd jail.
sudo apt install fail2ban
sudo fail2ban-client status sshd
Fail2ban crashed on Ubuntu 24.04 at release because Python 3.12 removed the asynchat module (Launchpad bug 2055114); version 1.0.2-3ubuntu0.1 fixed it and reached noble-updates in June 2024, so the upgrade in step 1 covers you. Bans live in nftables, not UFW, so ufw status does not list them. To lift one:
sudo fail2ban-client set sshd unbanip 198.51.100.7
Rate-limit SSH with UFW instead
Per the ufw manual, a limit rule denies an address that tries to open 6 or more connections within 30 seconds. One command is enough: when an allow rule for the same profile exists, UFW replaces it in place and prints Rule updated:
sudo ufw limit OpenSSH
sudo ufw status should now show LIMIT for OpenSSH on the IPv4 and (v6) lines. If a deploy script trips it, SSH connection multiplexing (ControlMaster in ~/.ssh/config, covered in Ubuntu’s OpenSSH guide) reuses one connection.
Changing the SSH port on Ubuntu 24.04
A non-standard port cuts automated noise in your logs but does not stop a full port scan. Ubuntu 24.04 starts SSH through systemd socket activation, so editing Port and restarting ssh.service is not enough: Ubuntu’s own sshd_config says to run systemctl daemon-reload and restart ssh.socket. Open the new port in UFW first:
sudo ufw allow 2222/tcp
echo 'Port 2222' | sudo tee -a /etc/ssh/sshd_config.d/00-hardening.conf
sudo sshd -t && sudo systemctl daemon-reload && sudo systemctl restart ssh.socket
The generator that builds ssh.socket reads files in sshd_config.d too, as the package’s README.Debian says. If systemctl is-enabled ssh.socket prints disabled, your image runs sshd without socket activation, so sudo systemctl restart ssh applies the new port instead.
Then connect from a second terminal with ssh -p 2222 [email protected]. Only after that works, remove the old rule with sudo ufw delete allow OpenSSH (or sudo ufw delete limit OpenSSH if you rate-limited it). If you run fail2ban, set port = 2222 in the [sshd] section of /etc/fail2ban/jail.local as well.
Advice you can skip
Protocol 2in sshd_config. OpenSSH 7.4 removed server support for the old SSH-1 protocol in December 2016 (OpenSSH release notes), and Ubuntu 24.04 ships OpenSSH 9.6, so the line changes nothing.UsePAM no. Some checklists add it to stop PAM password prompts, butKbdInteractiveAuthentication noalready does that. Persshd_config(5),UsePAMalso runs PAM’s account and session modules for every login, and Ubuntu’s package sets it toyes.- Disabling IPv6. UFW filters it by default; turning it off only removes connectivity.
- Editing the end of sshd_config. Drop-ins in
sshd_config.dwin, as step 4 shows.
Verify your VPS security in two minutes
Run this block on any server you build or inherit. Each line maps to a step above:
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication) '
sudo ufw status verbose
cat /etc/apt/apt.conf.d/20auto-upgrades
timedatectl status
sudo ss -tulpn
ls /var/run/reboot-required
Then, from your computer, check the two logins that must fail and one port you never opened. Test only servers you own: probing other people’s systems without written permission breaks our Acceptable Use Policy.
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password [email protected]
ssh [email protected]
nc -zv 203.0.113.10 5432
| Check | Healthy result | If not |
|---|---|---|
sshd -T | Three lines, each ending in no | Another drop-in sorts before yours, or your file does not end in .conf (step 4) |
ufw status verbose | Status: active, deny (incoming), OpenSSH allowed on IPv4 and (v6) | Add the SSH rule, then enable (step 5) |
20auto-upgrades | Both values "1" | Set both to "1" (step 6) |
timedatectl status | System clock synchronized: yes | sudo timedatectl set-ntp true (step 7) |
ss -tulpn | Public addresses only for services you meant to expose | Disable or purge the service (step 8) |
ls /var/ | No such file or directory | sudo reboot (step 1) |
| Password or root SSH login | Permission denied (publickey). | Recheck the drop-in (step 4) |
nc -zv to a port you never opened | The connection times out or is refused | Find the listener; check Docker port bindings (step 5) |
Locked out? Recover through the VNC console
A lockout on a VPS is an inconvenience, not a disaster, as long as you can reach the console.
- Open your server’s VNC console in the HourlyVPS portal.
- Log in as your sudo user with its password. The console is a local login, so sshd settings and UFW rules do not apply to it. Some browser consoles do not pass clipboard paste, so keep fixes short.
- Match the symptom below, apply the fix, and test SSH from a second terminal before you close the console.
| Symptom | Likely cause | Fix from the console |
|---|---|---|
Connection timed out | UFW is on without an SSH rule, or a new port is not allowed | sudo ufw allow OpenSSH (or sudo ufw allow 2222/) |
Connection refused | sshd is not listening on that port: Port changed without regenerating the socket | sudo systemctl daemon-reload && sudo systemctl restart ssh. |
Permission denied (publickey). for your own user | Key missing from authorized_, or wrong owner or permissions | Repeat step 3; read sudo journalctl -u ssh -n 50 |
| A password prompt still appears | Another drop-in sets PasswordAuthentication yes first | Name yours 00-hardening.; confirm with sudo sshd -T |
alex is not in the sudoers file | The group change in step 2 is missing | As root at the console, if root has a password: adduser alex sudo; otherwise restore or reinstall |
REMOTE HOST IDENTIFICATION HAS CHANGED after a reinstall | The reinstalled server has new host keys | On your computer: ssh-keygen -R 203., only when you know you reinstalled |
sudo sshd -t prints Missing privilege separation directory: /run/ | ssh. has not run since boot; with socket activation it starts on the first connection | sudo mkdir -p /run/, then run the test again |
| Refused again after several failed logins | fail2ban banned your address | sudo fail2ban-client set sshd unbanip YOUR_ |
To roll back step 4 completely, move the drop-in aside and restart SSH. A restart works even when ssh.service is not running yet, and it leaves open sessions alone:
sudo mv /etc/ssh/sshd_config.d/00-hardening.conf /root/ && sudo systemctl restart ssh
If the file also set a new Port, run sudo systemctl daemon-reload && sudo systemctl restart ssh.socket as well.
If no account can log in at the console, restore the snapshot from step 9 or reinstall the OS from the portal; on a server that is minutes old, a reinstall is the quickest fix. If you suspect a break-in rather than a typo, rebuild from a clean image and tell us in a ticket; compromised servers are how IP addresses end up on blocklists, and our acceptable use policy explains how we isolate a compromised server while you regain control.
What changes on Debian 12 and 13?
The same order works on Debian, with five differences. For a new server, choose Debian 13 (trixie). Debian 12 left regular security support on June 11, 2026, and Debian LTS covers it until June 30, 2028.
| Topic | Ubuntu 24.04 LTS | Debian 12 and 13 |
|---|---|---|
| UFW | Installed, off until you enable it | Not in Debian’s official cloud images: sudo apt install ufw first |
| SSH listener | Socket activation: a port change needs daemon-reload and a restart of ssh. | ssh.; the socket unit ships but is off: sudo systemctl restart ssh |
| fail2ban | Works as installed: systemd backend, nftables bans | Debian 12: the cloud image has no rsyslog, so add backend = systemd under [sshd] in /etc/. Debian 13: systemd backend by default, but its nftables bans need the nftables package, which the cloud image lacks: sudo apt install fail2ban nftables |
| unattended-upgrades | Installed and on | In the cloud images; on other installs it may be missing or off: install it, then run sudo dpkg-reconfigure unattended-upgrades |
| Service restarts after updates | needrestart restarts affected services automatically | No needrestart in the cloud images: reboot after a large upgrade, or install it |
The drop-in rule is identical: Debian’s openssh-server package also includes /etc/ssh/sshd_config.d/*.conf at the top of sshd_config, so 00-hardening.conf works there too.
To rehearse SSH keys and UFW from a blank server, Lab 5 of our Linux labs walks through both on a throwaway VPS. When a practice server becomes one you keep running, it needs no plan change: it keeps billing by the hour and stops at the plan’s monthly price each billing period (one month from your order date), as the monthly VPS page explains. Compare sizes on the pricing page; the Quartz Q1 plan page lists the smallest.
Deploy this setup
Practice this security checklist on a fresh Ubuntu 24.04 server
Quartz Q1 · 1 shared vCPU · 1 GB RAM · 25 GB NVMe · Istanbul
- Per hour$0.01/hourFor this job
- Per day (24 h)$0.24/day
- Monthly cap$5.00/month
Starts with a $5 initial credit, which goes into the server’s balance and pays for its hours.
Billed by the hour, never more than $5.00 per billing period. Delete the server and billing stops.



