# How to Secure an Ubuntu 24.04 Server: A Practical Hardening Checklist

> Harden an Ubuntu 24.04 web server step by step: SSH keys, the ssh.socket port change, UFW, automatic updates, Fail2ban, per-site isolation, backups and audits.

Source: https://hanoo.io/blog/secure-ubuntu-24-04-server-hardening · Published 2026-11-09 · Updated 2026-11-09 · By the Hanoo team

A fresh Ubuntu VPS starts getting SSH login attempts within minutes of booting. Most of them are bots trying `root` with a list of common passwords, and none of them will get in if the server is set up properly. Most breaches of small servers come from a few known causes: password logins, unpatched packages, services left open to the internet, and every site running as the same user.

This checklist hardens an **Ubuntu 24.04 LTS** server that hosts web applications such as Laravel, WordPress or any PHP app. Every step includes the commands, a way to check it worked, and the Ubuntu 24.04 quirks that older tutorials miss.

> **In short:** create a sudo user with an SSH key, turn off password and root login, open only ports 22, 80 and 443, turn on automatic security updates, keep the clock in sync, run every site as its own Linux user with its own PHP-FPM pool, keep databases and caches on localhost, and back up off the server. Then check it all again every month.

## Before you start: don't lock yourself out

Hardening SSH and the firewall is the one job where a typo can cost you the server. Two habits prevent that:

1. **Keep a second SSH session open** while you change SSH or firewall settings. A running session survives a restart of the SSH service. Use it to undo a mistake.
2. **Know where your provider's web console is.** DigitalOcean, Hetzner, AWS and the rest all offer a browser console that works even if SSH is broken.

Ubuntu 24.04 is supported with standard security updates until 2029, and until 2034 with Ubuntu Pro. Check what you are running with `lsb_release -a`.

## 1. Update everything first

```bash
sudo apt update && sudo apt full-upgrade -y
[ -f /var/run/reboot-required ] && echo "reboot needed" && cat /var/run/reboot-required.pkgs
```

If a reboot is needed, a kernel or core library was updated. Reboot now, before you change anything else, so that every step after this runs on the patched system.

## 2. Create a sudo user and use SSH keys

Don't do daily work as `root`. Create your own user, give it `sudo`, and copy your SSH key to it:

```bash
sudo adduser deploy
sudo usermod -aG sudo deploy
sudo rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
```

If you don't have a key yet, create one **on your own computer**, not on the server. Ed25519 keys are short and strong:

```bash
ssh-keygen -t ed25519 -C "you@laptop"
ssh-copy-id deploy@your-server-ip
```

Open a **new** terminal and check that `ssh deploy@your-server-ip` works and `sudo -v` asks for that user's password. Only then continue.

## 3. Harden the SSH server

Ubuntu 24.04 ships OpenSSH 9.6. Its main file `/etc/ssh/sshd_config` begins by including `/etc/ssh/sshd_config.d/*.conf`, and **sshd keeps the first value it reads for each setting**. That matters because many cloud images include `50-cloud-init.conf` with `PasswordAuthentication yes`. If you put your settings in a file whose name sorts before it, your settings win:

```bash
sudo nano /etc/ssh/sshd_config.d/00-hardening.conf
```

```sshd
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowUsers deploy
```

`AllowUsers` is optional but strong: only the users you list can log in over SSH. If an application needs its own SFTP login, add that user here too.

Test the configuration, then check what sshd will actually use:

```bash
sudo sshd -t
sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication|pubkeyauthentication|maxauthtries'
sudo systemctl restart ssh
```

`sshd -t` checks the syntax. `sshd -T` prints the settings sshd will really use after reading every included file. That is how you find a cloud-init file quietly turning password login back on.

### Changing the SSH port on Ubuntu 24.04

Moving SSH off port 22 cuts down log noise. It does not stop anyone who scans every port. If you do it, note that **Ubuntu 24.04 starts SSH through systemd's `ssh.socket`**. Changing `Port` and restarting `ssh.service` is not enough, and many people have locked themselves out this way. Allow the new port in the firewall first, then:

```bash
echo "Port 2222" | sudo tee /etc/ssh/sshd_config.d/10-port.conf
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
ss -tlnp | grep 2222
```

## 4. Turn on the firewall

