
Most VPS breaches don’t start with a dramatic “root exploit.” They start small: a buggy plugin, a PHP worker that goes off the rails, or a helper process you forgot was running.
In this systemd service hardening tutorial for 2026, you’ll use systemd’s built-in sandboxing to shrink what common hosting services can read, write, and execute. You won’t touch firewall rules.
This guide fits Ubuntu 24.04/26.04 LTS and Debian 12/13-style systems where Nginx, PHP-FPM, and OpenSSH run as systemd units.
The same ideas work on AlmaLinux/Rocky with systemd. Expect different paths and, sometimes, different unit names.
What you’ll harden (and what you shouldn’t break)
You’ll harden three services you’ll see on almost every hosting VPS:
- Nginx (static files, reverse proxy, TLS termination)
- PHP-FPM (WordPress/WooCommerce execution)
- OpenSSH (admin access—handle with care)
The idea is simple. If one process gets compromised, it should hit walls fast.
You’ll keep services working by applying changes as systemctl edit drop-ins, testing after each change, and rolling back cleanly when something doesn’t behave.
If you want an environment where these controls and rollbacks are easier to manage, start with a clean VM on a HostMyCode VPS.
Or use managed VPS hosting if you’d rather offload patching and “did we break the service?” checks.
Prerequisites and a safe workflow before you touch systemd
Do these first. They prevent the classic “one small tweak, one big outage” problem.
- Confirm you’re on systemd:
ps -p 1 -o comm=You should see
systemd. - List your unit names (they vary slightly by distro):
systemctl status nginx --no-pager systemctl status php8.3-fpm --no-pager systemctl status ssh --no-pager || systemctl status sshd --no-pager - Keep a root session open while testing SSH hardening.
- Know how to back out: remove drop-ins and restart.
systemctl revert nginx systemctl restart nginxrevertdeletes drop-ins and returns the unit to vendor defaults.
If you’re hardening a production WordPress host, pair this with a tested restore plan.
HostMyCode’s backup tutorials complement this well. Review VPS disaster recovery planning before you start tightening permissions.
Quick baseline: inspect what systemd already does for your services
Some distros ship with partial hardening already. Check what you have before piling on more directives.
systemctl cat nginx
systemctl show nginx -p User,Group,ProtectSystem,ProtectHome,NoNewPrivileges,PrivateTmp,CapabilityBoundingSet
Repeat for PHP-FPM and SSH.
You’re mainly looking for:
User=/Group=not running as root (SSH will still need privileges)NoNewPrivileges=yesto block privilege escalation via setuid binariesProtectSystem=plusReadWritePaths=to constrain writesPrivateTmp=yesto isolate/tmpusage
Harden Nginx with a drop-in (no package edits)
Create a drop-in override so package updates don’t overwrite your settings:
sudo systemctl edit nginx
Paste the following.
It’s conservative enough for typical hosting setups, but it still cuts down the process’ reach.
[Service]
# Stop Nginx from gaining new privileges via exec
NoNewPrivileges=true
# Give Nginx an isolated /tmp
PrivateTmp=true
# Make most of the filesystem read-only
ProtectSystem=strict
# Block access to /home unless explicitly allowed
ProtectHome=true
# Limit device access
PrivateDevices=true
# Allow writes only where Nginx must write
ReadWritePaths=/var/log/nginx /var/lib/nginx /run
# Basic kernel hardening toggles
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
# Reduce info-leaks from /proc
ProcSubset=pid
ProtectProc=invisible
# Typical safe syscall filter for web daemons
SystemCallArchitectures=native
SystemCallFilter=@system-service @network-io
Reload systemd, then restart Nginx:
sudo systemctl daemon-reload
sudo systemctl restart nginx
sudo systemctl status nginx --no-pager
Quick diagnostic if Nginx fails to start
Start with the journal. It usually points straight at the blocked path or permission.
sudo journalctl -u nginx -xe --no-pager
Common fixes:
- If Nginx needs write access to a custom cache path (for example
/var/cache/nginx), add it toReadWritePaths=. - If you serve sites from
/srv/wwwor other mount points, keepProtectSystem=strictbut add read access viaReadOnlyPaths=/srv/www. (Nginx typically only needs read access to web roots.) - If you terminate TLS and write OCSP stapling cache or temp files elsewhere, include that directory explicitly.
If you run Nginx as a reverse proxy in front of Apache, pay attention to web root and socket paths.
The reverse-proxy layout is covered in this Nginx-in-front-of-Apache tutorial.
Harden PHP-FPM without breaking WordPress uploads and cache
PHP-FPM is the one that bites people. WordPress (and its plugins) write uploads, create temp files, and rely on sessions.
If you clamp down too hard, you’ll find out quickly. Common symptoms include broken media uploads or strange login behavior.
Systemd hardening helps, but it must match where your sites actually write.
Find your PHP-FPM unit name. On Ubuntu it’s usually php8.3-fpm:
systemctl list-units --type=service | grep -E 'php.*fpm'
Create a drop-in:
sudo systemctl edit php8.3-fpm
Start with this hosting-friendly baseline:
[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
PrivateDevices=true
# This is stricter than many defaults, but still workable.
# We’ll explicitly allow common write locations.
ProtectSystem=strict
ProtectHome=true
# Adjust these paths if your distro differs
ReadWritePaths=/run /var/lib/php /var/log
# If you host sites in /var/www (common), PHP needs write access for uploads.
# Better practice is per-site pool isolation, but this works as a starting point.
ReadWritePaths=/var/www
# Reduce /proc visibility
ProcSubset=pid
ProtectProc=invisible
SystemCallArchitectures=native
SystemCallFilter=@system-service
Restart and test:
sudo systemctl daemon-reload
sudo systemctl restart php8.3-fpm
sudo systemctl status php8.3-fpm --no-pager
Verify WordPress still works (two fast checks)
- Uploads: upload an image in WordPress Media Library. If it fails, check permissions and confirm your real docroot matches
ReadWritePaths. - PHP sessions: create a simple file and load it.
printf '%s\n' '<?php session_start(); echo "OK ".session_id();' | sudo tee /var/www/html/session-test.phpLoad
/session-test.phpand confirm you get an ID.If sessions fail, confirm where your PHP stores sessions (often
/var/lib/php/sessions) and allow it.
If you plan to add object caching, do it before you squeeze PHP too hard. Less PHP work means fewer edge cases.
See WordPress Redis object cache setup for a practical starting point.
Harden SSH carefully (and keep a recovery path)
SSH is a different animal. It needs privileges, touches PAM, and interacts with user homes and shells.
If you overdo it, you don’t get a warning. You get locked out.
The goal is to tighten what’s safe, then stop.
Before you touch the systemd unit, make sure your SSH access is already sane.
If you haven’t yet, follow SSH key setup without lockouts and confirm key-based logins work reliably.
Now create a drop-in for the SSH unit (Ubuntu often uses ssh, others use sshd):
sudo systemctl edit ssh
Add a minimal hardening set that rarely interferes with normal admin work:
[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
# SSH often needs access to user homes and authorized_keys.
# Don't enable ProtectHome=true here unless you test every login path.
# Reduce /proc visibility for the daemon process.
ProcSubset=pid
ProtectProc=invisible
Restart SSH only after you’ve confirmed you still have an active root session open:
sudo systemctl daemon-reload
sudo systemctl restart ssh
sudo systemctl status ssh --no-pager
Lockout recovery checklist
- If you can’t connect, use your provider console/KVM, then run
systemctl revert sshand restart. - Keep one “break-glass” admin key stored in a password manager and a documented process for rotating it.
- On control-panel servers, confirm WHM/cPanel integrations still work after changes.
Measure the impact: confirm protections are active
After each service change, verify systemd applied the settings you think it did.
systemctl show nginx -p NoNewPrivileges,PrivateTmp,ProtectSystem,ProtectHome,ReadWritePaths,ProtectProc
systemctl show php8.3-fpm -p NoNewPrivileges,PrivateTmp,ProtectSystem,ProtectHome,ReadWritePaths,ProtectProc
systemctl show ssh -p NoNewPrivileges,PrivateTmp,ProtectProc
You can also inspect the merged unit configuration:
systemctl cat nginx
Common hosting pitfalls (and how to avoid them)
- Custom web roots: If you host sites under
/home(typical cPanel-style layouts),ProtectHome=truewill break access. UseProtectHome=read-onlyor disable it for that unit, then tighten with explicitReadOnlyPaths=/ReadWritePaths=. - ACME/Let’s Encrypt paths: Nginx might need read access to
/etc/letsencryptand write access to/var/lib/letsencryptdepending on your client. If HTTPS renewals start failing, checkjournalctlfor denied paths and adjust. - Socket locations: If Nginx talks to PHP-FPM via a UNIX socket in
/run/php/, both units must be allowed to access it. YourReadWritePaths=/runusually covers this. - Plugin temp directories: Some WordPress plugins write to odd paths (bad practice, but common). It’s usually better to fix the plugin config than to permanently widen service permissions.
Optional: tighten write access per site instead of globally
On a multi-site hosting VPS, the clean approach is per-site PHP-FPM pools and filesystem permissions that keep each site inside its own sandbox.
That includes uploads, cache, and a controlled temp directory.
Systemd hardening still helps, but it works best as a second line of defense. It shouldn’t be your only control.
If you run multiple client sites or a reseller setup, consider a control panel built for multi-tenant management, plus predictable backups and migrations.
In many cases, a properly sized managed VPS hosting plan costs less than cleaning up after a single serious incident.
Rollback and change management: treat hardening like code
Hardening changes are production changes. Treat them that way.
- Keep drop-ins in version control (even a private Git repo). You can copy files from
/etc/systemd/system/nginx.service.d/. - Document exceptions: every extra
ReadWritePathsshould have a reason and a ticket. - Test after updates: package updates can introduce new runtime paths. If a restart fails,
journalctl -uusually points to the missing permission.
Production checklist (copy/paste)
- [ ] Added drop-ins using
systemctl edit(not by editing vendor unit files) - [ ] Restarted one service at a time and validated health checks
- [ ] Verified applied settings with
systemctl show - [ ] Confirmed WordPress login, uploads, and checkout (if WooCommerce)
- [ ] Confirmed SSH login from a separate terminal before closing the old session
- [ ] Documented exceptions (
ReadWritePaths/ReadOnlyPaths)
Summary: practical hardening that doesn’t depend on a firewall
Systemd sandboxing won’t replace patching or sane filesystem permissions. What it does well is limit blast radius.
If Nginx or PHP-FPM gets compromised, NoNewPrivileges, ProtectSystem, and explicit write paths can turn “server takeover” into a smaller, containable incident.
If you want a clean baseline for this systemd service hardening tutorial, deploy on a HostMyCode VPS.
If you’d rather hand off routine service tuning and safety checks, managed VPS hosting is the simplest route.
Hardening is easier on a fresh, correctly sized server with some resource headroom. If you’re rebuilding or migrating to a cleaner baseline, start with a HostMyCode VPS, or choose managed VPS hosting if you want help keeping Nginx/PHP/SSH stable while you tighten security.
FAQ
Will systemd hardening break WordPress plugins?
It can if a plugin writes outside your allowed paths. Start with conservative ReadWritePaths, test uploads and updates, then tighten. Fix plugin paths where possible.
Should I enable ProtectHome=true for SSH?
Not until you test every login path. SSH often needs to read ~/.ssh/authorized_keys and interact with PAM. A minimal set (NoNewPrivileges, PrivateTmp, ProtectProc) is safer.
Is this better than a firewall hardening guide?
It’s different. A firewall limits network reachability; systemd sandboxing limits what a compromised process can do locally. On a hosting VPS you generally want both layers.
How do I see exactly what caused a restart failure?
Use journalctl -u servicename -xe. Denied filesystem paths are the most common cause; add only the specific directory to ReadWritePaths or ReadOnlyPaths.
Can I apply this on a cPanel server?
You can, but tread lightly. cPanel manages many services and paths. Test in staging first, and prefer hardening only the services you fully understand.