Securing a web server is a set of layers: if one fails, the next limits the damage. This checklist goes from the operating system up to the application, for a typical Nginx + PHP server on Ubuntu or Debian. Most steps apply to any stack.
Layer 1: The operating system
- Updates: enable automatic security updates — unattended upgrades.
- SSH: keys only, no root login, Fail2ban — SSH security on Debian.
- Firewall: deny by default; open 22 (restricted), 80 and 443 only — Linux firewall rules.
- Minimal software: remove packages and services you don’t use; check
ss -tulpn.
Layer 2: Isolate each site
Run each website as its own Linux user with its own PHP-FPM pool. A compromised site then can’t read or modify the others.
adduser --system --group --home /var/www/example.com site_example
/etc/php/8.3/fpm/pool.d/example.conf:
[example]
user = site_example
group = site_example
listen = /run/php/example.sock
listen.owner = www-data
listen.group = www-data
pm = ondemand
pm.max_children = 10
php_admin_value[open_basedir] = /var/www/example.com:/tmp
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen
File ownership: the site user owns the files; only upload/cache directories need to be writable.
chown -R site_example:site_example /var/www/example.com
find /var/www/example.com -type d -exec chmod 750 {} +
find /var/www/example.com -type f -exec chmod 640 {} +
Layer 3: Nginx
server_tokens off;
client_max_body_size 32m;
# Block access to hidden files and sensitive files
location ~ /\.(?!well-known) { deny all; }
location ~* \.(env|ini|log|sql|bak|old|swp)$ { deny all; }
# Don't execute PHP in upload folders
location ~* /(?:uploads|files)/.*\.php$ { deny all; }
# Rate-limit logins and APIs
limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;
Layer 4: HTTPS and headers
certbot --nginx -d example.com -d www.example.com
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# Content-Security-Policy: build one for your site; start with Report-Only
Details on TLS settings: encrypting VPS traffic.
Layer 5: PHP
expose_php = Off
display_errors = Off
log_errors = On
allow_url_include = Off
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax
Layer 6: The application
- Keep the CMS, plugins, themes and libraries updated.
- Remove unused plugins and themes.
- Use strong, unique admin passwords and 2FA.
- Store secrets outside the web root (or at least with
600/640permissions). - For WordPress specifics, see Sucuri vs Wordfence and customizing and securing the login page.
Layer 7: Web application firewall (optional)
- Edge WAF/CDN (Cloudflare, Sucuri and others) filters attacks before they reach you and hides your origin IP.
- On-server WAF: ModSecurity with the OWASP Core Rule Set, available for Nginx and Apache. Start in detection-only mode to avoid blocking legitimate traffic.
Layer 8: Backups and monitoring
- Daily off-server backups, tested restores — backup and disaster recovery.
- Uptime monitoring and alerts.
- File integrity checks (AIDE, or your security plugin) to detect unexpected changes.
- Review logs:
/var/log/nginx/error.log,journalctl -p err.
Quick audit
ss -tulpn # unexpected listeners?
find /var/www -name '*.php' -newer /etc/hostname -mtime -2 # recently changed PHP files
lynis audit system # apt install lynis — security audit report
Summary
Patch the OS, lock down SSH and the firewall, isolate sites, harden Nginx and PHP, enforce HTTPS with security headers, keep applications updated and back up off-server. On a KVM VPS you control every one of these layers. For multiple sites on one server, see hosting multiple websites on a VPS.