WordPress Security Hardening with Nginx

Professional Guide to Server-Level Protection

angelscript

Overview

This comprehensive guide demonstrates how to implement robust security measures for WordPress sites using Nginx directives. By implementing server-level security controls, you can block malicious requests before they reach your WordPress application, significantly reducing the attack surface and improving overall site security.

Key Benefits:
  • Block bad requests at the server level before they reach WordPress
  • Prevent unauthorized access to sensitive files and directories
  • Disable PHP execution in critical directories to prevent code injection
  • Protect against SQL injection, file injection, and common exploits
  • Block spam bots and malicious user agents

Security Architecture Flow

Incoming HTTP Request
Nginx Security Directives
Request Filtering & Validation
WordPress Application (if allowed)

Security Layers Implemented

File Protection

Deny access to sensitive configuration files, readme files, and .ini files

PHP Execution Control

Prevent PHP execution in uploads, plugins, and themes directories

Injection Prevention

Block SQL and file injection attempts through query string filtering

Bot Protection

Block known malicious bots and vulnerability scanners

Step-by-Step Implementation Guide

1Navigate to Nginx Includes Directory

First, ensure you are in the correct directory where Nginx include files are stored.

cd /etc/nginx/includes/

2Create Security Directives Configuration File

Create a new configuration file to store all security directives.

sudo nano nginx_security_directives.conf
Note: The following security directives file contains over 90 rules designed specifically for WordPress security. It is recommended to copy and paste these directives directly into your configuration file.

3Add Security Directives Configuration

Copy and paste the following comprehensive security directives into the nginx_security_directives.conf file:

Complete Nginx Security Directives Configuration

## WORDPRESS-SAFE NGINX 8G (based) FIREWALL Ruleset Low false-positive risks Updated December 2026 Disable favicon logging location = /favicon.ico { access_log off; log_not_found off; } Deny access to sensitive core and config files location = /wp-config.php { deny all; } location = /wp-admin/install.php { deny all; } location ~* ^/(readme|license|licence).(txt|html)$ { deny all; } location ~* .ini$ { deny all; } Harden WP core location ~* ^/wp-includes/[^/]+.php$ { deny all; } location ~* ^/wp-includes/js/tinymce/langs/.+.php$ { deny all; } location ~* ^/wp-includes/theme-compat/ { deny all; } Prevent PHP execution in uploads, themes and plugin directories location ~* ^/wp-content/uploads/.*.(php[1-8]?|pht|phtml?|phps)$ { deny all; } location ~* ^/wp-content/plugins/.*.(php[1-8]?|pht|phtml?|phps)$ { deny all; } location ~* ^/wp-content/themes/.*.(php[1-8]?|pht|phtml?|phps)$ { deny all; } Protect upgrade and backup directories location ~* ^/wp-content/(upgrade|backup-.)/..(php[1-8]?|pht|phtml?|phps)$ { deny all; } Block development and dependency files/dirs location ~* (composer.(json|lock)|package.json|yarn.lock|/vendor/|/node_modules/) { deny all; } Block dangerous or unused HTTP methods if ($request_method ~* ^(TRACE|DELETE|TRACK)$) { return 403; } Block known vulnerability scanners if ($http_user_agent ~* (nikto|sqlmap|masscan|nmap|dirbuster|acunetix|openvas)) { return 444; }
angelscript
Important: The XMLRPC directive at the top of the file should remain commented for now. This will be discussed in a later section of the course.

4Save the Configuration File

After pasting the directives, save and close the file (in nano: Ctrl+X, then Y, then Enter).

5Get the Full Path to the Configuration File

Use the readlink command to obtain the absolute path to your newly created configuration file.

readlink -f nginx_security_directives.conf
Example Output: /etc/nginx/includes/nginx_security_directives.conf

6Navigate to Sites Available Directory

Change to the directory containing your site's Nginx server block configuration.

cd /etc/nginx/sites-available/

7Edit Your Site's Server Block Configuration

Open your site's Nginx configuration file for editing.

sudo nano example.com.conf

8Include the Security Directives File

