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 uploads
  • wp-content/cache/ — if using a caching plugin
  • wp-content/upgrade/ — temporary upgrade directory
  • wp-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.