Skip to content

15. Hardening WordPress Security

WordPress powers over 40% of all websites on the internet, making it a prime target for attackers. This section covers essential security measures to protect your WordPress installation from common threats.

The security measures in this guide follow a defense-in-depth approach, implementing multiple layers of protection:

  1. Isolation: Separate WordPress sites using dedicated users and PHP-FPM pools
  2. Access Control: Restrict file system access and set proper permissions
  3. Encryption: Implement SSL/TLS with optimal security settings
  4. Request Filtering: Block malicious requests before they reach WordPress
  5. Application Hardening: Limit WordPress functionality that could be exploited

By implementing these measures, you’ll significantly reduce the attack surface of your WordPress installation and make it much more difficult for attackers to compromise your site.

Before implementing security measures, it’s important to understand how to monitor your server for suspicious activity.

Nginx logs provide valuable information about requests to your website, including potential attack attempts:

# Navigate to the Nginx logs directory
cd /var/log/nginx
# List available log files
ls
# View the contents of a specific log file
sudo cat access_example.com.log
# View logs with pagination for large files
sudo less error_example.com.log

Look for patterns like repeated failed login attempts, unusual HTTP methods, or requests to non-existent PHP files, which could indicate attack attempts.

15.3. Separating Sites Using PHP Pools - Part 1

Section titled “15.3. Separating Sites Using PHP Pools - Part 1”

15.3.1. Creating a Dedicated User for WordPress

Section titled “15.3.1. Creating a Dedicated User for WordPress”

A key security principle is to run WordPress under a dedicated user account rather than the default www-data user. This provides better isolation and control.

First, create a dedicated user for your WordPress site:

# Create a new user for your WordPress site
sudo useradd wordpress_user

Configure the proper group relationships to allow both the web server and your site-specific user to access the necessary files:

# Add www-data to the WordPress user group
sudo usermod -a -G wordpress_user www-data
# Add WordPress user to www-data group
sudo usermod -a -G www-data wordpress_user
# If needed, add your admin user to the WordPress user group
sudo usermod -a -G wordpress_user admin_username

This setup creates proper isolation while maintaining the necessary access permissions.

15.4. Separating Sites Using PHP Pools - Part 2

Section titled “15.4. Separating Sites Using PHP Pools - Part 2”

By default, all PHP processing happens through a single PHP-FPM pool. Creating a dedicated pool for each WordPress site provides better security isolation and resource control.

15.4.1. Creating a Site-Specific PHP-FPM Pool

Section titled “15.4.1. Creating a Site-Specific PHP-FPM Pool”
# Navigate to the PHP-FPM pool configuration directory
cd /etc/php/8.3/fpm/pool.d/
# Create a new pool configuration by copying the default
sudo cp www.conf example.com.conf
# Edit the new pool configuration
sudo vim example.com.conf

Modify the pool configuration file with these security-focused settings:

; Change the pool name from 'www' to your site name
[example]
; Set the user and group to your site-specific user
user = wordpress_user
group = wordpress_user
; Use a site-specific socket file
listen = /run/php/php8.3-fpm-example.com.sock
; Set resource limits
rlimit_files = 15000
rlimit_core = 100
; Disable error display but enable logging
php_flag[display_errors] = off
php_admin_flag[log_errors] = on
php_admin_value[error_log] = /var/log/fpm-php.example.com.log

Add these directives to further secure the PHP-FPM socket:

; Set socket ownership and permissions
listen.owner = www-data
listen.group = www-data
listen.mode = 0600

These settings:

  • Restrict socket access to only the web server user
  • Prevent other users on the system from connecting to your PHP-FPM pool
# Create the log file
sudo touch /var/log/fpm-php.example.com.log
# Set appropriate ownership
sudo chown wordpress_user:www-data /var/log/fpm-php.example.com.log
# Set appropriate permissions
sudo chmod 660 /var/log/fpm-php.example.com.log

15.4.5. Applying the PHP-FPM Configuration