Add the include directive in your server block. Place it directly above the PHP processing location block.

Placement in Server Block

# Place ABOVE the php processing location block include /etc/nginx/includes/nginx_security_directives.conf;
angelscript
Tip: Locate the PHP processing block (typically location ~ \.php$ {) and add the include directive immediately before it.

9Test Nginx Configuration

Always test your Nginx configuration before reloading to catch any syntax errors.

sudo nginx -t
Expected Output:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

10Reload Nginx

If the syntax test is successful, reload Nginx to apply the new configuration.

sudo systemctl reload nginx

Testing the Security Implementation

Test 1: Verify Readme File Protection

Before implementing security directives, WordPress readme files were publicly accessible. Let's verify they are now blocked.

Test Access to Readme File:

Open your browser and navigate to:

https://example.com/readme.html
Expected Result: You should receive a 403 Forbidden error, confirming that access to sensitive files is now blocked.

Test 2: Verify PHP Execution is Blocked in Plugins Directory

Let's create a test PHP file in the plugins directory to verify that PHP execution is properly blocked.

Navigate to Your Site's Root Directory:

cd /var/www/example.com/

Change to WordPress Content Directory:

cd public_html/wp-content/

Navigate to Plugins Directory:

cd plugins

Create a Test PHP Info File:

sudo nano test556.php

Add PHP Info Code:

<?php phpinfo(); ?>
angelscript

Save and close the file.

Test in Browser:

Navigate to the test file in your browser:

https://example.com/wp-content/plugins/test556.php
Expected Result: You should receive a 403 Forbidden error, confirming that PHP execution in the plugins directory is blocked.

Allowing PHP Execution for Specific Plugins

Important Consideration: While we have blocked PHP execution in the plugins directory for security, some legitimate plugins may require the ability to execute specific PHP files. Rather than opening the entire plugins directory to PHP execution, we create targeted exceptions for specific files.

Understanding the Security Approach

The security model follows these principles:

Exception Handling Flow

Plugin Requires PHP Execution
Identify Exact File Path
Create Location Exception in Server Block
Specific File Can Execute, Others Remain Blocked

How to Identify Required PHP Files

When a plugin needs to execute a PHP file, you can identify it through:

Step-by-Step: Creating a PHP Execution Exception

1Determine the Full File Path

Use the readlink command to get the absolute path to the plugin file:

readlink -f test556.php
Example Output: /var/www/example.com/public_html/wp-content/plugins/test556.php

2Open Your Site's Server Block Configuration

sudo nano /etc/nginx/sites-available/example.com.conf

3Locate the PHP Processing Block

Find the location context that processes PHP files (typically looks like this):

location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_param HTTP_HOST $host; fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock; include /etc/nginx/includes/fastcgi_optimize.conf; }
angelscript

4Add Exception Location Block

Important: Add the new location block immediately after (underneath) the closing brace of the PHP processing block.

Critical Note About Path Specification:
The path in the location directive must be relative to the root directive defined in your server block. Do NOT use the absolute file system path.

Understanding Root-Relative Paths

If your server block defines:

root /var/www/example.com/public_html;

Then the location path should be relative to this root:

Correct Exception Configuration

location = /wp-content/plugins/test556.php { allow all; include snippets/fastcgi-php.conf; fastcgi_param HTTP_HOST $host; fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock; include /etc/nginx/includes/fastcgi_optimize.conf; }
angelscript
Explanation:
Absolute Path: /var/www/example.com/public_html/wp-content/plugins/test556.php
Root Directive: /var/www/example.com/public_html
Relative Path for Location: /wp-content/plugins/test556.php

Complete Server Block Example