A web server needs only three ports open to the world: SSH (22), HTTP (80) and HTTPS (443). Add UDP 443 if your web server speaks HTTP/3. Caddy does by default. UFW is the simplest way to set that up:

```bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw limit OpenSSH          # allow SSH, rate-limit repeated connection attempts
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp          # HTTP/3
sudo ufw enable
sudo ufw status verbose
```

If you changed the SSH port, run `sudo ufw limit 2222/tcp` instead of `OpenSSH`. If you prefer to write **nftables** rules directly, put them in `/etc/nftables.conf` and run `sudo systemctl enable --now nftables`. Pick one tool and use only that one, because two tools managing the same rules will fight each other.

Two things to know:

- **Your provider's cloud firewall is a second layer worth using.** It blocks traffic before it reaches the server, and it stays in place even if a local rule is removed by mistake.
- **Docker publishes ports around UFW.** A container started with `-p 6379:6379` is reachable from the internet even with UFW enabled. Bind published ports to localhost: `-p 127.0.0.1:6379:6379`.

## 5. Automatic security updates

Most attacks on servers use flaws that were patched long ago. `unattended-upgrades` installs security updates every day:

```bash
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
cat /etc/apt/apt.conf.d/20auto-upgrades
```

The last command should show both lines set to `"1"`:

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

By default, only packages from Ubuntu's security pocket are upgraded automatically. If you installed PHP from the Ondřej Surý PPA (see [installing PHP 8.5 on Ubuntu 24.04](/blog/install-php-8-5-ubuntu-24-04)), its updates are **not** included unless you add the PPA's origin under `Unattended-Upgrade::Origins-Pattern` in `/etc/apt/apt.conf.d/50unattended-upgrades`.

Kernel updates only take effect after a reboot. You can let the server reboot itself at a quiet hour:

```apt
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
```

If you'd rather reboot by hand, check `/var/run/reboot-required` every week. Ubuntu Pro, free for personal use on up to five machines, adds **Livepatch**, which applies many kernel security fixes without a reboot (`sudo pro attach` and then `sudo pro enable livepatch`).

## 6. Optional: Fail2ban

With password login off, brute-force attempts can't succeed, so Fail2ban is about less log noise and less load, not about stopping a break-in. If you want it:

```bash
sudo apt install fail2ban
sudo tee /etc/fail2ban/jail.d/sshd.local >/dev/null <<'EOF'
[sshd]
enabled = true
maxretry = 5
findtime = 10m
bantime = 1h
EOF
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
```

The first Fail2ban package in Ubuntu 24.04 failed to start under Python 3.12 (`No module named 'asynchat'`). The fixed package has been in `noble-updates` for a long time, so step 1 already installed it. If you still see that error, the server is missing updates.

## 7. Keep the clock in sync

TLS certificates, two-factor codes, log timelines and scheduled jobs all depend on the right time:

```bash
timedatectl
```

Look for `System clock synchronized: yes`. Ubuntu runs `systemd-timesyncd` by default, and some providers swap in `chrony`. Either one is fine, as long as one of them is running.

## 8. Isolate every website

This is the step most server guides skip, and it matters most when you host more than one site. If every site runs as `www-data`, one vulnerable WordPress plugin can read every other site's `.env` file and database password.

Give each application:

- its **own Linux user**, which owns its files;
- its **own PHP-FPM pool** running as that user, listening on its own socket;
- its **own database and database user**, with privileges on that one database only.

A per-site PHP-FPM pool looks like this, in `/etc/php/8.4/fpm/pool.d/shop.conf`:

```ini
[shop]
user = shop
group = shop
listen = /run/php/php8.4-fpm-shop.sock
listen.owner = caddy
listen.group = caddy
listen.mode = 0660
```

Also set `expose_php = Off` in `/etc/php/8.4/fpm/php.ini`, so PHP stops announcing its version in every response header.

Keep secrets out of the web root. Files such as `.env` and `wp-config.php` should be `640` or tighter and owned by the site's user. The web server should only serve the `public/` directory, never the project root.

## 9. Keep databases and caches off the internet

MariaDB, MySQL, PostgreSQL, Redis and Valkey should listen only on `127.0.0.1` unless another machine really needs them. Check what is listening on the server:

```bash
sudo ss -tlnp
```

