Patch everything, weekly.
The single most common vector we see is an unpatched plugin with a public CVE. Most "hacks" are not zero-days — they're 90-day-olds.
- Enable automatic minor updates for WordPress core in
wp-config.php. - Run plugin and theme updates weekly. Test on staging if you have it; ship anyway if you don't.
- Remove any plugin you haven't used in 60 days. Inactive plugins can still be exploited.
Why this matters: 78% of the compromised sites we cleaned in 2025 had a known, patched CVE in an outdated plugin. Patching is the highest-ROI security work you can do.
Secure wp-config.php.
Your wp-config.php holds database credentials and salts. Treat it like a secret.
# 1. Move it one directory above the web root if you can # 2. Set permissions tight chmod 440 wp-config.php # 3. Add to wp-config.php (above "stop editing" line) define('DISALLOW_FILE_EDIT', true); define('WP_AUTO_UPDATE_CORE', 'minor'); define('FORCE_SSL_ADMIN', true);
Regenerate the eight unique keys in wp-config.php using the official WordPress salt generator. Rotate them every 6 months, or immediately after any suspicious activity.
Why this matters: A leaked wp-config.php is a full site takeover. We've seen it exposed via misconfigured backups, public Git repos, and verbose error pages.
Force strong passwords and 2FA on every admin.
Brute force still works. In Q1 2026, we logged a median of 1,200 login attempts per day on small business sites. Don't make it easy.
- Require 16+ character passwords for any user with editor or higher privileges.
- Mandate TOTP-based 2FA for administrators — not SMS.
- Audit your user list quarterly. Delete or downgrade abandoned accounts.
Why this matters: A single admin with the password "Summer2024!" cancels out every other security control on this list.
Disable XML-RPC unless you need it.
The xmlrpc.php endpoint allows password testing at thousands of attempts per HTTP request, and most fail2ban rules miss it.
# nginx
location = /xmlrpc.php { deny all; access_log off; return 444; }
If you genuinely use XML-RPC (Jetpack, the WordPress mobile app, some legacy clients), restrict it to known IP addresses instead of leaving it world-readable.
Disable in-dashboard file editing.
The "Edit Theme" and "Edit Plugin" screens let any compromised admin account drop a webshell in seconds. Turn them off.
define('DISALLOW_FILE_EDIT', true);
define('DISALLOW_FILE_MODS', true); // also blocks plugin install/update via dashboard
Use SFTP or your deploy pipeline for legitimate file changes. The friction is the point.
Lock down file permissions.
WordPress should never be able to write to its own core files in production.
- Directories:
755 - Files:
644 wp-config.php:440(owner+group read only).htaccess:644with extra care — many compromises start here
Make the web user (usually www-data) the group, not the owner. The owner should be your deploy user.
Add security headers.
Free, easy, surprisingly often skipped. Add these in nginx, Apache, or via a header plugin if you have no server access.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
A Content-Security-Policy header is more effective but requires testing — we cover it in a separate post on the blog.
Limit login attempts.
Rate-limit /wp-login.php at the web server, not just at the application layer. By the time WordPress sees the request, it has already done a database lookup.
# nginx
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
location = /wp-login.php { limit_req zone=login burst=3 nodelay; }
Five attempts per minute per IP is plenty for a real human. Bots will hit the wall.
Harden the database.
- Change the default
wp_table prefix on new installs. - Use a dedicated database user with privileges scoped to the WordPress database only.
- Never run MySQL on a public IP. Bind to localhost or use a private network.
Changing the table prefix on an existing site is risky — only do it on a fresh install or a fully tested staging clone.
Set up off-site, encrypted, tested backups.
This is the step that turns "we got hacked" into "we restored in 20 minutes." Three rules:
- Off-site: a backup on the same server is not a backup. Use S3, B2, or any object storage in a different region.
- Encrypted: with a key you control, not the backup plugin's hosted service.
- Tested: restore to a staging environment quarterly. Untested backups fail at the worst possible moment.
Why this matters: Of the sites we couldn't fully recover in 2025, 100% had backup systems that the owner believed were working but had silently broken months earlier.
Quick-print checklist
If you only do these ten things, you'll be ahead of 95% of WordPress sites on the public internet.
- Plugins, themes, and core updated this week
wp-config.phpmoved or chmod 440- Salts regenerated in the last 6 months
- Strong passwords + TOTP 2FA on all admins
- Inactive admin accounts removed
- XML-RPC disabled or IP-restricted
DISALLOW_FILE_EDIT= true- File permissions: 755 / 644 / 440
- HSTS + nosniff + frame-options + referrer-policy headers set
- Login endpoint rate-limited at web server
- Dedicated, scoped database user
- MySQL not exposed publicly
- Off-site backups, encrypted, restore-tested in last 90 days
Want this audited automatically?
SecureAuditWP runs every check on this page (and 60 more) on a schedule, and writes you a PDF report.