server { listen 443 ssl; http2 on; server_name example.com www.example.com; root /var/www/example.com/public_html; index index.php index.html; # Security directives include include /etc/nginx/includes/nginx_security_directives.conf; # PHP processing block location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_param HTTP_HOST $host; fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock; include /etc/nginx/includes/fastcgi_optimize.conf; } # Exception for specific plugin file location = /wp-content/plugins/test556.php { allow all; include snippets/fastcgi-php.conf; fastcgi_param HTTP_HOST $host; fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock; include /etc/nginx/includes/fastcgi_optimize.conf; } # Other configurations... }
angelscript

5Test and Apply Configuration

Test the Nginx configuration:

sudo nginx -t

Reload Nginx to apply changes:

sudo systemctl reload nginx

Restart PHP-FPM to ensure all changes take effect:

sudo systemctl restart php8.3-fpm

6Test the Exception

Navigate to the file in your browser:

https://example.com/wp-content/plugins/test556.php
Expected Result: The PHP info page should now display correctly, confirming that this specific file can execute while all other PHP files in the plugins directory remain blocked.

Clean Up Test Files

After testing, remove the test PHP file:

cd /var/www/example.com/public_html/wp-content/plugins/
sudo rm test556.php

Remove the test exception from your server block:

sudo nano /etc/nginx/sites-available/example.com.conf

Delete the location block for test556.php, then test and reload:

sudo nginx -t
sudo systemctl reload nginx

Security Directives Breakdown

Protection Type What It Does Example
File Access Control Denies access to sensitive configuration files wp-config.php, readme.txt, license.txt
PHP Execution Prevention Blocks PHP execution in critical directories uploads/, plugins/, themes/
Core File Protection Protects WordPress core files from direct access wp-includes/*.php files
Development File Blocking Prevents access to development dependencies composer.json, package.json, /vendor/
HTTP Method Filtering Blocks dangerous HTTP methods TRACE, DELETE, TRACK
User Agent Filtering Blocks known malicious scanners and bots nikto, sqlmap, masscan

Best Practices and Recommendations

Security Best Practices:

  • Regular Updates: Keep your security directives updated as new threats emerge
  • Log Monitoring: Regularly check your Nginx error logs for blocked attempts
  • Minimal Exceptions: Only create exceptions for files that absolutely require PHP execution
  • Documentation: Document all exceptions you create and why they are necessary
  • Testing: Always test in a staging environment before applying to production

Common Pitfalls to Avoid:

  • Never disable all PHP blocking in the plugins directory – create specific exceptions instead
  • Don't forget to test configuration with sudo nginx -t before reloading
  • Remember to use root-relative paths in location directives, not absolute file system paths
  • Don't skip restarting PHP-FPM after making PHP-related changes

Monitoring and Maintenance

Check Nginx Access Logs

Monitor which requests are being blocked:

sudo tail -f /var/log/nginx/access.log

Check Nginx Error Logs

View detailed error information:

sudo tail -f /var/log/nginx/error.log

Check PHP-FPM Logs

Monitor PHP-specific issues:

sudo tail -f /var/log/fpm-php.example.com.log

Troubleshooting

Issue: 403 Forbidden on Legitimate Plugin

Solution: Create a specific location exception for the required plugin file as described in the "Allowing PHP Execution for Specific Plugins" section.

Issue: 404 Not Found Instead of 403 Forbidden

Possible Cause: The location directive path may be incorrect. Verify that the path is relative to your root directive.

Issue: Configuration Test Fails

Solution: Carefully review the error message from sudo nginx -t. Common issues include:

Issue: Changes Not Taking Effect

Solution: Ensure you've reloaded Nginx and restarted PHP-FPM:

sudo systemctl reload nginx && sudo systemctl restart php8.3-fpm

Summary and Conclusion

What We've Accomplished:

  • Created comprehensive Nginx security directives to protect WordPress
  • Implemented server-level blocking of malicious requests
  • Prevented PHP execution in critical directories
  • Protected sensitive configuration and readme files
  • Learned how to create targeted exceptions for legitimate plugin needs
  • Established a security model based on "default deny, explicit allow"

By implementing these Nginx security directives, you have added a robust layer of protection to your WordPress site at the web server level. This approach stops attacks before they can even reach your WordPress application, significantly reducing server load from malicious traffic and protecting against common exploits.

Next Steps: Continue to monitor your logs, keep your security directives updated, and document any exceptions you create for plugin functionality. Remember that security is an ongoing process, not a one-time configuration.