The 2 AM Wake-Up Call and Lessons from Nginx Logs
The terminal screen was scrolling incessantly. The Netdata dashboard was glowing red: the CPU of a 4-vCore VPS running WordPress for a client spiked to 100%, and the Load Average hit 50. I quickly typed tail -f /var/log/nginx/access.log, and a “flood” of POST requests to xmlrpc.php appeared at a frequency of 20 requests per second. The server was under a massive brute-force attack.
That was when I realized: installing security plugins like Wordfence only addresses the symptoms. By the time a request hits the PHP layer, server resources have already evaporated. A smart SysAdmin stops these attacks right at the Web Server “gateway,” before they even reach the source code.
Below is the production-grade WordPress hardening workflow I’ve used to keep servers stable at under 5% CPU even while being scanned.
Defense in Depth Strategy
Never rely on a single barrier. For WordPress, we will set up a multi-layered defense from the outside in:
- Web Server Layer (Nginx): Acts as a “bodyguard,” filtering out malicious requests and restricting access to sensitive areas.
- Operating System Layer (OS): Tightens file permissions and strictly prevents unauthorized scripts from executing.
- Source Code Layer (PHP): Fine-tunes wp-config.php and removes version footprints.
- Database Layer: Isolates permissions, granting only the necessary rights for WordPress to function.
Detailed Hardening Steps for SysAdmins
1. Blocking Brute-Force at the Nginx Layer
Most automated scanning tools today target xmlrpc.php and wp-login.php. Instead of letting PHP process them and consume RAM, I configure Nginx to flatly reject these requests.
# Completely block access to xmlrpc.php to save resources
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
}
# Only allow office IP or VPN to access the admin page
location ~ ^/(wp-admin|wp-login\.php) {
allow 1.2.3.4; # Replace with your static IP
deny all;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
}
If you travel frequently and don’t have a static IP, use Basic Authentication (htpasswd). This forces hackers to crack an additional password layer at the web server level before they can even see the WordPress login form.
2. Setting Up File System Permissions via the Principle of Least Privilege
Setting 777 permissions for source code is a fatal mistake. My principle is simple: The web server should only have write access to the /uploads directory; all other files should be read-only.
Run the following commands as the www-data user to secure your server:
# Set ownership for the entire project
chown -R www-data:www-data /var/www/html
# Directories set to 755 (Owner has write access, Group and Others are read/execute only)
find /var/www/html -type d -exec chmod 755 {} \;
# Files set to 644 (Only Owner can write)
find /var/www/html -type f -exec chmod 644 {} \;
# The most sensitive configuration file should only be readable by the Owner
chmod 600 /var/www/html/wp-config.php
Most importantly: Block PHP execution in the uploads directory. Even if a hacker bypasses a plugin to upload a web shell, they won’t be able to execute it.
# Configuration within the Nginx server block
location ~* ^/wp-content/uploads/.*\.php$ {
deny all;
}
3. “Freezing” Dangerous Features in wp-config.php
The wp-config.php file isn’t just for database declarations. You can turn it into a security layer by adding the following lines:
# Disable the code editor in the Dashboard (prevents hackers from editing files if admin is compromised)
define( 'DISALLOW_FILE_EDIT', true );
# Prevent plugin installation or updates directly from the Web UI (use WP-CLI or SFTP instead)
define( 'DISALLOW_FILE_MODS', true );
# Force SSL for all admin connections
define( 'FORCE_SSL_ADMIN', true );
# Limit post revisions to avoid database bloat
define( 'WP_POST_REVISIONS', 3 );
Quick tip: Refresh your Salt keys periodically. This will immediately invalidate all old login cookies, which is extremely useful if you suspect a session leak.
4. Removing Footprints (Security by Obscurity)
Don’t let hackers know you’re running an old version of WordPress through meta generator tags. In your theme’s functions.php file, add:
remove_action('wp_head', 'wp_generator'); // Hide WordPress version
add_filter('login_errors', function(){ return 'Invalid login credentials.'; }); // Do not specify if user or pass is wrong
Additionally, disable Web Server identification info to prevent version-specific vulnerability exploits:
# Nginx: /etc/nginx/nginx.conf
server_tokens off;
# PHP: /etc/php/8.1/fpm/php.ini
expose_php = Off
5. Isolating the Database User
Using the root user for WordPress is a disaster. Create a separate user and grant only the necessary permissions. A database compromise involving deletion (DROP) or configuration modification (ALTER) causes much more damage than simple data theft.
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE ON wordpress_db.* TO 'wp_user'@'localhost';
FLUSH PRIVILEGES;
Conclusion
Security is a process, not a destination. By implementing hardening from the Nginx layer down to the source code, you eliminate over 90% of the risks from automated attacks. Always maintain regular backups and update WordPress core as soon as patches are released. Once your system is reinforced, those 2 AM wake-up calls will become a thing of the past.

