GuidesGuide

How to Secure a VPS for Web Hosting on Ubuntu 24.04

A fresh VPS ships with defaults meant to work, not to withstand the bots that start knocking within minutes. Securing an Ubuntu 24.04 VPS that hosts websites comes down to a short list: key-only SSH, a firewall that exposes only the ports you need, fail2ban, automatic security updates, a separate Linux user and PHP-FPM pool per site, malware scanning and off-server backups. Below are the exact commands to do it by hand, followed by what Koapanel already does for you out of the box.

Where the real attacks come from

Hosting servers rarely fall to exotic kernel exploits. The usual suspects are:

  • weak passwords on SSH, the control panel or wp-admin, hammered by automated bots;
  • vulnerable plugins and themes (WordPress above all) that let an attacker drop a web shell;
  • no isolation between sites: when every site runs as the same user, one compromised site can read and rewrite all the others.

Each step below closes one of these doors. Order matters: access first, then network, then sites.

1. SSH: keys only, no password logins

On your own machine, create a key if you don't have one and copy it to the server:

ssh-keygen -t ed25519 -C "you@laptop"
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@SERVER-IP

If you still work as root, create an admin user:

sudo adduser deploy
sudo usermod -aG sudo deploy

Put your settings in a drop-in file. On Ubuntu 24.04, /etc/ssh/sshd_config includes /etc/ssh/sshd_config.d/*.conf at the very top, and in sshd the first value read wins. A file named 00-... therefore beats 50-cloud-init.conf, which on many cloud images turns password logins back on.

sudo nano /etc/ssh/sshd_config.d/00-hardening.conf
PermitRootLogin prohibit-password
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no

Test the syntax and restart the service (on Ubuntu it's called ssh, not sshd):

sudo sshd -t && sudo systemctl restart ssh

Keep your current session open and log in from a second terminal before closing it. If you also want a non-standard port, note that Ubuntu 24.04 uses socket activation for SSH: after changing Port you need sudo systemctl daemon-reload and sudo systemctl restart ssh.socket. A different port cuts log noise; it is not a security control. Keys are.

2. Firewall with ufw

ufw ships with Ubuntu. Allow only SSH, web traffic and, if you run one, your panel:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw limit OpenSSH
sudo ufw allow 80,443/tcp
sudo ufw enable
sudo ufw status verbose

limit temporarily blocks an address that opens 6 or more connections within 30 seconds, a cheap first filter against brute force. Never expose MariaDB to the internet: on Ubuntu it already listens only on 127.0.0.1 (bind-address in /etc/mysql/mariadb.conf.d/50-server.cnf). Check what is listening with:

sudo ss -tulpn

One caveat: if you run Docker, published container ports bypass ufw rules, so audit them separately.

3. fail2ban for repeated login failures

sudo apt update && sudo apt upgrade
sudo apt install fail2ban

Upgrade first: the fail2ban build that shipped with Ubuntu 24.04 had a Python 3.12 bug that was fixed in updates. Don't edit jail.conf; create /etc/fail2ban/jail.local instead:

[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
bantime.increment = true
ignoreip = 127.0.0.1/8 ::1 203.0.113.10
[sshd]
enabled = true

Replace 203.0.113.10 with your office IP, then:

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

To unban an address: sudo fail2ban-client set sshd unbanip 198.51.100.7.

4. Automatic security updates

unattended-upgrades is normally preinstalled on Ubuntu Server. Make sure it's on:

sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
sudo unattended-upgrade --dry-run --debug

Tune it in /etc/apt/apt.conf.d/50unattended-upgrades. For a production hosting box, the sensible default is: security updates install themselves, reboots stay manual (Unattended-Upgrade::Automatic-Reboot "false";). To see whether a reboot is pending:

cat /var/run/reboot-required 2>/dev/null || echo "No reboot required"

5. One Linux user and one PHP-FPM pool per site

This is the step that keeps one hacked site from becoming a hacked server. Create a system user with no shell:

sudo useradd --system --user-group --home-dir /var/www/example.com --shell /usr/sbin/nologin site_example
sudo mkdir -p /var/www/example.com/{public_html,tmp,logs}

Then a dedicated pool in /etc/php/8.3/fpm/pool.d/example.com.conf (8.3 is Ubuntu 24.04's stock PHP):

[example.com]
user = site_example
group = site_example
listen = /run/php/php8.3-fpm-example.com.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = ondemand
pm.max_children = 10
pm.process_idle_timeout = 30s
php_admin_value[open_basedir] = /var/www/example.com/:/tmp/
php_admin_value[upload_tmp_dir] = /var/www/example.com/tmp
php_admin_value[session.save_path] = /var/www/example.com/tmp
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen
php_admin_flag[expose_php] = off

disable_functions can break plugins that shell out, so test per site. Validate and reload:

sudo php-fpm8.3 -t && sudo systemctl reload php8.3-fpm

In the site's nginx server block, point fastcgi_pass at that socket (unix:/run/php/php8.3-fpm-example.com.sock). Every site gets its own.

6. File permissions

PHP runs as the site user; nginx (www-data) only needs to read. A simple scheme: the site owns its files, group www-data, and directories carry the setgid bit so new files inherit the group.

sudo chown -R site_example:www-data /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 2750 {} +
sudo find /var/www/example.com -type f -exec chmod 640 {} +
sudo chmod 600 /var/www/example.com/public_html/wp-config.php

Never 777. If a plugin asks for it, the real problem is which user PHP runs as.

7. WordPress-specific hardening

WordPress is the most attacked CMS simply because it's the most common. What pays off most:

  • block PHP in uploads and close XML-RPC in nginx:
location ~* ^/wp-content/uploads/.*\.php$ { deny all; }
location = /xmlrpc.php { deny all; }
  • rate-limit logins: in the nginx http block limit_req_zone $binary_remote_addr zone=wplogin:10m rate=10r/m;, and in location = /wp-login.php add limit_req zone=wplogin burst=5 nodelay; next to your usual FastCGI directives;
  • disable the file editor in wp-config.php: define('DISALLOW_FILE_EDIT', true);
  • verify integrity with WP-CLI, run as the site user:
sudo -u site_example wp core verify-checksums --path=/var/www/example.com/public_html
sudo -u site_example wp plugin verify-checksums --all --path=/var/www/example.com/public_html
  • keep core, plugins and themes updated and delete the ones you don't use.

For the full stack setup, see our guide to WordPress hosting on a VPS.

8. Malware scanning

ClamAV catches known malware; PHP web shells also need heuristic checks.

sudo apt install clamav clamav-daemon
sudo systemctl enable --now clamav-freshclam
sudo clamscan -r -i --max-filesize=20M /var/www

The clamd daemon keeps its signatures in RAM (more than a gigabyte), so on a 2 GB VPS run plain clamscan at night instead. Two quick checks that catch a surprising number of infections:

sudo find /var/www -path '*/wp-content/uploads/*' -name '*.php'
sudo find /var/www -type f -name '*.php' -mtime -2

The first lists PHP files in uploads (almost always bad news), the second PHP files changed in the last two days.

9. Off-server backups

None of the above replaces a backup. Ransomware, a bad deploy or a dead disk will happen eventually. Ship files and databases to external storage, encrypted, with several versions kept. We cover it step by step in backing up websites to S3 with restic.

10. Minimal monitoring

You don't need a full observability stack to catch trouble early:

systemctl --failed
last -a | head -20
sudo journalctl -u ssh --since today | grep -i accepted
df -h

Add an external uptime check (any service that emails you when a site goes down) and an alert when disk usage passes 90%. A full disk stops databases and mail cold.

What Koapanel does out of the box

If you'd rather not maintain all of this by hand, Koapanel applies most of it for you on Ubuntu 24.04. According to the manual:

Measure In Koapanel
Firewall ufw managed from the Security page: allow, deny or limit rules, optionally per IP; the SSH and panel rules can't be deleted, so you can't lock yourself out
fail2ban banned IPs listed, manual unban and ban, configurable retries, ban time, window and never-ban IPs
Ubuntu updates automatic security updates on after install, optional automatic reboot, pending package list (updates)
Isolation per site: Linux user, PHP-FPM pool, open_basedir, private tmp and sessions
File access SFTP only (no plain FTP), confined to the site folder with no shell; SSH keys instead of passwords
WordPress login limited to 10 attempts per minute, fail2ban on login and XML-RPC, XML-RPC closed, PHP blocked in uploads, file editor off, sensitive files never downloadable, nightly integrity check (WordPress)
Antimalware built-in heuristic scanner plus ClamAV, mode picked from RAM (daemon from 4 GB, on-demand on smaller servers), incremental low-priority nightly scans (antimalware)
Real time every PHP, JS, HTML, SVG, .htaccess or .user.ini file written to a site is checked seconds later; only certain detections are quarantined immediately (real-time protection)
Quarantine reversible: restore, mark as false positive, repair WordPress core without touching wp-content
Audit and alerts activity log of who did what, emails for disk over 90%, failed backups, password changes
Backups encrypted restic backups to S3 or disk (backup and restore)

S3 keys and DNS tokens are stored encrypted on the server, and panel updates are signed and verified before they install.

What's still on you

  • SSH hardening for your own admin login (section 1). One catch: if you give customers password-based SFTP generated by the panel, don't disable passwords server-wide. Add a Match User root,deploy block with PasswordAuthentication no at the end of /etc/ssh/sshd_config, or use keys for SFTP too.
  • An off-site backup that is actually configured, with the repository password stored away from the server.
  • External uptime monitoring for your sites.

To be fair about the gaps: Koapanel doesn't include a web application firewall. If you need a WAF with commercial rule sets, tools like Imunify360 in the cPanel/CloudLinux world cover more ground, at a higher price. Koapanel also runs only on Ubuntu 24.04 with nginx: no Apache and no .htaccess-based configuration.

FAQ

Is changing the SSH port worth it?

It trims automated noise in your logs, but it doesn't stop anyone actually looking for you. Key-only auth plus fail2ban is the real protection.

Is ClamAV enough to find web shells?

No. ClamAV is strongest on known malware; PHP web shells are often obfuscated and change constantly. Pair it with heuristic checks and WordPress checksum verification.

Should I enable automatic reboots after updates?

On a production server, usually not. Let security updates install automatically and pick the reboot time yourself, checking /var/run/reboot-required.

Does one user per site slow the server down?

Not noticeably. With pm = ondemand, a site's PHP workers only start when requests arrive, so many pools on a small VPS use little memory.

Does Koapanel run on Debian or Ubuntu 22.04?

No. It supports only a fresh Ubuntu 24.04 LTS install.

Try it on a fresh server

Follow this guide by hand, or install Koapanel on a new Ubuntu 24.04 server and get the firewall, fail2ban, site isolation and antimalware already running. It's free for up to 3 personal sites:

curl -fsSL https://get.koapanel.app | sudo bash

Want to look first? Open the public demo, or check the installation guide and pricing.

Try Koapanel on your server

One command on Ubuntu 24.04, free up to 3 sites. Are you a provider or an agency? Let's talk wholesale pricing and migrations.

More guides