Back to tutorials
Tutorial

Systemd Service Hardening Tutorial (2026): Lock Down Nginx, PHP-FPM, and SSH on a Hosting VPS

Systemd service hardening tutorial for 2026: apply sandboxing to Nginx, PHP-FPM, and SSH on a hosting VPS safely.

By Anurag Singh
Updated on Sep 06, 2026
Category: Tutorial
Share article
Systemd Service Hardening Tutorial (2026): Lock Down Nginx, PHP-FPM, and SSH on a Hosting VPS

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.

  1. Confirm you’re on systemd:
    ps -p 1 -o comm=
    

    You should see systemd.

  2. 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
    
  3. Keep a root session open while testing SSH hardening.
  4. Know how to back out: remove drop-ins and restart.
    systemctl revert nginx
    systemctl restart nginx
    

    revert deletes 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=yes to block privilege escalation via setuid binaries
  • ProtectSystem= plus ReadWritePaths= to constrain writes
  • PrivateTmp=yes to isolate /tmp usage

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 to ReadWritePaths=.
  • If you serve sites from /srv/www or other mount points, keep ProtectSystem=strict but add read access via ReadOnlyPaths=/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)

  1. Uploads: upload an image in WordPress Media Library. If it fails, check permissions and confirm your real docroot matches ReadWritePaths.
  2. 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.php
    

    Load /session-test.php and 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 ssh and 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=true will break access. Use ProtectHome=read-only or disable it for that unit, then tighten with explicit ReadOnlyPaths=/ReadWritePaths=.
  • ACME/Let’s Encrypt paths: Nginx might need read access to /etc/letsencrypt and write access to /var/lib/letsencrypt depending on your client. If HTTPS renewals start failing, check journalctl for 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. Your ReadWritePaths=/run usually 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 ReadWritePaths should have a reason and a ticket.
  • Test after updates: package updates can introduce new runtime paths. If a restart fails, journalctl -u usually 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.

Systemd Service Hardening Tutorial (2026): Lock Down Nginx, PHP-FPM, and SSH on a Hosting VPS | HostMyCode