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
| Measure | Tool | Priority |
|---|---|---|
| Update WP, themes, plugins | Auto-updates | Critical |
| Strong password + 2FA | WP 2FA plugin | Critical |
| Disable xmlrpc.php | Nginx | High |
| 644/755 permissions on files | chmod | High |
| WAF / Firewall | Wordfence, Sucuri | High |
| Hide the wp-login.php URL | WPS Hide Login | Medium |
| Backups | UpdraftPlus | Critical |
| SSL certificate | Let's Encrypt | Critical |
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.phprequest in a browser returns the textXML-RPC server accepts POST requests only.— the endpoint is alive and responding, it is just waiting for a POST request. The200 OKresponse itself means thedeny allrule has not taken effect yet. - The Nginx config was edited, yet
/wp-login.phpstill opens from an outside network — a common cause: the edit landed in the wrongserver {}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 successfuldeny allblock looks like in the Nginx log; the browser simply shows a plain403 Forbidden.- An editor backup file such as
wp-config.php~orwp-config.php.bakis served by the server as plain text instead of being executed as PHP — thelocation = /wp-config.phprule 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.phpusing thesystem.multicallmethod 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
| Method | What it gives | Weak point |
|---|---|---|
| IP allowlist in Nginx | Fully closes the login to outside addresses | Breaks with a dynamic IP, needs the rule updated regularly |
| HTTP Basic Auth in front of the login form | An extra password even before the WordPress form | One shared password for everyone, not tied to a specific user |
| Hiding the URL (WPS Hide Login) | Stops automated bot brute-forcing at the standard address | Does not protect against a targeted attack once the address becomes known |
| Two-factor authentication | Protects the account even if the password leaks | Does 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_EDITbreaks 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.phpdoes 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.