Most “WordPress hacked” stories I get called into are not zero-days. They are credential stuffing against admin with a leaked password from a 2019 forum dump, hitting /wp-login.php from a rotating residential proxy pool. The site owner blames WordPress. The actual cause is that login security was treated as one toggle (“strong password”) instead of four or five layered controls.
This guide walks the layers I actually configure on client sites: username hygiene, 2FA with a working WP-CLI fallback, rate-limiting at Cloudflare and the server, and the recovery path for the day someone loses their phone or commits an application password to a public repo.
How WordPress logins actually get attacked
Three patterns cover almost everything I see in access logs:
- Brute-force on
/wp-login.php: bots POST to wp-login.php withlog=admin&pwd=.... Distributed across hundreds of IPs, often from residential proxy networks like 911.re successors. Single-IP rate limits do not stop them. - Credential stuffing: same endpoint, but credentials come from public breach corpora (the “Collection #1” through #5 dumps, RockYou2024). Username is whatever email or login leaked. You can pre-empt this with a haveibeenpwned API check on password set.
- XML-RPC
system.multicallamplification: one POST to/xmlrpc.phpcontaining asystem.multicallbody lets the attacker test hundreds of passwords in a single HTTP request, bypassing per-request rate limits. If you do not use the WP mobile app or Jetpack, block xmlrpc.php at the server. - REST API user enumeration:
?rest_route=/wp/v2/usersand/wp-json/wp/v2/usersreturn realslugvalues for every published author. That slug is the login name. Combine with the breach dumps above and the attacker has half the credential pair.
Username hygiene and user enumeration
Why do hackers love the username “admin”? Because it cuts their work in half.
In a brute force attack, the attacker needs to guess two things: the Username and the Password. If you use “admin”, you gave them 50% of the credentials for free. This is why eliminating default usernames is the first and most critical step in securing your WordPress login.
The “ID 1” Problem
By default, the first user created in WordPress has the ID of 1. Hackers often scan your-site.com/?author=1. If your site redirects to your-site.com/author/admin/, you have just revealed your username to every attacker who knows to look.
This user enumeration vulnerability exists because WordPress, by default, creates author archive pages that expose usernames in URLs. Even if you change your display name, the username often remains visible in these archives.
The Fix: Surgical Removal
You cannot simply “rename” a username in WordPress. You must perform a transplant that preserves all content while eliminating the vulnerable account.
-
Create a New Commander:
- Go to Users -> Add New.
- Username: Something obscure (e.g.,
Obsidian_Eagle_88). Avoid using your real name, email prefix, or anything guessable from your domain. - Email: Your secure email address.
- Role: Administrator.
- Generate a strong password using a password manager.
-
Log In as the New Commander: Use a private browser window to ensure you’re not still authenticated as the old user.
-
Delete the Recruit:
- Go to Users.
- Hover over “admin” and click Delete.
- CRITICAL STEP: WordPress will ask: “What should be done with content owned by this user?”
- Select: “Attribute all content to:” -> [Select your New User].
- This ensures all posts, pages, and media remain associated with your new account.
-
Confirm. “Admin” is dead. Long live
Obsidian_Eagle_88.
Additional User Enumeration Protections
Beyond removing the admin user, implement these additional measures:
Disable Author Archives: If you don’t need author pages, disable them via server config (e.g. block /?author=) or by adding this to your functions.php:
// Disable author archives to prevent username enumeration
add_action('template_redirect', function() {
if (is_author()) {
wp_redirect(home_url(), 301);
exit;
}
});
Block REST API User Enumeration: WordPress REST API exposes user data by default. Block unauthorized access:
// Block user enumeration via REST API
add_filter('rest_endpoints', function($endpoints) {
if (isset($endpoints['/wp/v2/users'])) {
unset($endpoints['/wp/v2/users']);
}
if (isset($endpoints['/wp/v2/users/(?P<id>[\d]+)'])) {
unset($endpoints['/wp/v2/users/(?P<id>[\d]+)']);
}
return $endpoints;
});
Two-factor authentication and the lockout problem
A password alone is a single point of failure. With 2FA, an attacker needs the password plus the second factor; with passkeys (covered below) there is no shared secret to leak in the first place.
The part most guides skip is the failure mode: what happens when the admin loses their phone, leaves the company, or the TOTP secret was set up on a device that got wiped. You need a documented WP-CLI escape hatch before you enforce 2FA, not after.
# Emergency: disable 2FA for a locked-out user (Two-Factor plugin / WP 2FA)
wp user meta delete <user_id> _two_factor_enabled_providers
wp user meta delete <user_id> _two_factor_provider
wp user meta delete <user_id> _two_factor_options
# Or list what is set so you know what to remove
wp user meta list <user_id> --keys=_two_factor_enabled_providers,_two_factor_provider
Run this from SSH on the server. If the admin no longer has SSH either, the recovery path is your hosting control panel terminal or a database query against wp_usermeta. Document this before you need it.
The hierarchy of 2FA methods
Not all 2FA methods are created equal. Understanding the hierarchy helps you choose appropriate protection:
1. SMS Codes (Deprecated - Do Not Use) SMS-based 2FA was once common but is now considered insecure. SIM swapping attacks allow hackers to hijack phone numbers, and SMS messages can be intercepted. Major security organizations now recommend against SMS 2FA.
2. Email Codes (Weak but Better Than Nothing) Email-based 2FA sends a one-time code to your registered email address. While better than no 2FA, this method is vulnerable if your email account is compromised. Use only if no other options are available.
3. TOTP Apps (Standard Security) Time-based One-Time Password (TOTP) apps like Google Authenticator, Authy, Microsoft Authenticator, and Ente Auth generate codes that change every 30 seconds. These are significantly more secure than SMS or email because:
- Codes are generated locally on your device
- No network transmission of the secret
- Works offline
- Synchronized time-based algorithm
Setup Process:
- Install a TOTP app on your smartphone
- In WordPress, scan the QR code provided by your 2FA plugin
- Enter the verification code to confirm setup
- Save backup codes in a secure location (password manager)
4. Push Notifications (Convenient but Vendor-Dependent) Services like Duo Security send push notifications to your phone for approval. While convenient, you’re dependent on the vendor’s infrastructure. Use for low-risk accounts, not critical admin access.
5. Hardware Security Keys (The Ironclad Standard) Physical security keys like YubiKey, Titan Security Key, and FIDO2-compliant devices represent the gold standard in authentication. These devices use public-key cryptography to provide unphishable authentication:
- Unphishable: Even if you type your password on a fake site, the authentication fails because the physical key verifies the domain
- Cryptographic proof: Uses challenge-response protocols that cannot be replayed
- No shared secrets: Unlike TOTP, there’s no secret that can be stolen from the server
- Multi-protocol support: FIDO2, FIDO U2F, and PIV compatibility
Recommended Hardware Keys:
- YubiKey 5 Series: Supports FIDO2, PIV, OpenPGP, and OTP protocols
- Google Titan Security Key: Affordable FIDO2 compliance
- Thetis FIDO2 Key: Budget-friendly option for basic FIDO2 needs
Recommended 2FA Plugins for WordPress
WP 2FA (by Melapress)
- Excellent free version with TOTP support
- Premium adds hardware key support, backup codes, and policy enforcement
- User-friendly setup wizard
- Role-based 2FA requirements
Two-Factor (Plugin Contributors)
- Lightweight, official WordPress.org plugin
- Supports TOTP, FIDO U2F, and backup codes
- No premium upsells
- Developer-friendly hooks for customization
Solid Security Pro (formerly iThemes Security)
- Comprehensive security suite including 2FA
- Supports TOTP, email, and backup codes
- Integrated with broader security hardening
Duo Security
- Enterprise-grade solution
- Push notifications, phone callbacks, and hardware tokens
- Centralized user management for multiple sites
Implementing 2FA: Best Practices
- Require 2FA for all administrator accounts: No exceptions
- Enforce 2FA for editor roles: They can publish content and affect your site’s reputation
- Provide backup codes: Users will inevitably lose access to their 2FA device
- Set grace periods: Give users time to set up 2FA before enforcement
- Document recovery procedures: Have a plan for when users lock themselves out
Application passwords: audit them
WordPress core ships application passwords (24-character tokens for REST API and XML-RPC clients). They bypass 2FA by design. I have seen these end up in .env files committed to public GitHub repos and in mobile app crash reports posted to support forums. Audit them quarterly:
wp user application-password list <user_id>
wp user application-password delete <user_id> <uuid>
If you do not use the REST API for authenticated writes, disable application passwords entirely with add_filter( 'wp_is_application_passwords_available', '__return_false' ); in a mu-plugin.
Passwordless logins with passkeys
In 2026, we are moving beyond passwords entirely. Passkeys represent the future of authentication, utilizing the biometrics on your device (TouchID, FaceID, Windows Hello) to authenticate via public-key cryptography.
How Passkeys Work
Passkeys use the WebAuthn standard to create cryptographic key pairs:
- Registration: Your device creates a private key (stored securely) and a public key (sent to the server)
- Authentication: The server sends a challenge; your device signs it with the private key
- Verification: The server verifies the signature with the stored public key
Advantages of Passkeys
- No password to steal: There’s no shared secret that can be leaked
- Phishing-resistant: The private key is bound to the specific domain
- Cross-device synchronization: Passkeys sync across your Apple ID, Google account, or password manager
- Biometric convenience: Use fingerprint or face recognition instead of typing passwords
Implementing Passkeys in WordPress
Plugin Solutions:
- Solid Security Pro: Includes passkey support for administrators
- Passkey Login for WordPress: Dedicated passkey implementation
- Auth0 WordPress Plugin: Enterprise identity provider with passkey support
Enterprise Solutions: For larger organizations, consider integrating with identity providers like:
- Okta: Comprehensive identity management with passkey support
- Microsoft Azure AD: Enterprise-grade authentication including passwordless options
- 1Password: Password manager with passkey support for teams
Rate-limiting wp-login.php and xmlrpc.php
Even when 2FA blocks the actual takeover, an unrate-limited brute-force still burns PHP-FPM workers and MySQL connections. On shared hosting it can take the site down without ever cracking a password. You want rate limits at three layers, in this order of effectiveness: edge (Cloudflare), server (Nginx or fail2ban), application (plugin).
Layer 1: Application Level (Plugins)
Limit Login Attempts Reloaded The most popular and reliable rate limiting plugin for WordPress:
Recommended Configuration:
- Lockout after 3 failed attempts
- Lockout time: 24 hours for repeated failures
- Email notifications for lockouts
- GDPR-compliant logging (no IP storage without consent)
- Whitelist your office IP addresses
We do not recommend security plugins. Use server-level rate limiting (fail2ban, Nginx limit_req) and your host’s login protection. This avoids plugin overhead and keeps security outside PHP.
Layer 0: Cloudflare rate-limiting rule
If the site is on Cloudflare (free plan included), this is the highest-leverage rule you can set. Dashboard → Security → WAF → Rate limiting rules:
- Field: URI Path
- Operator: equals
- Value:
/wp-login.php - When: 5 requests / 1 minute from the same IP
- Then: Managed Challenge (or Block for 1 hour)
Add a second rule for /xmlrpc.php if you keep it enabled. The advantage over fail2ban: it stops the requests at the edge, before they hit your origin.
Layer 2: CAPTCHA (Cloudflare Turnstile)
Google reCAPTCHA is annoying (nobody likes clicking traffic lights), potentially impacts accessibility, and sends data to Google’s servers. Cloudflare Turnstile is the modern alternative.
Why Cloudflare Turnstile?
- Invisible: Most users never see a challenge
- Privacy-focused: No personal data sent to third parties
- Accessibility: Works with screen readers and assistive technologies
- Performance: Minimal impact on page load times
- Free tier: Generous free tier for most websites
How It Works: Turnstile checks vulnerability signals from the browser to determine if the visitor is human without asking them to solve a puzzle. It analyzes browser behavior, JavaScript execution, and other signals invisible to legitimate users.
Implementation:
- Sign up for a free Cloudflare account
- Navigate to Turnstile in the dashboard
- Create a new site key and secret key
- Add Turnstile to your login form via code (script + site key) or your host’s integration; we do not recommend installing security plugins.
- Enable on login, registration, and password reset forms
Advanced Configuration:
- Set “Invisible” mode for minimal user friction
- Configure fallback challenges for suspicious traffic
- Enable logging to monitor blocked attempts
Layer 3: Server Level (Fail2Ban)
This is for the heavy hitters (VPS and dedicated server users). Fail2Ban scans your server logs and automatically bans IP addresses showing malicious behavior.
How Fail2Ban Works:
- Monitors log files (e.g.,
/var/log/nginx/access.log) - Matches patterns defined in filters (e.g., multiple failed logins)
- Updates firewall rules to ban offending IPs
- Automatically unbans after a configured time (or permanently)
Installation on Ubuntu/Debian:
sudo apt update
sudo apt install fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
WordPress-specific configuration:
A subtlety most tutorials get wrong: a failed wp-login.php POST returns HTTP 200 (the page re-renders with an error), and a successful one returns 302 (redirect to wp-admin). So matching only on 200 catches both legitimate failed attempts and successful logins, which means you ban yourself if you mistype your password three times. The pattern below combines the 200 with the brute-force volume in the jail’s findtime:
Create /etc/fail2ban/filter.d/wordpress.conf:
[Definition]
failregex = ^<HOST> .* "POST /wp-login\.php.*" 200
^<HOST> .* "POST /xmlrpc\.php.*" 200
ignoreregex = ^<HOST> .* "POST /wp-login\.php.*" 302
Create /etc/fail2ban/jail.local:
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
backend = systemd
[wordpress]
enabled = true
port = http,https
filter = wordpress
logpath = /var/log/nginx/access.log
maxretry = 3
bantime = 86400
Nginx Configuration for Rate Limiting: Add to your Nginx configuration:
# Rate limit zone definition (in http block)
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
# Apply to wp-login.php
location = /wp-login.php {
limit_req zone=login burst=5 nodelay;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
# Block XML-RPC if not needed
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
}
Monitoring Fail2Ban:
# Check status
sudo fail2ban-client status
# Check WordPress jail status
sudo fail2ban-client status wordpress
# List banned IPs
sudo fail2ban-client status wordpress | grep "Banned IP list"
# Unban an IP manually
sudo fail2ban-client set wordpress unbanip 192.168.1.1
Login monitoring and alerting
You cannot secure what you cannot see. Comprehensive login monitoring provides visibility into attack patterns and early warning of compromise attempts.
What to Monitor
Failed Login Attempts:
- Track failed login attempts by username and IP
- Identify patterns (e.g., multiple usernames from same IP)
- Detect credential stuffing attacks
Successful Logins:
- Log all successful admin logins
- Track login times and locations
- Alert on logins from new locations or devices
Account Changes:
- Monitor password changes
- Track role modifications
- Alert on new user creation
Geographic Anomalies:
- Flag logins from unexpected countries
- Detect impossible travel (logins from distant locations within impossible timeframes)
Monitoring Solutions
We do not recommend security plugins. Use server- and infrastructure-level monitoring instead:
Server and access logs:
- Monitor
/var/log/nginx/access.log(or Apache) for failed logins and wp-login.php requests - Use fail2ban to alert on brute-force patterns
- Rotate and archive logs for at least 90 days
External / hosted monitoring:
- Loggly / Splunk / Datadog: Log aggregation and alerting for high-traffic or high-value sites
- Hosting dashboards: Many hosts provide intrusion and login attempt summaries
- Uptime and integrity checks: External monitoring for defacement or downtime
Setting Up Email Alerts
Use server-level or external monitoring to send immediate alerts for:
- Failed login attempts from new IP addresses
- Successful logins from unusual locations
- Administrator account modifications
- Plugin or theme file changes
- Core WordPress file modifications
Log Retention and Analysis
Retention Policies:
- Keep security logs for at least 90 days
- Archive logs for 1 year if compliance requires
- Ensure GDPR compliance in log retention
Regular Analysis:
- Weekly review of failed login patterns
- Monthly analysis of blocked attack trends
- Quarterly security posture assessment
Incident response when the login wall fails
Despite best efforts, security incidents occur. Having documented procedures ensures rapid, effective response.
Incident Response Plan Template
Phase 1: Detection and Identification
-
Recognize the incident:
- Unexpected admin activity
- Unfamiliar files in WordPress directories
- Sudden traffic spikes
- Google Safe Browsing warnings
- Customer reports of malware warnings
-
Assess the scope:
- Which accounts are compromised?
- What files have been modified?
- Is the database affected?
- Are backups clean?
Phase 2: Containment
-
Immediate actions:
- Change all admin passwords immediately
- Force logout all users (delete WordPress sessions)
- Enable maintenance mode if necessary
- Block suspicious IP addresses at firewall level
-
Preserve evidence:
- Export security logs before they rotate
- Save suspicious files for analysis
- Document timeline of events
- Take screenshots of suspicious activity
Phase 3: Eradication
-
Clean the infection:
- Restore core WordPress files from official source
- Reinstall all plugins from WordPress.org
- Switch to a clean, default theme temporarily
- Scan database for malicious injections
-
Close vulnerabilities:
- Update all software to latest versions
- Remove unused plugins and themes
- Implement missing security measures from this guide
- Change all passwords and regenerate API keys
Phase 4: Recovery
- Restore from clean backup if available
- Verify integrity of all restored files
- Re-enable site with enhanced monitoring
- Notify affected parties (customers, partners, regulators if required)
Phase 5: Post-Incident Review
- Analyze the attack vector
- Document lessons learned
- Update security procedures
- Schedule security training for team members
Emergency Contacts and Resources
- Hosting provider security team: For server-level issues
- WordPress security professionals: Sucuri, WPScan, or local experts
- Law enforcement: For serious criminal attacks
- Cyber insurance provider: If you have coverage
Block author/user enumeration at the server
Hackers use automated scripts to guess usernames by scanning /?author=1, /?author=2, etc. You can block this at the server level.
Apache (.htaccess) Method
## Stop author scans
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteCond %{QUERY_STRING} (author=\d+) [NC]
RewriteRule .* - [F]
</IfModule>
Nginx Method
# Block author enumeration
if ($args ~* "author=([0-9]*)") {
return 403;
}
# Block user enumeration via REST API
location ~* /wp-json/wp/v2/users {
deny all;
return 403;
}
No plugin recommended
User enumeration can be disabled with the server or .htaccess rules above. We do not recommend installing security plugins for this.
Hardening at the edges
Locking wp-admin to known IPs
If your team works from a small set of static IPs (office, VPN), a CIDR allowlist on /wp-admin/ removes the public attack surface entirely. Anything outside the allowlist gets a 403 before WordPress loads. The trade-off is real: travelling admins, freelancers on hotel wifi, and emergency access from a phone all break. Only use this if you have a documented VPN with reliable uptime.
location ^~ /wp-admin/ {
allow 203.0.113.0/24; # office
allow 198.51.100.42/32; # VPN egress
deny all;
# ...existing fastcgi config
}
location = /wp-login.php {
allow 203.0.113.0/24;
allow 198.51.100.42/32;
deny all;
}
For Cloudflare-fronted sites, do this with a WAF custom rule on http.request.uri.path matches "^/wp-(admin|login)" and not ip.src in {203.0.113.0/24}.
Renaming wp-login.php
Moving the login URL is obscurity, not security. It cuts dumb bot traffic in your access logs (useful for log signal-to-noise) but a determined attacker scrapes it from the source of any page. Treat it as a noise filter, never as a layer.
Disable file editing in admin
Two wp-config.php constants close the most common post-compromise escalation path: an attacker with admin access editing theme/plugin PHP from the dashboard.
define( 'DISALLOW_FILE_EDIT', true ); // hides Appearance > Theme File Editor
define( 'DISALLOW_FILE_MODS', true ); // also blocks plugin/theme install + updates
DISALLOW_FILE_MODS is heavy because it breaks one-click updates; use it on sites where you deploy code through CI and run updates via WP-CLI.
Web application firewall
A WAF filters malicious traffic before it reaches your WordPress install:
- Cloudflare WAF: free tier includes Managed Rules for WordPress; the Pro plan adds OWASP CRS at paranoia level 1
- Sucuri: premium WAF with WordPress-specific virtual patches
- ModSecurity + OWASP CRS: server-level option for self-hosted Nginx/Apache, paranoia level 1 is the practical baseline (PL2+ generates too many false positives on Gutenberg)
SSL/TLS for Login Pages
Ensure your entire site uses HTTPS, especially login pages:
- Install an SSL certificate (Let’s Encrypt provides free certificates)
- Force HTTPS redirects in your web server configuration
- Enable HTTP Strict Transport Security (HSTS)
- Use secure cookies for WordPress authentication
Database Security
Protect your WordPress database:
- Change the default
wp_table prefix during installation - Use strong database passwords
- Restrict database access to localhost only
- Regular database backups stored securely
What I actually configure on a client site
Not a generic posture checklist. The specific work order I run when hardening a WordPress login from scratch:
- Delete the
adminusername, reattribute content, set the surviving admin’snicknameanddisplay_nameso the slug does not leak the login. - Block
/wp-json/wp/v2/usersand?rest_route=/wp/v2/usersfor unauthenticated requests via therest_endpointsfilter shown earlier. - Enforce 2FA for all
administratorandeditorroles, with TOTP as default and a documentedwp user meta deleterecovery runbook in the team password manager. - Audit
wp user application-password listfor every admin; delete anything older than 90 days or unrecognised. - Add a Cloudflare rate-limit rule on
/wp-login.php(5/min, Managed Challenge) and a second rule on/xmlrpc.phpif it is not blocked outright. - Install fail2ban with the wp-login filter that distinguishes 200 (failed) from 302 (success) so locked-out users are not wrongly banned.
- Set
DISALLOW_FILE_EDITinwp-config.php. AddDISALLOW_FILE_MODSonly if the team deploys via CI. - Force HTTPS, HSTS with
preload, andSet-Cookie: Secure; HttpOnly; SameSite=Laxforwordpress_logged_in_*. - Confirm
wp-config.phphas unique salts (wp config shuffle-salts), the DB user has only the privileges WordPress needs (noGRANT ALL), and database access is bound to localhost.
Login security is four or five layered controls, not one. Skip any layer and the rest still hold; skip three and you are back to relying on the password.
Related Resources and Further Reading
Official Documentation
We do not recommend installing security plugins. Prefer server-level hardening (fail2ban, WAF, backups), strong passwords, 2FA via hosting or minimal solutions, and external monitoring.
Cloud Services
- Cloudflare Turnstile Documentation
- Sucuri Website Security
- Google reCAPTCHA (if Turnstile unavailable)
Hardware Security Keys
Learning Resources
- OWASP WordPress Security Implementation Guide
- NIST Digital Identity Guidelines
- SANS WordPress Security Checklist
Community and Support
Re-audit application passwords, fail2ban ban counts, and the 2FA recovery runbook every quarter. Most sites I take over have one of those three drifted: a stale app password from a former employee, a fail2ban that has not banned anything in 6 months because the log path moved, or a 2FA setup nobody knows how to bypass when the admin’s phone dies.
Learn more about WordPress security services at WPPoland. Updated: January 28, 2026 Next Review: April 28, 2026

