SafeServerSetup
All case studies · Sample engagement · representative numbers

Locked down a Hetzner CX22 hosting 12 WordPress sites for a small agency

One server, twelve client sites, one shared user. We isolated everything.

Customer
Boutique web agency
Team size
4 people
Provider
Hetzner
OS
Debian 12
Plan · Duration
Pro · 32 hours
Engagement summary

A four-person agency hosting twelve WordPress sites for clients on a single Hetzner CX22. Their concern: a compromise on one client app could pivot to all the others because everything ran as the same www-data user. We hardened the host and added per-vhost isolation.

Why they came to us

They'd had a near-miss the previous quarter where a vulnerable plugin on one client's site had let an attacker write to <code>/tmp</code>. Nothing escalated, but it spooked them. They wanted defense-in-depth before a bigger client onboarded.

Audit findings

What we found on the box.

First pass before changing anything. Severity ranges from critical (act immediately) to low (worth knowing).

  • All 12 vhosts running as the same www-data user — no inter-app isolation

    critical
  • xmlrpc.php exposed publicly on every WordPress install

    high
  • No fail2ban; logs showed ~11k WordPress login attempts / day

    high
  • Two installs still running default WordPress salts (never rotated)

    high
  • Let's Encrypt auto-renewal silently broken — certs expiring in 9 days

    high
  • 14 days of unapplied security patches

    medium
  • PHP 8.1 across the box (security support ending soon)

    medium
  • No off-box backups — only Hetzner snapshots, untested

    medium
Hardening applied

What we changed, in order.

Each change is reversible and documented in the handover doc. Commands shown are illustrative.

  1. 1

    One PHP-FPM pool per vhost, each running as its own dedicated system user with chroot.

  2. 2

    NGINX: blocked xmlrpc.php at the server level (return 444), rate-limited wp-login.php per IP.

  3. 3

    fail2ban with nginx-http-auth, nginx-botsearch, plus a custom WordPress login filter watching the access log.

  4. 4

    Migrated PHP 8.1 → 8.3 with a per-site smoke test before cutover.

  5. 5

    Rotated the WordPress auth keys and salts on the two affected installs.

  6. 6

    Fixed the certbot cron path (it was running but couldn't write to /var/log/letsencrypt).

  7. 7

    Layered Hetzner Cloud Firewall on top of UFW + fail2ban as defense-in-depth.

  8. 8

    Set up restic daily backups to a Backblaze B2 bucket; retention 7 daily / 4 weekly. Restore-tested before handover.

  9. 9

    Wrote a per-vhost rollback runbook the agency could follow during client incidents.

Before / after

The numbers that moved.

Representative figures from this engagement. Real, named-customer studies will publish actual numbers with a link to verify.

WordPress login attempts / day blocked
Before
0
After
11,200
Cert expiry headroom
Before
9 days
After
87 days
Vhost user isolation
Before
1 shared
After
12 isolated
Patch lag
Before
14 days
After
0 days
Off-box backup coverage
Before
none
After
daily, restore-tested
Ready to be the next one

Hand us your VPS, get an engagement like this one.

Pick a plan, send the credentials through the encrypted form, and we'll come back with the same kind of audit + hardening + handover the studies above describe.