Section titled “15.4.5. Applying the PHP-FPM Configuration”
# Reload PHP-FPM to apply changes
sudo systemctl reload php8.3-fpm
# Verify the socket path in your configuration
sudo grep "listen = /" example.com.conf

15.4.6. Updating Nginx to Use the New PHP-FPM Pool

Section titled “15.4.6. Updating Nginx to Use the New PHP-FPM Pool”

Edit your Nginx server block to use the new PHP-FPM socket:

# Edit your site's Nginx configuration
sudo vim /etc/nginx/sites-available/example.com.conf

Find the PHP processing section and update the fastcgi_pass directive:

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

Apply the changes:

# Test Nginx configuration
sudo nginx -t
# Reload Nginx if the test is successful
sudo systemctl reload nginx

Proper file ownership and permissions are critical for WordPress security. The goal is to allow WordPress to function normally while restricting access to sensitive files.

For a balance of security and functionality, use these permissions:

# Set ownership to the WordPress user
sudo chown -R wordpress_user:wordpress_user /var/www/example.com/public_html/
# Set directory permissions to 770 (owner and group can read, write, execute)
sudo find /var/www/example.com/public_html/ -type d -exec chmod 770 {} \;
# Set file permissions to 660 (owner and group can read and write)
sudo find /var/www/example.com/public_html/ -type f -exec chmod 660 {} \;
# Set wp-config.php to be readable only by the owner
sudo chmod 400 /var/www/example.com/public_html/wp-config.php

For maximum security (may require adjustments for some plugins):

# Set ownership to the WordPress user
sudo chown -R wordpress_user:wordpress_user /var/www/example.com/public_html/
# Set directory permissions to 550 (owner and group can read and execute)
sudo find /var/www/example.com/public_html/ -type d -exec chmod 550 {} \;
# Set file permissions to 440 (owner and group can read)
sudo find /var/www/example.com/public_html/ -type f -exec chmod 440 {} \;
# Allow write access to wp-content directory for updates and uploads
sudo find /var/www/example.com/public_html/wp-content/ -type d -exec chmod 770 {} \;
sudo find /var/www/example.com/public_html/wp-content/ -type f -exec chmod 660 {} \;

15.6. Limit File System Access using PHP Open_BaseDir

Section titled “15.6. Limit File System Access using PHP Open_BaseDir”

The open_basedir directive restricts which files PHP can access, preventing attackers from accessing files outside your WordPress installation.

15.6.1. Configuring PHP Temporary Directories

Section titled “15.6.1. Configuring PHP Temporary Directories”

First, create a dedicated temporary directory for your WordPress site:

# Create a temporary directory
cd /var/www/example.com/
sudo mkdir tmp/
# Set proper ownership and permissions
sudo chown wordpress_user:wordpress_user tmp/
sudo chmod 770 tmp/

15.6.2. Setting Up open_basedir Restriction

Section titled “15.6.2. Setting Up open_basedir Restriction”

Edit your PHP-FPM pool configuration to add the open_basedir directive:

# Edit the PHP-FPM pool configuration
sudo vim /etc/php/8.3/fpm/pool.d/example.com.conf

Add these lines to the configuration:

; Set custom temporary directories
php_admin_value[upload_tmp_dir] = /var/www/example.com/tmp/
php_admin_value[sys_temp_dir] = /var/www/example.com/tmp/
; Restrict PHP file access to specific directories
php_admin_value[open_basedir] = /var/www/example.com/public_html/:/var/www/example.com/tmp/

Apply the changes:

# Reload PHP-FPM
sudo systemctl reload php8.3-fpm

Restrict PHP functions that could be used for malicious purposes:

; Disable potentially dangerous PHP functions
php_admin_value[disable_functions] = shell_exec, opcache_get_configuration, opcache_get_status, disk_total_space, diskfreespace, dl, exec, passthru, pclose, pcntl_alarm, pcntl_exec, pcntl_fork, pcntl_get_last_error, pcntl_getpriority, pcntl_setpriority, pcntl_signal, pcntl_signal_dispatch, pcntl_sigprocmask, pcntl_sigtimedwait, pcntl_sigwaitinfo, pcntl_strerror, pcntl_waitpid, pcntl_wait, pcntl_wexitstatus, pcntl_wifcontinued, pcntl_wifexited, pcntl_wifsignaled, pcntl_wifstopped, pcntl_wstopsig, pcntl_wtermsig, popen, posix_getpwuid, posix_kill, posix_mkfifo, posix_setpgid, posix_setsid, posix_setuid, posix_uname, proc_close, proc_get_status, proc_nice, proc_open, proc_terminate, show_source, system

15.7. Install and Configure FREE SSL Certificates - Part 1

Section titled “15.7. Install and Configure FREE SSL Certificates - Part 1”

HTTPS encryption is essential for WordPress security. Let’s Encrypt provides free SSL certificates that are trusted by all major browsers.

Certbot is a tool that automates the process of obtaining and renewing Let’s Encrypt certificates:

# Update package lists
sudo apt update
# Install Certbot and the Nginx plugin
sudo apt install certbot python3-certbot-nginx

Use the webroot plugin to obtain a certificate without interrupting your website:

# Obtain a certificate for your domain
sudo certbot certonly --webroot -w /var/www/example.com/public_html/ -d example.com -d www.example.com

Follow the prompts to complete the certificate issuance process.

15.8. Install and Configure FREE SSL Certificates - Part 2

Section titled “15.8. Install and Configure FREE SSL Certificates - Part 2”

To strengthen the security of your SSL/TLS implementation, create a strong Diffie-Hellman parameter file:

# Create the SSL directory
cd /etc/nginx/
sudo mkdir ssl/
cd ssl/
# Generate DH parameters (this may take a few minutes)
sudo openssl dhparam -out dhparam.pem 2048

15.8.2. Creating Site-Specific SSL Configuration

Section titled “15.8.2. Creating Site-Specific SSL Configuration”

Create a configuration file for your site’s SSL certificates:

# Create the SSL certificate configuration file
sudo vim /etc/nginx/ssl/ssl_certs_example.com.conf

Add the following content:

# Certificate paths
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# SSL stapling certificate
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

15.9. Install and Configure FREE SSL Certificates - Part 3

Section titled “15.9. Install and Configure FREE SSL Certificates - Part 3”

Create a global SSL configuration file with optimal security settings:

# Create the global SSL configuration file
sudo vim /etc/nginx/ssl/ssl_all_sites.conf

Add the following content:

# CONFIGURATION RESULTS IN A+ RATING AT SSLLABS.COM
# WILL UPDATE DIRECTIVES TO MAINTAIN A+ RATING - CHECK DATE
# DATE: OCTOBER 2024
# SSL CACHING AND PROTOCOLS
ssl_session_cache shared:SSL:20m;
ssl_session_timeout 180m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
# ssl_ciphers must be on a single line, do not split over multiple lines
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
ssl_stapling on;
ssl_stapling_verify on;
# resolver set to Cloudflare
# timeout can be set up to 30s
resolver 1.1.1.1 1.0.0.1;
resolver_timeout 15s;
ssl_session_tickets off;
# HSTS HEADERS
add_header Strict-Transport-Security "max-age=31536000;" always;
# After setting up ALL of your sub domains - comment the above and uncomment the directive below
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains;" always;
# Enable QUIC and HTTP/3
ssl_early_data on;
add_header Alt-Svc 'h3=":$server_port"; ma=86400';
add_header x-quic 'H3';
quic_retry on;

Update your Nginx server block to use HTTPS:

# Edit your site's Nginx configuration
sudo vim /etc/nginx/sites-available/example.com.conf

Add a server block to redirect HTTP to HTTPS:

# Redirect HTTP to HTTPS
server {
listen 80;
server_name example.com www.example.com;
# 301 Permanent redirect
return 301 https://example.com$request_uri;
}

Configure the HTTPS server block:

# HTTPS server block
server {
# Enable SSL, HTTP/2, and HTTP/3
listen 443 ssl;
http2 on;
# HTTP/3 support
listen 443 quic reuseport;
http3 on;
server_name example.com www.example.com;
# Include SSL configuration files
include /etc/nginx/ssl/ssl_certs_example.com.conf;
include /etc/nginx/ssl/ssl_all_sites.conf;
# Rest of your server configuration...
}

Apply the changes:

# Test Nginx configuration
sudo nginx -t
# Reload Nginx if the test is successful
sudo systemctl reload nginx

Verify that your SSL configuration is working correctly:

# Test HTTP to HTTPS redirection
curl -I http://example.com
# Test HTTPS connection
curl -I https://example.com

You can also test your SSL configuration using online tools:

15.9.4. Setting Up Certificate Auto-Renewal

Section titled “15.9.4. Setting Up Certificate Auto-Renewal”

Let’s Encrypt certificates expire after 90 days, so automatic renewal is essential:

# Edit the root crontab
sudo crontab -e

Add the following lines:

# Renew certificates twice a month and reload Nginx
00 1 14,28 * * certbot renew --force-renewal --deploy-hook "systemctl reload nginx" >> /var/log/certbot-renew.log 2>&1

Set appropriate permissions for the log file:

# Create the log file if it doesn't exist
sudo touch /var/log/certbot-renew.log
# Set appropriate permissions
sudo chmod 644 /var/log/certbot-renew.log

15.10. Hardening the HTTP Response Headers

Section titled “15.10. Hardening the HTTP Response Headers”

HTTP security headers help protect your site from various attacks like XSS, clickjacking, and content type sniffing.

15.10.1. Creating a Security Headers Configuration

Section titled “15.10.1. Creating a Security Headers Configuration”

Create a configuration file for security headers:

# Create the security headers configuration file
cd /etc/nginx/includes/
sudo vim http_headers.conf

Add the following content:

# Referrer Policy
# Controls how much referrer information is included with requests
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Content Type Options
# Prevents browsers from MIME-sniffing a response away from the declared content-type
add_header X-Content-Type-Options "nosniff" always;
# Frame Options
# Protects against clickjacking by preventing your page from being displayed in a frame
add_header X-Frame-Options "sameorigin" always;
# XSS Protection
# Enables the cross-site scripting (XSS) filter in browsers
add_header X-XSS-Protection "1; mode=block" always;
# Permissions Policy
# Controls which browser features and APIs can be used
add_header Permissions-Policy "geolocation=(),midi=(),sync-xhr=(),microphone=(),camera=(),magnetometer=(),gyroscope=(),fullscreen=self,payment=()" always;

15.10.2. Creating a Caching and Security Headers Configuration

Section titled “15.10.2. Creating a Caching and Security Headers Configuration”

For static content, create a configuration that combines caching and security headers:

# Create the caching and security headers configuration file
sudo vim /etc/nginx/includes/browser_caching_security_headers.conf

Add the following content:

# Enable browser caching
expires 30d;
etag on;
if_modified_since exact;
add_header Pragma "public" always;
add_header Cache-Control "public, no-transform" always;
# Include security headers
include /etc/nginx/includes/http_headers.conf;
# Disable access logging for static content
access_log off;

Add the security headers to your Nginx server block:

# Edit your site's Nginx configuration
sudo vim /etc/nginx/sites-available/example.com.conf

Add the include directive above the PHP processing location block:

# Include security headers
include /etc/nginx/includes/http_headers.conf;

Apply the changes:

# Test Nginx configuration
sudo nginx -t
# Reload Nginx if the test is successful
sudo systemctl reload nginx

15.11. Using Nginx Directives to Protect WordPress

Section titled “15.11. Using Nginx Directives to Protect WordPress”

Nginx can be configured to block access to sensitive WordPress files and prevent common attack vectors.

15.11.1. Creating WordPress Security Directives

Section titled “15.11.1. Creating WordPress Security Directives”

Create a configuration file for WordPress security directives:

# Create the WordPress security directives file
cd /etc/nginx/includes/
sudo vim nginx_security_directives.conf

Add the following content:

# Deny access to sensitive WordPress files
location = /wp-config.php { deny all; }
location = /wp-admin/install.php { deny all; }
location ~* /readme\.html$ { deny all; }
location ~* /readme\.txt$ { deny all; }
location ~* /licence\.txt$ { deny all; }
location ~* /license\.txt$ { deny all; }
location ~ ^/wp-admin/includes/ { deny all; }
location ~ ^/wp-includes/[^/]+\.php$ { deny all; }
location ~ ^/wp-includes/js/tinymce/langs/.+\.php$ { deny all; }
location ~ ^/wp-includes/theme-compat/ { deny all; }
# Disable PHP execution in potentially dangerous directories
location ~* ^/wp\-content/uploads/.*\.(?:php[1-7]?|pht|phtml?|phps)$ { deny all; }
location ~* ^/wp\-content/plugins/.*\.(?:php[1-7]?|pht|phtml?|phps)$ { deny all; }
location ~* ^/wp\-content/themes/.*\.(?:php[1-7]?|pht|phtml?|phps)$ { deny all; }
# Disable access to the site PHP override file
location = /.user.ini { deny all; }
# Filter suspicious request methods
if ( $request_method ~* ^(TRACE|DELETE|TRACK)$ ) { return 403; }
# Filter suspicious query strings in the URL
set $susquery 0;
if ( $args ~* "\.\./" ) { set $susquery 1; }
if ( $args ~* "\.(bash|git|hg|log|svn|swp|cvs)" ) { set $susquery 1; }
if ( $args ~* "etc/passwd" ) { set $susquery 1; }
if ( $args ~* "boot\.ini" ) { set $susquery 1; }
if ( $args ~* "ftp:" ) { set $susquery 1; }
if ( $args ~* "(<|%3C)script(>|%3E)" ) { set $susquery 1; }
if ( $args ~* "mosConfig_[a-zA-Z_]{1,21}(=|%3D)" ) { set $susquery 1; }
if ( $args ~* "base64_decode\(" ) { set $susquery 1; }
if ( $args ~* "%24&x" ) { set $susquery 1; }
if ( $args ~* "127\.0" ) { set $susquery 1; }
if ( $args ~* "(globals|encode|request|localhost|loopback|request|insert|concat|union|declare)" ) { set $susquery 1; }
if ( $args ~* "%[01][0-9A-F]" ) { set $susquery 1; }
# Exceptions for legitimate WordPress functionality
if ( $args ~ "^loggedout=true" ) { set $susquery 0; }
if ( $args ~ "^action=jetpack-sso" ) { set $susquery 0; }
if ( $args ~ "^action=rp" ) { set $susquery 0; }
if ( $http_cookie ~ "wordpress_logged_in_" ) { set $susquery 0; }
if ( $http_referer ~* "^https?://maps\.googleapis\.com/" ) { set $susquery 0; }
if ( $susquery = 1 ) { return 403; }
# Block common SQL injections
set $block_sql_injections 0;
if ($query_string ~ "union.*select.*\(") { set $block_sql_injections 1; }
if ($query_string ~ "union.*all.*select.*") { set $block_sql_injections 1; }
if ($query_string ~ "concat.*\(") { set $block_sql_injections 1; }
if ($block_sql_injections = 1) { return 403; }
# Block file injections
set $block_file_injections 0;
if ($query_string ~ "[a-zA-Z0-9_]=http://") { set $block_file_injections 1; }
if ($query_string ~ "[a-zA-Z0-9_]=(\.\.//?)+") { set $block_file_injections 1; }
if ($query_string ~ "[a-zA-Z0-9_]=/([a-z0-9_.]//?)+") { set $block_file_injections 1; }
if ($block_file_injections = 1) { return 403; }
# Block common exploits
set $block_common_exploits 0;
if ($query_string ~ "(<|%3C).*script.*(>|%3E)") { set $block_common_exploits 1; }
if ($query_string ~ "GLOBALS(=|\[|\%[0-9A-Z]{0,2})") { set $block_common_exploits 1; }
if ($query_string ~ "_REQUEST(=|\[|\%[0-9A-Z]{0,2})") { set $block_common_exploits 1; }
if ($query_string ~ "proc/self/environ") { set $block_common_exploits 1; }
if ($query_string ~ "mosConfig_[a-zA-Z_]{1,21}(=|\%3D)") { set $block_common_exploits 1; }
if ($query_string ~ "base64_(en|de)code\(.*\)") { set $block_common_exploits 1; }
if ($block_common_exploits = 1) { return 403; }
# Block spam
set $block_spam 0;
if ($query_string ~ "\b(ultram|unicauca|valium|viagra|vicodin|xanax|ypxaieo)\b") { set $block_spam 1; }
if ($query_string ~ "\b(erections|hoodia|huronriveracres|impotence|levitra|libido)\b") { set $block_spam 1; }
if ($query_string ~ "\b(ambien|blue\spill|cialis|cocaine|ejaculation|erectile)\b") { set $block_spam 1; }
if ($query_string ~ "\b(lipitor|phentermin|pro[sz]ac|sandyauer|tramadol|troyhamby)\b") { set $block_spam 1; }
if ($block_spam = 1) { return 403; }
# Block malicious user agents
set $block_user_agents 0;
if ($http_user_agent ~ "Indy Library") { set $block_user_agents 1; }
if ($http_user_agent ~ "libwww-perl") { set $block_user_agents 1; }
if ($http_user_agent ~ "GetRight") { set $block_user_agents 1; }
if ($http_user_agent ~ "GetWeb!") { set $block_user_agents 1; }
if ($http_user_agent ~ "Go!Zilla") { set $block_user_agents 1; }
if ($http_user_agent ~ "Download Demon") { set $block_user_agents 1; }
if ($http_user_agent ~ "Go-Ahead-Got-It") { set $block_user_agents 1; }
if ($http_user_agent ~ "TurnitinBot") { set $block_user_agents 1; }
if ($http_user_agent ~ "GrabNet") { set $block_user_agents 1; }
# Block common security scanning tools
if ($http_user_agent ~ "dirbuster") { set $block_user_agents 1; }
if ($http_user_agent ~ "nikto") { set $block_user_agents 1; }
if ($http_user_agent ~ "sqlmap") { set $block_user_agents 1; }
if ($http_user_agent ~ "fimap") { set $block_user_agents 1; }
if ($http_user_agent ~ "nessus") { set $block_user_agents 1; }
if ($http_user_agent ~ "whatweb") { set $block_user_agents 1; }
if ($http_user_agent ~ "Openvas") { set $block_user_agents 1; }
if ($http_user_agent ~ "jbrofuzz") { set $block_user_agents 1; }
if ($http_user_agent ~ "libwhisker") { set $block_user_agents 1; }
if ($http_user_agent ~ "webshag") { set $block_user_agents 1; }
if ($http_user_agent ~ "Acunetix") { set $block_user_agents 1; }
if ($block_user_agents = 1) { return 403; }

15.11.2. Implementing WordPress Security Directives

Section titled “15.11.2. Implementing WordPress Security Directives”

Add the security directives to your Nginx server block:

# Edit your site's Nginx configuration
sudo vim /etc/nginx/sites-available/example.com.conf

Add the include directive above the PHP processing location block:

# Include WordPress security directives
include /etc/nginx/includes/nginx_security_directives.conf;

Apply the changes:

# Test Nginx configuration
sudo nginx -t
# Reload Nginx if the test is successful
sudo systemctl reload nginx

15.11.3. Allowing PHP Execution for Specific Plugin Files

Section titled “15.11.3. Allowing PHP Execution for Specific Plugin Files”

Some plugins require PHP execution in the plugins directory. Since we’ve blocked PHP execution in this directory for security, we need to selectively allow it for legitimate plugin files.

First, verify that PHP execution is blocked in the plugins directory:

# Create a test PHP file in the plugins directory
cd /var/www/example.com/
sudo vim public_html/wp-content/plugins/test.php

Add this content to the test file:

<?php
phpinfo();
?>

When you try to access this file in your browser (https://example.com/wp-content/plugins/test.php), you should receive a 403 Forbidden error, confirming that PHP execution is blocked.

To allow PHP execution for specific plugin files, add a location block for each file:

# Edit your site's Nginx configuration
sudo vim /etc/nginx/sites-available/example.com.conf

Add a location block for each plugin file that needs PHP execution:

# Allow PHP execution for specific plugin files
location = /wp-content/plugins/specific-plugin/file.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;
}

Apply the changes:

# Test Nginx configuration
sudo nginx -t
# Reload Nginx and restart PHP-FPM
sudo systemctl reload nginx && sudo systemctl restart php8.3-fpm

Don’t forget to remove the test file:

# Remove the test PHP file
sudo rm /var/www/example.com/public_html/wp-content/plugins/test.php

15.12. Nginx DDoS Protection and WordPress

Section titled “15.12. Nginx DDoS Protection and WordPress”

Hot linking is when other websites link directly to your images or files, consuming your bandwidth. Prevent this with Nginx:

# Edit your site's Nginx configuration
sudo vim /etc/nginx/sites-available/example.com.conf

Add the following to your server block:

# Prevent hot linking of images
location ~* \.(jpg|jpeg|png|gif|webp|svg)$ {
valid_referers none blocked server_names *.example.com example.com;
if ($invalid_referer) {
return 403;
}
# Include browser caching for images
include /etc/nginx/includes/browser_caching_security_headers.conf;
}

15.13. Stop Brute Force Attacks - Nginx Rate Limiting

Section titled “15.13. Stop Brute Force Attacks - Nginx Rate Limiting”

Rate limiting helps prevent brute force attacks by limiting the number of requests a client can make in a given time period.

First, add the rate limiting zone to your main Nginx configuration:

# Edit the main Nginx configuration
cd /etc/nginx/
sudo vim nginx.conf

Add the following to the http context:

# Rate limiting zone for WordPress
limit_req_zone $binary_remote_addr zone=wp:10m rate=30r/m;

This creates a 10MB zone named “wp” that limits clients to 30 requests per minute.

15.13.2. Creating a Rate Limiting Configuration

Section titled “15.13.2. Creating a Rate Limiting Configuration”

Create a configuration file for rate limiting WordPress login and XML-RPC:

# Create the rate limiting configuration file
cd /etc/nginx/includes/
sudo vim rate_limiting.conf

Add the following content:

# Rate limit WordPress login page
location = /wp-login.php {
limit_req zone=wp burst=20 nodelay;
limit_req_status 444;
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;
}
# Rate limit WordPress XML-RPC (often used in brute force attacks)
location = /xmlrpc.php {
limit_req zone=wp burst=20 nodelay;
limit_req_status 444;
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;
}

Add the rate limiting configuration to your Nginx server block:

# Edit your site's Nginx configuration
sudo vim /etc/nginx/sites-available/example.com.conf

Add the include directive:

# Include rate limiting configuration
include /etc/nginx/includes/rate_limiting.conf;

Apply the changes:

# Test Nginx configuration
sudo nginx -t
# Reload Nginx if the test is successful
sudo systemctl reload nginx

WordPress allows administrators to edit theme and plugin files directly from the admin dashboard. This can be a security risk if an attacker gains access to your admin account.

15.14.1. Disabling File Modifications in wp-config.php

Section titled “15.14.1. Disabling File Modifications in wp-config.php”

Edit your wp-config.php file to disable file modifications:

# Edit wp-config.php
sudo vim /var/www/example.com/public_html/wp-config.php

Add the following line before the “That’s all, stop editing!” comment:

/* Disable file modifications */
define('DISALLOW_FILE_MODS', true);

Apply the changes:

# Reload PHP-FPM
sudo systemctl reload php8.3-fpm

15.15. Restricting Database User Privileges

Section titled “15.15. Restricting Database User Privileges”

The WordPress database user should have only the privileges it needs to function, not full access to the database.

15.15.1. Limiting Database User Privileges

Section titled “15.15.1. Limiting Database User Privileges”

Connect to your MariaDB server and restrict the WordPress database user’s privileges:

# Connect to MariaDB
sudo mysql -u root -p

Execute these SQL commands:

-- Revoke all privileges
REVOKE ALL PRIVILEGES ON wordpress_db.* FROM 'wordpress_user'@'localhost';
-- Grant only necessary privileges
GRANT SELECT, INSERT, UPDATE, DELETE ON wordpress_db.* TO 'wordpress_user'@'localhost';
-- Apply the changes
FLUSH PRIVILEGES;

Exit the MariaDB console:

EXIT;

The WordPress REST API can be a target for attackers. By default, it’s accessible to anyone, but we can restrict access to authenticated users only.

Add the following code to your theme’s functions.php file or a custom plugin:

/**
* Restrict REST API to authenticated users
*/
function restrict_rest_api_to_authenticated_users($access) {
// If not authenticated, block access
if (!is_user_logged_in()) {
return new WP_Error('rest_api_restricted', 'REST API access is restricted to authenticated users.', array('status' => 401));
}
// Otherwise, allow access
return $access;
}
add_filter('rest_authentication_errors', 'restrict_rest_api_to_authenticated_users');

For an additional layer of security, you can also restrict access to the REST API at the Nginx level:

# Edit your site's Nginx configuration
sudo vim /etc/nginx/sites-available/example.com.conf

Add the following location block:

# Restrict WP REST API
location /wp-json/ {
# Allow access only if logged in or from specific IPs
if ($http_cookie !~* "wordpress_logged_in_") {
# Allow specific IPs (replace with your IP)
allow 192.168.1.1;
# Block everyone else
deny all;
}
# Continue processing the request
try_files $uri $uri/ /index.php?$args;
}

Apply the changes:

# Test Nginx configuration
sudo nginx -t
# Reload Nginx if the test is successful
sudo systemctl reload nginx

A Web Application Firewall (WAF) provides an additional layer of security by filtering and monitoring HTTP traffic between a web application and the Internet.

15.17.1. Installing ModSecurity with Nginx

Section titled “15.17.1. Installing ModSecurity with Nginx”

ModSecurity is an open-source WAF that can be integrated with Nginx:

# Install ModSecurity and dependencies
sudo apt update
sudo apt install -y libmodsecurity3 libmodsecurity-dev nginx-module-security

Create a basic ModSecurity configuration:

# Create ModSecurity configuration directory
sudo mkdir -p /etc/nginx/modsec
# Download OWASP Core Rule Set
sudo git clone https://github.com/coreruleset/coreruleset /etc/nginx/modsec/coreruleset
# Create ModSecurity configuration file
sudo vim /etc/nginx/modsec/modsec.conf

Add the following content:

# Basic ModSecurity configuration
SecRuleEngine On
SecRequestBodyAccess On
SecResponseBodyAccess On
SecResponseBodyMimeType text/plain text/html text/xml application/json
SecResponseBodyLimit 1024
# Include OWASP Core Rule Set
Include /etc/nginx/modsec/coreruleset/crs-setup.conf
Include /etc/nginx/modsec/coreruleset/rules/*.conf

Edit your Nginx configuration to enable ModSecurity:

# Edit the main Nginx configuration
sudo vim /etc/nginx/nginx.conf

Add the following to the http context:

# Load ModSecurity module
load_module modules/ngx_http_modsecurity_module.so;

Edit your site’s server block:

# Edit your site's Nginx configuration
sudo vim /etc/nginx/sites-available/example.com.conf

Add ModSecurity directives to your server block:

# Enable ModSecurity
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/modsec.conf;

Apply the changes:

# Test Nginx configuration
sudo nginx -t
# Reload Nginx if the test is successful
sudo systemctl reload nginx

By implementing these security measures, you’ve significantly hardened your WordPress installation against common threats. Remember that security is an ongoing process, and you should:

  1. Regularly update WordPress core, themes, and plugins
  2. Monitor your logs for suspicious activity
  3. Perform regular security audits
  4. Keep backups of your site and database
  5. Stay informed about new security threats and best practices