Every WordPress site running with default config is being hammered right now by brute-force scripts hitting xmlrpc.php and wp-login.php. If you run multiple sites on a single OpenLiteSpeed (LSWS) box, dropping a per-site .htaccess rule on each one is tedious and easy to miss — and worse, security plugins like Wordfence add overhead at the application layer when the right place to drop these is the web server.
Here’s a one-shot approach to block xmlrpc.php and hide wp-login.php behind a secret URL across every vhost on the LSWS server, using only LSWS’s native rewrite engine. No plugins, no per-site changes.
Why these two URLs?
- xmlrpc.php — gives unauthenticated
system.multicallbrute-force surface, and is also abused for amplification DDoS via pingbacks. Almost no modern WP site needs it. - wp-login.php — the front door for credential stuffing. Hiding it stops 99% of automated attacks, since they don’t bother fingerprinting.
The rewrite block
This is the snippet we want injected into the rewrite { ... } section of every vhost’s vhconf.conf. It does three things: 403 on xmlrpc, alias /wp-login-r.php to /wp-login.php, and 403 on direct hits to wp-login.php that don’t come from the form itself.
# Block xmlrpc.php (anti-brute-force / amplification)
RewriteRule ^/?xmlrpc\.php$ - [F,L,NC]
# Internal alias for hidden login URL: /wp-login-r.php → /wp-login.php
RewriteRule ^/?wp-login-r\.php$ /wp-login.php [END]
# Block direct external access to /wp-login.php
# Allow when: original request was via /wp-login-r.php,
# OR Referer is same-site wp-login/wp-admin (form POST + wp-admin actions)
RewriteCond %{THE_REQUEST} !\s/wp-login-r\.php[\s\?]
RewriteCond %{HTTP_REFERER} !^https?://[^/]+/(wp-login(-r)?\.php|wp-admin/)
RewriteRule ^/?wp-login\.php$ - [F,L,NC]Two things worth calling out:
[END]instead of[L]on the alias rule.[L]stops the current rewrite pass but lets the per-directory.htaccessrules fire again — which can re-route the rewritten URL through WordPress’sindex.phpand give you a 404.[END]stops all further rewrite processing.- The Referer-based allowlist is what lets the WP login form’s POST submission still work. WordPress’s form posts back to
wp-login.phpwith a Referer header pointing atwp-login.phporwp-login-r.phpon the same host — those POSTs pass. Bots POSTing without Referer get 403’d.
Apply to every vhost in one shot
Editing 17 vhost configs by hand is not what we got into sysadmin for. Here’s a small Python loop that drops the snippet into every vhconf.conf right after the RewriteEngine on line, idempotently:
sudo python3 - <<'PY'
import os, re, glob
SNIPPET = r"""
# Block xmlrpc.php (added 2026-04-27)
RewriteRule ^/?xmlrpc\.php$ - [F,L,NC]
# Internal alias for hidden login URL
RewriteRule ^/?wp-login-r\.php$ /wp-login.php [END]
# Block direct hits on wp-login.php
RewriteCond %{THE_REQUEST} !\s/wp-login-r\.php[\s\?]
RewriteCond %{HTTP_REFERER} !^https?://[^/]+/(wp-login(-r)?\.php|wp-admin/)
RewriteRule ^/?wp-login\.php$ - [F,L,NC]
"""
for f in sorted(glob.glob("/usr/local/lsws/conf/vhosts/*/vhconf.conf")):
src = open(f).read()
if "wp-login-r" in src: continue # idempotent
rw = re.search(r'rewrite\s*\{[^{}]*?enable\s+(\d)', src, re.S)
if not rw or rw.group(1) == '0': continue
new = src.replace("RewriteEngine on",
"RewriteEngine on\n" + SNIPPET.rstrip(), 1)
if new != src:
open(f, "w").write(new)
print("updated:", os.path.basename(os.path.dirname(f)))
PY
sudo /usr/local/lsws/bin/lswsctrl restartIdempotent — running it twice is a no-op. The enable 1 check skips vhosts where rewrite is disabled (default LSWS Example vhost, etc.).
Verify
for h in your-site-1.com your-site-2.com; do
d=$(curl -s -o /dev/null -w "%{http_code}" -k --resolve "${h}:443:127.0.0.1" "https://${h}/wp-login.php")
a=$(curl -s -o /dev/null -w "%{http_code}" -k --resolve "${h}:443:127.0.0.1" "https://${h}/wp-login-r.php")
ref=$(curl -s -o /dev/null -w "%{http_code}" -k --resolve "${h}:443:127.0.0.1" \
-H "Referer: https://${h}/wp-login-r.php" "https://${h}/wp-login.php")
echo "$h direct=$d (want 403) alias=$a (want 200) with-referer=$ref (want 200)"
doneExpected output:
your-site-1.com direct=403 alias=200 with-referer=200
your-site-2.com direct=403 alias=200 with-referer=200Gotchas
- LiteSpeed Cache may serve a stale 404 page if you tested while the rule was being tuned. Purge
/usr/local/lsws/cachedata/and the LSCWP plugin’s data dir for each site after the rule lands. - Save your bookmark. Update password manager entries and team docs to point at
/wp-login-r.php. - Privacy plugins that strip Referer headers on the user side will cause their form POSTs to get 403’d. Modern browser defaults (
strict-origin-when-cross-origin) send Referer for same-site requests so this works for normal users — but if anyone hits this, the fix is to log in via the alias URL, not via cached form submissions from elsewhere. - This is not a substitute for keeping WordPress and its plugins updated. It’s a layer that buys you time by removing the cheapest attack surface.
One snippet, one Python loop, every vhost protected. Pair this with disabling PHP execution under wp-content/uploads/ (RewriteRule \.(php|hph|phtml|phar)$ - [F,L,NC] in an uploads .htaccess) and you’ve closed the two most-exploited paths into a typical WordPress install.