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
httpblocklimit_req_zone $binary_remote_addr zone=wplogin:10m rate=10r/m;, and inlocation = /wp-login.phpaddlimit_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,deployblock withPasswordAuthentication noat 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.