Anything bound to `0.0.0.0` or `*` that isn't SSH or your web server needs a reason to be there. For Redis or Valkey, see [how to configure Valkey safely](/blog/valkey-vs-redis#configure-it-safely). Ubuntu's MariaDB package already binds to `127.0.0.1` (`bind-address` in `/etc/mysql/mariadb.conf.d/50-server.cnf`). If you need remote database access, allow it for **specific IP addresses** in both the database grants and the firewall, never for everyone.

Run `sudo mariadb-secure-installation` once on a new MariaDB server to remove anonymous users and the test database.

## 10. Backups are a security control

Ransomware, a bad deploy, and an `rm -rf` in the wrong directory are all fixed the same way: with a backup that is **off the server** and that you have **restored at least once**. Back up databases with `mariadb-dump --single-transaction` and application files to object storage in another account or region, encrypted. Then try a restore on a spare server. A backup you have never restored is only a hope.

## 11. Audit the result

Two free tools give you a second opinion:

```bash
sudo apt install lynis
sudo lynis audit system
```

Lynis scores the system and lists its suggestions in order. Don't chase a perfect score; read each suggestion and decide. On Ubuntu Pro machines, the **Ubuntu Security Guide** (`usg`) can audit against, and apply, the CIS benchmark profiles.

Repeat these checks every month:

```bash
sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication'
sudo ufw status
sudo ss -tlnp
ls /var/run/reboot-required 2>/dev/null
apt list --upgradable 2>/dev/null | grep -i security
```

## How Hanoo handles this

[Hanoo](/) manages Ubuntu servers you own, and it was built around the points above.

- **No inbound management port.** The Hanoo agent connects *out* to Hanoo over mutually authenticated TLS. You don't open an extra port or share a root password, and Hanoo never stores your SSH private keys. Your firewall only needs 22, 80 and 443.
- **Typed operations, no remote shell.** The agent accepts only signed, validated operations, such as "create this application" or "add this domain". It never runs arbitrary shell commands, and every change records who asked for it.
- **A Security tab for every server.** Hanoo checks the server and reports whether automatic security updates are on, whether the Ubuntu security repository is configured, whether the clock is synchronized, whether SSH allows root password login or password authentication at all, whether public-key login is on and whether an nftables firewall is active. It also shows how many security updates are pending and whether a reboot is required.
- **Isolation by default.** Every application gets its own Linux user, its own PHP-FPM pool and, if you want one, its own MariaDB database and user. Caddy refuses known sensitive files such as WordPress's `wp-config.php`.
- **Localhost by default.** Valkey installed through Hanoo listens on localhost only. Remote database access is off until you allow specific IP addresses.
- **PHP patched like the rest of the system.** Hanoo adds its PHP repository to unattended-upgrades, so PHP security releases arrive with Ubuntu's own updates.

You keep root access, and everything Hanoo sets up is standard Ubuntu, Caddy and PHP-FPM.

## Frequently asked questions

### What is the most important step to secure an Ubuntu server?

Turn off SSH password login and use SSH keys. Most automated attacks on a new VPS are password guesses over SSH, and key-only login stops all of them. After that, turn on automatic security updates and the firewall.

### Should I change the default SSH port?

It's optional. A different port cuts the number of bot attempts in your logs, but it does not add real security, since anyone scanning all ports will find it. On Ubuntu 24.04, changing the port also means running `systemctl daemon-reload` and restarting `ssh.socket`.

### Do I need Fail2ban if password login is disabled?

No, it's optional. With key-only SSH, brute-force attempts cannot succeed. Fail2ban still reduces log noise and blocks noisy scanners, so it is a reasonable extra, not a requirement.

### UFW or nftables on Ubuntu 24.04?

Either works. UFW is simpler for a typical web server that needs ports 22, 80 and 443. Writing nftables rules directly gives you more control. Use one tool only, so they don't overwrite each other's rules.

### Does unattended-upgrades reboot the server?

Not by default. It installs security updates and writes `/var/run/reboot-required` when a reboot is needed. You can let it reboot at a set time with `Unattended-Upgrade::Automatic-Reboot`, or use Livepatch to apply many kernel fixes without rebooting.

### How do I run several websites on one server safely?

Give each site its own Linux user, its own PHP-FPM pool running as that user, and its own database user with access to that one database only. Then a compromised site cannot read the files or data of the others.
