Security plugins can help, but they are not a substitute for sane hosting, clean permissions, updates, and backups. Start with the boring controls that reduce risk without adding another dashboard, another update cycle, and another potential conflict.
1. Keep WordPress boringly current
Most compromises are not exotic zero-days. They are old plugins, abandoned themes, weak passwords, and writable files.
Automated checks
# Core, plugin, and theme updates
wp core check-update
wp plugin list --update=available
wp theme list --update=available
# PHP version
php -v
# MySQL/MariaDB version
mysql --version
Auto-update policy
Enable minor core updates (default). For plugins and themes:
# Enable auto-updates for trusted plugins
wp plugin auto-updates enable plugin-slug
# Disable for plugins that need testing
wp plugin auto-updates disable plugin-slug
Do not enable auto-updates for everything. A plugin update that breaks WooCommerce checkout is an outage, not a security improvement.
2. File permissions
WordPress needs the web server to write to wp-content/uploads. Nothing else should be writable:
# Set default permissions
find . -type d -exec chmod 755 {} \;
find . -type f -exec chmod 644 {} \;
# Lock down wp-config.php
chmod 600 wp-config.php
# Lock down .htaccess if using Apache
chmod 644 .htaccess
The writable directories
Only these should be writable:
wp-content/uploads/— media uploadswp-content/cache/— if using a caching pluginwp-content/upgrade/— temporary upgrade directorywp-content/languages/— if using automatic translations
If a plugin requires writable code directories (some page builders do), at least restrict it to that plugin’s directory rather than making all of wp-content writable.
Check current state
# Find world-writable files and directories
find . -perm -o+w -not -path './wp-content/uploads/*' -not -path './wp-content/cache/*' -ls
Fix any unexpected writable files. World-writable code directories are a common persistence mechanism after compromise.
3. Disable file editing
In wp-config.php:
define('DISALLOW_FILE_EDIT', true);
This removes the theme and plugin editors from wp-admin. It does not stop an administrator from installing plugins, but it prevents editing PHP files through the browser — a common post-compromise tactic.
Add while you are there:
define('DISALLOW_FILE_MODS', false); // Keep false unless you want to block all plugin/theme installs
define('WP_AUTO_UPDATE_CORE', 'minor'); // Auto-update minor core releases
define('FORCE_SSL_ADMIN', true); // Force HTTPS on admin
4. Database security
Change the table prefix
If you are setting up a new site, change the default wp_ prefix:
$table_prefix = 'xyz_';
This does not prevent SQL injection, but it makes automated attacks slightly less trivial. For an existing site, changing the prefix is possible but requires careful table renaming — the security benefit is marginal compared to keeping plugins updated.
Database user permissions
The WordPress database user should have the minimum required privileges:
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX, CREATE TEMPORARY TABLES, LOCK TABLES ON database_name.* TO 'wp_user'@'localhost';
FLUSH PRIVILEGES;
Do not grant SUPER, FILE, PROCESS, or GRANT OPTION to the WordPress database user.
Secure wp-config.php
// Move wp-config.php one directory above the web root when possible
// If not possible, restrict access at the web server level
For nginx:
location ~ /wp-config.php {
deny all;
}
5. Login protection without breaking users
Strong passwords (enforced)
Add to wp-config.php:
define('WP_FORCE_ADMIN_PASSWORD', true);
This forces strong passwords when creating admin accounts.
Two-factor authentication
Use an application-level 2FA plugin for admin accounts. Wordfence Login Security is free and lightweight. Avoid plugins that add 2FA to every user by default — it increases support requests for password resets.
Rate limiting at the web server
For nginx:
limit_req_zone $binary_remote_addr zone=wp_login:10m rate=5r/m;
location = /wp-login.php {
limit_req zone=wp_login burst=3 nodelay;
# ... rest of config
}
This limits login attempts to 5 per minute per IP. Pair with Cloudflare rate limiting (Pro or higher) for an additional layer.
Block XML-RPC brute force
XML-RPC allows multiple authentication attempts in a single request via system.multicall. If you use Cloudflare, enable “Block XML-RPC” in the WAF rules.
Disable author archives
Author archives expose usernames:
// functions.php or a mu-plugin
add_action('template_redirect', function () {
if (is_author()) {
wp_safe_redirect(home_url(), 301);
exit;
}
});
6. XML-RPC
If you do not use Jetpack, the WordPress mobile app, or any remote publishing tool that requires XML-RPC:
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
}
If you need XML-RPC, whitelist the IPs that use it:
location = /xmlrpc.php {
allow 192.0.2.10; # Jetpack IP
deny all;
}
7. Security headers
Baseline headers
For nginx:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
Content Security Policy
Be careful with CSP on WordPress. Themes, builders, analytics, ad scripts, and third-party integrations can break. Start with Content-Security-Policy-Report-Only to collect violation reports before enforcing:
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' *.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: *.cloudflare.com; font-src 'self'; frame-src 'self' *.youtube.com; connect-src 'self' *.google-analytics.com; report-uri https://example.report-uri.com/r/d/csp/reportOnly" always;
HSTS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
Only add HSTS if you are confident the site will always be served over HTTPS. Once the header is cached by browsers, HTTP access is blocked for the duration of max-age.
8. Backups as security control
A clean restore path matters more than a long list of hardening tweaks. Maintain:
- Daily database backups, stored off-server
- File backups for uploads, themes, and custom plugins
- At least 30 days retention
- At least one pre-incident backup from before any suspicious activity
- Tested restore process (not theoretical)
9. User and administrator audit
# List administrators
wp user list --role=administrator
# List all user emails
wp user list --field=user_email
# Check last login (if tracked by a plugin)
wp user list --fields=ID,user_login,user_email,user_registered
Remove old accounts, contractor access, and shared admin logins. Every admin account is a potential entry point. Shared logins make incident response miserable — you cannot tell who did what.
10. Document the baseline
After hardening, record:
- Which rules are active and why
- File permissions baseline
- Database prefix and user privileges
- Security headers applied
- Backup schedule and restore process
- User account audit date
Security work that cannot be explained or reproduced during an incident is a vulnerability, not a protection. Future you should understand every intentional restriction without guessing.