We built a 10-server high-availability WordPress cluster across three Australian cities. We hardened it with CIS benchmarks, AppArmor, auditd, and a Wazuh SIEM. Then we asked the obvious question: does it actually hold up?
So we red-teamed ourselves — running a full penetration test from an external attack server against our own infrastructure. Here is what we found, what we fixed, and what we learned.
The Setup
Our HA cluster runs across Sydney, Brisbane, and Melbourne on BinaryLane infrastructure:
- 6 web servers (nginx + PHP-FPM + WordPress) behind an anycast load balancer
- 3 MariaDB servers (primary + 2 replicas) on a private VPC with no public IPs
- 1 jumpbox running Wazuh SIEM, Ansible monitoring, and centralized logging
- 1 encrypted file server (LUKS + SMB3) accessible only via WireGuard
Before the pentest, we had already applied significant hardening:
- CIS Ubuntu 24.04 benchmark at 72.2%
- AppArmor enforcing on all servers
- auditd with 54 audit rules
- Wazuh SIEM with 10 agents
- Centralized rsyslog forwarding
- Unattended security updates
- SSH key-only authentication
- Dedicated Ansible user (not root)
We felt good about it. The question was: would an external attacker agree?
The Red Team Approach
We used a separate BinaryLane server (outside the VPC) as our attack box. No credentials, no insider knowledge — pure black-box external assessment. We built a Python-based pentest suite that ran 7 phases autonomously:
- Full port scan — all 65,535 TCP ports on every public-facing server (655,350 total port checks)
- SSH analysis — banner grabbing, authentication method enumeration
- SSL/TLS analysis — protocol versions, cipher suites, certificate validation
- HTTP header audit — security headers across all web endpoints
- WordPress-specific tests — 23 endpoint probes, XML-RPC, REST API, version fingerprinting, cookie analysis
- Standalone server analysis — our landing page servers
- DNS reconnaissance — records, SPF, DMARC, WHOIS
What the Infrastructure Got Right
The network layer was rock solid:
- Jumpbox: zero open ports. All 65,535 ports showed as filtered. Completely invisible from the outside.
- Web servers: only ports 80 and 443. Nothing else visible. SSH was completely dark.
- Database servers: unreachable. No public IP, no way to even attempt a connection.
- SSL/TLS: TLS 1.2 and 1.3 only. AES-256-GCM preferred. No weak ciphers, no deprecated protocols.
All the sysctl hardening, firewall rules, and BinaryLane hypervisor-level filtering did exactly what it was supposed to do. From the outside, this looked like a minimal web-only surface.
What We Found (The Bad News)
The infrastructure was solid. The WordPress application layer was not.
Our initial scan found 11 findings — including 2 critical:
Critical: XML-RPC Wide Open
WordPress ships with xmlrpc.php enabled by default. This endpoint exposes over 80 methods including system.multicall — which lets an attacker try hundreds of password guesses in a single HTTP request. It bypasses rate limiting, it bypasses fail2ban, and it bypasses login lockout plugins. It is the number one WordPress brute-force vector.
Critical: REST API Leaking Usernames
The WordPress REST API endpoint /wp-json/wp/v2/users was publicly returning our admin username, user ID, and Gravatar email hash — with no authentication required. Combined with XML-RPC, an attacker now has both the username AND an unlimited brute-force channel.
The Attack Chain
Put these together and you get a textbook WordPress compromise:
- Hit
/wp-json/wp/v2/users→ get username “admin“ - Brute-force password via
xmlrpc.phpusingsystem.multicall - Login to wp-admin → install a malicious plugin or edit theme PHP
- Achieve code execution as
www-data - Read
wp-config.phpfor database credentials - Access the database via the VPC
No amount of sysctl hardening or AppArmor would have stopped this. It is a completely different attack surface.
Other Findings
- No HSTS header — browsers could be tricked into connecting via HTTP first
- No Content Security Policy — XSS attacks not mitigated at the header level
- WordPress version disclosed — via RSS feed, asset URLs, and readme.html
- wp-cron.php externally accessible — potential DoS vector
- Plugin/theme directories listing — fingerprinting surface
- No SPF/DMARC — email spoofing possible on all domains
The Fix (Same Day)
We remediated everything within the same session and deployed across all 6 web servers:
| Finding | Severity | Fix |
|---|---|---|
| XML-RPC | Critical | Blocked in nginx → 403 |
| User enumeration | Critical | REST API users endpoint blocked → 403 |
| Version disclosure | High | Generator tag removed, version strings stripped, readme.html deleted |
| No HSTS | High | Strict-Transport-Security header added |
| No CSP | High | Content-Security-Policy deployed |
| wp-cron exposed | Medium | Blocked externally, server-side cron instead |
| Directory listing | Medium | Plugin/theme directories return 403 |
The Re-Test
After remediation, we ran the entire 7-phase pentest again. Results:
- 0 critical findings (down from 2)
- 0 high findings (down from 3)
- 1 medium finding — WordPress installer page accessible (low risk, WP already installed)
- 6 low findings — missing SPF/DMARC DNS records
The entire XML-RPC → user enumeration → brute-force attack chain is now dead. Every critical endpoint returns 403.
The Lesson
We spent a morning hardening the infrastructure layer — kernel parameters, audit rules, AppArmor profiles, CIS benchmarks. All valuable work. But the red team found that the real risk was in the application layer, not the infrastructure.
The two critical findings (XML-RPC and user enumeration) are default WordPress behaviour. They ship enabled on every WordPress installation. They are not bugs — they are features that most sites do not need and should disable.
Infrastructure hardening and application hardening are different disciplines that protect against different threats. You need both. CIS benchmarks will not save you from an open XML-RPC endpoint, and blocking XML-RPC will not save you from a weak kernel configuration.
Red-teaming your own infrastructure is the only way to see what an attacker actually sees. Everything else is assumptions.
Our Security Stack (Post-Remediation)
| Layer | Controls |
|---|---|
| Network | VPC isolation, hypervisor firewalls, nftables, no public IPs on DB/file servers |
| Access | SSH key-only, AllowUsers whitelist, dedicated ansible user, no persistent keys on servers |
| Encryption | TLS 1.2/1.3, LUKS at rest, SMB3 transport encryption, WireGuard tunnel |
| Monitoring | Wazuh SIEM (10 agents), auditd (54 rules), centralized rsyslog, Ansible health checks |
| Hardening | CIS 72.2%, AppArmor enforcing, sysctl hardened, kernel modules disabled |
| Application | XML-RPC blocked, user enum blocked, HSTS, CSP, version hidden, wp-cron internal |
| Patching | Unattended security updates (daily), kernel current (6.8.0-106) |
Total monthly cost for all of this: ~$66.50 AUD.
If you are running WordPress on any hosting provider — check if your xmlrpc.php is accessible. It probably is.