Skip to main content

Server-Side WordPress Hardening: Nginx and Permissions

Security · 19.04.2026 · 5 min read
Illustration for “Server-Side WordPress Hardening: Nginx and Permissions”

WordPress is the most attacked CMS in the world. About 90% of hacks are related to outdated plugins, weak passwords, and open xmlrpc.php. This checklist covers all the main attack vectors.

1. File and directory permissions

find /var/www/wordpress -type d -exec chmod 755 {} \;
find /var/www/wordpress -type f -exec chmod 644 {} \;
chmod 400 /var/www/wordpress/wp-config.php
chown www-data:www-data /var/www/wordpress/wp-config.php

2. Protecting wp-config.php and uploads in Nginx

location = /wp-config.php { deny all; }
location ~* /wp-content/uploads/.*\.php$ { deny all; }

3. Disable xmlrpc.php

location = /xmlrpc.php { deny all; access_log off; log_not_found off; }

4. Protecting the login page

location = /wp-login.php {
    allow 1.2.3.4;
    deny all;
    fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}

5. Additions to wp-config.php

define('DISALLOW_FILE_EDIT', true);
define('DISALLOW_FILE_MODS', true);
// Generate keys: https://api.wordpress.org/secret-key/1.1/salt/
define('AUTH_KEY', 'unique_key_1');
define('SECURE_AUTH_KEY', 'unique_key_2');

6. WordPress Security Checklist

MeasureToolPriority
Update WP, themes, pluginsAuto-updatesCritical
Strong password + 2FAWP 2FA pluginCritical
Disable xmlrpc.phpNginxHigh
644/755 permissions on fileschmodHigh
WAF / FirewallWordfence, SucuriHigh
Hide the wp-login.php URLWPS Hide LoginMedium
BackupsUpdraftPlusCritical
SSL certificateLet's EncryptCritical
Plugins are the main threat: 56% of WordPress hacks happen through vulnerable plugins. Regularly update all plugins and remove unused ones — even deactivated plugins remain a vulnerability.

Common mistakes when hardening WordPress

  • A GET /xmlrpc.php request in a browser returns the text XML-RPC server accepts POST requests only. — the endpoint is alive and responding, it is just waiting for a POST request. The 200 OK response itself means the deny all rule has not taken effect yet.
  • The Nginx config was edited, yet /wp-login.php still opens from an outside network — a common cause: the edit landed in the wrong server {} block, when the server has separate blocks for HTTP and HTTPS or for several domains and only one of them was fixed.
  • access forbidden by rule, client: ..., server: ..., request: "GET /wp-config.php HTTP/1.1" — this is what a successful deny all block looks like in the Nginx log; the browser simply shows a plain 403 Forbidden.
  • An editor backup file such as wp-config.php~ or wp-config.php.bak is served by the server as plain text instead of being executed as PHP — the location = /wp-config.php rule only matches that exact filename, and a copy with a different extension slips past it and exposes the database password in plain text.
  • An attack on xmlrpc.php using the system.multicall method can test thousands of passwords in a single HTTP request instead of thousands of separate attempts — which is why disabling xmlrpc.php lowers risk more than a single, harmless-looking endpoint would suggest.

Ways to protect wp-login.php: what they give and where they fall short

MethodWhat it givesWeak point
IP allowlist in NginxFully closes the login to outside addressesBreaks with a dynamic IP, needs the rule updated regularly
HTTP Basic Auth in front of the login formAn extra password even before the WordPress formOne shared password for everyone, not tied to a specific user
Hiding the URL (WPS Hide Login)Stops automated bot brute-forcing at the standard addressDoes not protect against a targeted attack once the address becomes known
Two-factor authenticationProtects the account even if the password leaksDoes not limit the number of attempts against the login form itself

When these measures do not fit as-is

  • WordPress Multisite with different paths for each site in the network — strict location = rules for a specific path may not match the real network URLs, and the scheme needs to be adapted to the actual install.
  • Managed hosting with no access to the Nginx configuration, only .htaccess or a control panel — rules from a server {} block cannot be applied there, only what the panel or Apache via .htaccess supports will work.
  • The team regularly edits themes and plugins through the built-in file editor in the admin panel — turning on DISALLOW_FILE_EDIT breaks that workflow; the fix is switching to SFTP or a git-based deploy, or skipping this particular measure.
  • The site serves data to a mobile app or a headless frontend through the REST API — advice about blocking xmlrpc.php does not extend to the REST API (/wp-json/), which is a separate mechanism with its own access rules.

Verifying that the protection actually works

# Confirm xmlrpc.php is really closed (expect 403, not 200)
curl -I https://example.com/xmlrpc.php

# Check that editor backup files are not served as plain text
curl -I https://example.com/wp-config.php~
curl -I https://example.com/wp-config.php.bak

# Find files on the server that are writable by everyone
find /var/www/wordpress -type f -perm /o+w

The first three requests should return 403 or 404, not 200. The last command should return an empty list: if find turns up anything, those files have wider permissions than set in the first section of this article and need to be fixed separately.

Was this article helpful?
← Back to Knowledge Base Ask Support