Home / Blog / WordPress hardening in five layers, from .htaccess to mu-plugins

WordPress hardening in five layers, from .htaccess to mu-plugins

A default WordPress install is a target. Here's the five-layer hardening I apply, and which attack each layer is meant to shut down.

A client’s site got hacked. I did a clean reinstall, and it was hacked again the same day. Once I dug into the logs, the attack vector was brute-forcing wp-login.php. A clean install alone isn’t enough, you need defensive layers. Since then every site I stand up gets the same hardening. Here it is, layer by layer.

Layer 1: web server (.htaccess / nginx)

The first line of defence is filtering requests before they ever reach the app.

# hide wp-config.php
<Files wp-config.php>
  Require all denied
</Files>

# block xmlrpc.php (pingback + brute force target)
<Files xmlrpc.php>
  Require all denied
</Files>

# don't let php execute in uploads
<Directory "wp-content/uploads">
  <FilesMatch ".(php|phtml|phar)$">
    Require all denied
  </FilesMatch>
</Directory>

# block user enumeration (?author=1)
RewriteCond %{QUERY_STRING} author=d
RewriteRule ^ - [L,R=403]

# hide readme.html (leaks WP version)
<Files readme.html>
  Require all denied
</Files>

For nginx the equivalents live in location blocks.

Layer 2: wp-config.php

WordPress’s own config file. A handful of constants are non-negotiable:

// disable file editing (theme/plugin editor)
define('DISALLOW_FILE_EDIT', true);

// disable plugin/theme install/update from admin (do it via CI/CD)
define('DISALLOW_FILE_MODS', true);

// route db errors to debug.log, not the page
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);

// auth keys must be generated (wp-cli)
// fresh set from https://api.wordpress.org/secret-key/1.1/salt/

// force SSL admin
define('FORCE_SSL_ADMIN', true);

// cap autosaves and revisions
define('WP_POST_REVISIONS', 5);

DISALLOW_FILE_MODS is the critical one. Even if someone gets into admin, they can’t install a plugin. New plugins only go through WP-CLI or the deploy pipeline.

Layer 3: mu-plugins

Must-use plugins can’t be deactivated, which makes them the right home for security checks.

This theme ships [wp-content/mu-plugins/ac-security-hardening.php](wp-content/mu-plugins/ac-security-hardening.php) which:

  • Closes the REST API /users endpoint to unauthenticated callers.
  • Strips X-Powered-By and Server headers.
  • Locks down application passwords.
  • Logs failed logins (for fail2ban).
  • Hardens the wp-json/oembed endpoints.
  • Disables pingback (XMLRPC) services.
// block rest api user enumeration
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;
});

// log failed logins
add_action('wp_login_failed', function ($username) {
    error_log(sprintf('[LOGIN_FAIL] %s from %s', $username, $_SERVER['REMOTE_ADDR']));
});

// rate-limit the login endpoint
add_action('login_form_login', function () {
    $ip = $_SERVER['REMOTE_ADDR'];
    $key = 'ac_login_attempts_' . md5($ip);
    $attempts = get_transient($key) ?: 0;
    if ($attempts >= 5) wp_die('Too many attempts. Try again in 15 minutes.', 429);
    set_transient($key, $attempts + 1, 15 * MINUTE_IN_SECONDS);
});

Layer 4: user discipline

  • Admin username must not be “admin”. Brute force always targets it.
  • Enforce strong passwords.
  • Two-factor authentication plugin, required for admin users.
  • Rotate passwords every six months.
  • Delete unused accounts.
  • Trim capabilities with a role editor. Check whether an author actually needs delete_posts.

Layer 5: protection from outside

  • Cloudflare or a WAF in front. It blocks known attack patterns before they reach the app.
  • Fail2ban reading login logs to ban brute force IPs.
  • SSH key-only access, password login disabled.
  • Kernel kept current, PHP up to date.
  • Backups, so when you do get hacked you can roll back to a clean version.

Hostinger note

These sites run with Hostinger’s own security mu-plugins. hostinger-auto-updates keeps core patched automatically. Leave that on, but keep full control over your own code on top.

Testing

  • Run WPScan against your own site.
  • Check sitecheck.sucuri.net.
  • Run securityheaders.com for CSP, X-Frame-Options, Referrer-Policy.
  • Review access logs regularly. Spot the anomalies (500 hits from one IP, a POST flood).

Takeaway

No single fix secures a site. Defense in depth does. Web server, WordPress config, mu-plugins, user management, outside protection. Each layer closes a different attack vector. If an attacker gets past one, the next one still holds. That’s engineering.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch