How to secure an Ubuntu server: a checklist for Ubuntu 24.04
To secure an Ubuntu server, sign in with SSH keys through a sudo user, turn off password and root login, set UFW to deny incoming traffic except SSH, HTTP and HTTPS, add fail2ban, keep unattended-upgrades on, check time sync, remove services you don’t use, and keep tested off-server backups and monitoring.
Key takeaways
- Put SSH settings in a file under
/etc/ssh/sshd_config.d/. sshd uses the first value it reads, so check the result withsudo sshd -T. - Keep your current SSH session open until a new login works. It’s your way back in.
- Ports published by Docker bypass UFW. Keep internal services on
127.0.0.1. - Ubuntu Server installs security updates daily by default, but it doesn’t reboot unless you tell it to.
On this page
- Ubuntu server hardening checklist at a glance
- Create a sudo user and log in with SSH keys
- SSH hardening: turn off password and root login
- Set up UFW with a default deny policy
- Install fail2ban to block repeated SSH logins
- Turn on unattended-upgrades for security updates
- Check time sync and remove services you don’t need
- Backups and monitoring: the steps people skip
- What Nuvlo sets up for you by default
Ubuntu server hardening checklist at a glance
A new VPS is reachable from the whole internet within seconds of booting, and bots start trying SSH passwords almost right away. Most break-ins on small servers aren’t clever. They use a weak password, an unpatched package, or a service that was never meant to be public.
This checklist closes those doors on Ubuntu 24.04 LTS. Each step takes a few minutes. Do them in order, because the early steps keep you from locking yourself out in the later ones.
| Step | What you do | What it stops |
|---|---|---|
| 1. Patch | apt full-upgrade once, then leave unattended-upgrades on | Known vulnerabilities in installed packages |
| 2. Sudo user | A personal account with your SSH key | Everyday work as root |
| 3. SSH | Keys only, no password or root login | Password guessing and stolen passwords |
| 4. Firewall | UFW denies incoming by default; allow 22, 80, 443 | Services exposed by accident |
| 5. fail2ban | Ban addresses after repeated failed logins | Brute-force noise and load |
| 6. Time and services | Check time sync, remove what you don’t use | Broken certificates and logs, extra attack surface |
| 7. Backups and monitoring | Off-server backups, uptime and resource alerts | Losing data, or noticing problems late |
Start by bringing the server up to date. Fresh images are often weeks behind.
sudo apt update && sudo apt full-upgrade -y
[ -f /var/run/reboot-required ] && sudo rebootCreate a sudo user and log in with SSH keys
Many providers give you a root login on a new server. Work as a normal user instead and use sudo when you need it. Mistakes are smaller, and every privileged command is logged with your name.
If you don’t have a key on your computer yet, create one there. Ed25519 is what Ubuntu’s OpenSSH server guide recommends.
ssh-keygen -t ed25519 -C "you@laptop"Then, logged in to the server as root, create the user, give it sudo rights and copy root’s authorized keys to it:
adduser deploy
usermod -aG sudo deploy
install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
install -m 600 -o deploy -g deploy /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keysIf root has no key on the server and password login is still on, run ssh-copy-id deploy@203.0.113.10 from your computer instead. Some images, such as Ubuntu on AWS, already come with a sudo user called ubuntu; use that one.
Open a second terminal and check that the new account works, including sudo:
ssh deploy@203.0.113.10
sudo whoami # prints rootSSH hardening: turn off password and root login
With keys working, switch off the other ways in. On Ubuntu, /etc/ssh/sshd_config includes every file in /etc/ssh/sshd_config.d/ at the top, and sshd keeps the first value it reads for each setting. Cloud images often ship a 50-cloud-init.conf there that sets PasswordAuthentication, sometimes to yes. A file whose name sorts earlier wins, so call yours 00-hardening.conf.
sudo tee /etc/ssh/sshd_config.d/00-hardening.conf > /dev/null <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
EOF
sudo sshd -t && sudo systemctl restart ssh.service
sudo sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication) 'sshd -t checks the configuration for errors before the restart. sshd -T prints the settings sshd actually uses, which is the only reliable way to see that no other file overrides yours. All three lines should end in no.
For one more layer, add AllowUsers deploy to the same file so only that account can sign in over SSH. Add any other people or deploy accounts to the list first.
ImportantDon’t close the session you’re in. From the second terminal, check that ssh deploy@your-server still works and that ssh root@your-server is refused. If something is wrong, fix it from the open session. Your provider’s web console is the fallback if both are lost.
Set up UFW with a default deny policy
UFW is Ubuntu’s firewall front end. It’s installed but off by default, according to Ubuntu’s firewall guide. Allow SSH before you turn it on, or the new rule set cuts off your session.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verboseIf you always connect from a fixed IP address, you can narrow SSH to it with sudo ufw allow proto tcp from 203.0.113.5 to any port 22, then remove the open rule with sudo ufw delete allow OpenSSH. Leave it open if your address changes, or you’ll need the provider console to get back in.
ImportantDocker doesn’t respect UFW. Its firewall documentation explains that traffic to published container ports is routed before UFW’s rules run. Publish only what must be public, and bind the rest to localhost, for example 127.0.0.1:5432:5432.
Most cloud providers also offer a firewall in their console that filters traffic before it reaches the server. Using both is fine; just remember to open a port in both places.
Install fail2ban to block repeated SSH logins
fail2ban reads the SSH log and bans addresses that fail to log in too often. With passwords off, nobody can guess their way in anyway, so think of it as noise reduction: fewer junk connections, shorter logs, less load.
sudo apt install -y fail2ban
sudo tee /etc/fail2ban/jail.local > /dev/null <<'EOF'
[sshd]
enabled = true
maxretry = 5
findtime = 10m
bantime = 1h
EOF
sudo systemctl restart fail2ban
sudo fail2ban-client status sshdPut your own settings in jail.local, never in jail.conf, which package updates replace. If you have a static home or office IP, add it as ignoreip = 127.0.0.1/8 ::1 203.0.113.5 under [sshd] so a few typos can’t ban you. To lift a ban: sudo fail2ban-client set sshd unbanip 203.0.113.5.
Turn on unattended-upgrades for security updates
On Ubuntu Server, unattended-upgrades is installed by default and installs security updates once a day. Ubuntu’s automatic updates guide describes the files involved. Check that it’s really on:
cat /etc/apt/apt.conf.d/20auto-upgrades
# APT::Periodic::Update-Package-Lists "1";
# APT::Periodic::Unattended-Upgrade "1";
sudo unattended-upgrade -v --dry-runBoth values should be "1". If the package is missing, install it with sudo apt install unattended-upgrades. Each run is logged in /var/log/unattended-upgrades/.
Three things it doesn’t do on its own:
- Reboot. Kernel and some library updates need a restart, flagged by the file
/var/run/reboot-required. To reboot automatically at a quiet hour, setUnattended-Upgrade::Automatic-Reboot "true";andUnattended-Upgrade::Automatic-Reboot-Time "04:00";in/etc/apt/apt.conf.d/50unattended-upgrades. - Update third-party repositories. Only Ubuntu’s archive (and ESM with Ubuntu Pro) is covered. Packages from Docker’s repository or a PPA need their own entry or regular manual updates.
- Update containers. Docker images and apps you installed yourself need their own update routine.
Since Ubuntu 24.04, needrestart restarts services that use updated libraries automatically after an upgrade. That’s usually what you want, but expect the occasional short restart of a service such as nginx.
Check time sync and remove services you don’t need
A server with the wrong time breaks HTTPS certificates, two-factor codes and log timelines. Ubuntu 24.04 syncs the clock with systemd-timesyncd (newer releases from 25.10 use chrony). See Ubuntu’s time sync guide.
timedatectl
# System clock synchronized: yes
# NTP service: activeThen look at what is listening on the network and what is running. Every open port is something you have to keep patched.
sudo ss -tulpn
systemctl list-units --type=service --state=runningAnything listening on 0.0.0.0 or [::] is reachable from outside unless the firewall blocks it. Databases and caches such as MySQL, PostgreSQL and Redis should listen on 127.0.0.1 only. Remove packages you don’t use with sudo apt purge, then sudo apt autoremove.
Backups and monitoring: the steps people skip
Hardening lowers the odds of a break-in. Backups decide how bad a bad day gets, whether the cause is an attacker, a failed disk or a mistyped command.
- Keep backups off the server. Dump databases (
mysqldump,pg_dump) and archive app files on a schedule, then copy them to object storage at another provider or region. - Use provider snapshots as a second layer, not the only one. They’re quick to restore, but they live in the same account as the server.
- Test a restore. Restore to a spare server once, so you know the steps and the backup is complete.
- Watch from outside. An uptime check that runs elsewhere tells you when a site is down, and alerts on disk and memory warn you before they run out.
- Read the logs now and then.
journalctl -u ssh.service --since today,last -a | headandsudo fail2ban-client status sshdshow who connected and who was banned.
If you run apps in Docker on the server, the same rules apply to their volumes. Our guide to self-hosting n8n shows what a complete app backup looks like in practice.
What Nuvlo sets up for you by default
Nuvlo is a control panel that creates and manages servers in your own cloud account, such as Hetzner, DigitalOcean or AWS. Most of this checklist is part of how every Nuvlo server is built, as described on the Security page:
- Key-only SSH. Password logins are off on every server Nuvlo builds. You choose your saved SSH keys when you create a server.
- Default-deny firewall. UFW with only SSH, HTTP and HTTPS open. You add allow or deny rules per port and IP address or range from the dashboard; see Set up firewall rules.
- fail2ban blocks addresses that keep failing SSH logins, and its status shows on the server’s Services card.
- Automatic security updates with unattended upgrades, plus fleet rollouts that update up to 100 servers at once and reboot only those that need it.
- Malware scans for WordPress sites and Node.js apps, with automatic quarantine; see Scan your sites.
- Monitoring and backups: server metrics every minute, uptime checks for every site every 5 minutes with alerts, and scheduled site backups streamed to your own S3-compatible bucket. See Monitoring.
One difference from the checklist: Nuvlo manages servers over SSH as root on port 22, with its own key. Root login by key stays allowed, passwords stay off, and Nuvlo refuses a firewall rule that blocks port 22, since that would cut it off from deploys, backups and updates.
To bring an existing Ubuntu 22.04, 24.04 or 26.04 server, connect it with one command. It installs UFW, fail2ban and automatic security updates. It turns off password login only if you logged in with a key, and keeps a copy of every setting it changes in /root/nuvlo-before-<date>.tar.gz.
When Nuvlo is the wrong fit: if your security policy requires PermitRootLogin no everywhere, or SSH reachable only through a VPN or bastion host, Nuvlo’s root-over-SSH model won’t pass. The same goes for servers that must not run any third-party management agent. For everything else, the Free plan covers 1 server; see pricing.



