# Security Hardening
## WordPress — Public File Exposure
### Problem
Wordfence flags "Critical | Public Files" when these files are publicly accessible:
- `license.txt` — reveals WP version
- `wp-config-sample.php` — shouldn't be public
- `readme.html` / `readme.[hash].html` — reveals WP version
- `error_log` — exposes internal paths and PHP errors
- `xmlrpc.php` — brute force / DDoS attack vector
### Fix applied 2026-05-18 (all 17 sites)
1. Deleted `license.txt`, `wp-config-sample.php`, `readme*.html`, `error_log` from all webroots
2. Added `xmlrpc.php` block to `.htaccess` on all sites (kaburu.co already had it)
3. Installed mu-plugin `remove-sensitive-files.php` on all sites — auto-removes sensitive
files after any WP core update so they never come back
### mu-plugin location
`wp-content/mu-plugins/remove-sensitive-files.php` on every managed site.
Auto-loaded by WordPress, no activation needed. Hooks `upgrader_process_complete`
and fires on `core` updates only.
### .htaccess xmlrpc block (added 2026-05-18) — ⚠️ PROVEN DEAD CODE ON LITESPEED
The original 2026-05-18 fix used an Apache-style `.htaccess` block:
```
# Block xmlrpc
Order Deny,Allow
Deny from all
```
**This never actually worked.** LiteSpeed silently ignores Apache 2.2 `` /
`Order Deny,Allow` syntax in `.htaccess`. Verified 2026-08-13: claybusters.scot had the
block but `POST /xmlrpc.php` still returned HTTP 200. Wordfence's WAF was the only thing
actually blocking xmlrpc attacks the whole time.
### ✅ Real fix applied 2026-08-13 — vhost context block (all 30 WP sites)
Replaced the dead `.htaccess` block with a LiteSpeed-native vhost context (exact-path,
not regex — regex form is unreliable on LiteSpeed):
```
context /xmlrpc.php {
allowBrowse 0
accessControl {
deny *
}
}
```
Rolled out via `scripts/estate-vhost-xmlrpc-block.sh` (same proven pattern as the
debug.log rollout). Result: **28 sites added, 2 already had it (kaburu.co, rf.kaburu.co),
0 failed.** Full LiteSpeed restart (`lswsctrl stop && start`). Verified per-site at
origin (`curl --resolve SITE:443:49.13.202.144`) → HTTP 403 on all WP sites.
Edge cases (both fine):
- `chippy.kaburu.co.uk` → 301 (redirect-only alias → thechippyvan.co.uk, which is 403)
- `llmtest.kaburu.co` → 404 (xmlrpc not reachable; test-site vhost quirk, non-exploitable)
Pre-flight verified 2026-08-13: **nothing legit uses xmlrpc.php** — MainWP no longer
uses xmlrpc SSO (zero dashboard-IP traffic across weeks of logs), and all xmlrpc hits
were bot traffic (Azure/DO/Oracle cloud IPs).
### Sites covered (25 — full fleet as of 2026-07-31)
24hrresponse.com, aberdeenrda.co.uk, bankhousecatering.co.uk,
billing.myretonmarquees.co.uk, chippy.kaburu.co.uk, claybusters.scot, dm-ph.com,
doctorfin.co.uk, friendscic.org, ggsgenerators.co.uk, jafricasafari.com, kaburu.co,
kaburu.co.uk, kaburusvr.uk, l8.kaburu.co.uk, l8waterhygiene.co.uk, llmtest.kaburu.co,
longniddrybowlingclub.com, longniddrybowlingclub.co.uk, myretonmarquees.co.uk,
pilates-edinburgh.co.uk, rf.kaburu.co, test-md.kaburu.cc, touchwell.co.uk,
womenunlimited.africa
---
## WordPress — Debug Log Exposure (wp-content/debug.log)
### Problem
`wp-content/debug.log` is written by WordPress when `WP_DEBUG_LOG=true` is set in `wp-config.php`. Can leak full request/response payloads, database errors, stack traces, user data, installed plugin paths.
### Defence-in-depth (applied 2026-07-31)
Three layers, cheapest → most reliable:
1. **`WP_DEBUG_LOG=false`** in wp-config.php (eliminates source). Flipped on every site 2026-07-31.
2. **.htaccess Log File Protection ``** — present on 23/25 sites. **DOES NOT WORK on LiteSpeed** (Apache ``/`` ignored on OLS).
3. **Vhost context block — the ONLY reliable block on LiteSpeed.** Added to all 25 sites 2026-07-31:
```apache
context /wp-content/debug.log {
allowBrowse 0
accessControl {
deny *
}
}
```
- Appended to each `/usr/local/lsws/conf/vhosts/{site}/vhost.conf`
- Backups at `vhost.conf.bak-20260731-0501`
- **Requires full LiteSpeed restart** (`lswsctrl fullrestart`) — graceful SIGUSR1 does NOT pick up new contexts.
- Verified: 23/25 sites return HTTP 403 for debug.log; 2 (friendscic.org, llmtest.kaburu.co) return 404 (no file on disk = block never matches; equally safe).
- Belt-and-braces with Wordfence WAF (which also blocks the literal filename by name match).
**Steve's rule 2026-07-31: "if WAF ever stops, we become exposed" — now mitigated by vhost context block.**
---
## CyberPanel — Port Lockdown
- Port 8090 (CyberPanel): allow 100.64.0.0/10 (Tailscale), DROP rest — applied 2026-05-17
- Port 8081 (pdns API): allow 127.0.0.1, DROP rest — applied 2026-05-17
- Port 22 (SSH): allow Tailscale, DROP rest — applied 2026-05-17
- Rules persisted to /etc/iptables/rules.v4
---
## Pending
- [ ] Hetzner snapshot automation (manual snapshot done 2026-05-17)
- [ ] TrueNAS offsite backup
- [ ] FTP disabled / SFTP enforced
- [ ] Hermes self-monitoring heartbeat (if Z840 goes down)
- [ ] ufw proper firewall ruleset
- [ ] CyberPanel backup verification
- [ ] WordPress per-site backup plugin
- [ ] Wordfence — investigate rainbowvapes.co.uk (High | Error), touchwell.co.uk (High | Server State)