15. Hardening WordPress Security
15.1. Introduction to WordPress Security
Section titled “15.1. Introduction to 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:
- Isolation: Separate WordPress sites using dedicated users and PHP-FPM pools
- Access Control: Restrict file system access and set proper permissions
- Encryption: Implement SSL/TLS with optimal security settings
- Request Filtering: Block malicious requests before they reach WordPress
- 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.
15.2. Security Monitoring with Logs
Section titled “15.2. Security Monitoring with Logs”Before implementing security measures, it’s important to understand how to monitor your server for suspicious activity.
15.2.1. Checking Nginx Logs
Section titled “15.2.1. Checking Nginx Logs”Nginx logs provide valuable information about requests to your website, including potential attack attempts:
# Navigate to the Nginx logs directorycd /var/log/nginx
# List available log filesls
# View the contents of a specific log filesudo cat access_example.com.log
# View logs with pagination for large filessudo less error_example.com.logLook 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 sitesudo useradd wordpress_user15.3.2. Setting Up User Group Permissions
Section titled “15.3.2. Setting Up User Group Permissions”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 groupsudo usermod -a -G wordpress_user www-data
# Add WordPress user to www-data groupsudo usermod -a -G www-data wordpress_user
# If needed, add your admin user to the WordPress user groupsudo usermod -a -G wordpress_user admin_usernameThis 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 directorycd /etc/php/8.3/fpm/pool.d/
# Create a new pool configuration by copying the defaultsudo cp www.conf example.com.conf
# Edit the new pool configurationsudo vim example.com.conf15.4.2. Configuring the PHP-FPM Pool
Section titled “15.4.2. Configuring the PHP-FPM Pool”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 useruser = wordpress_usergroup = wordpress_user
; Use a site-specific socket filelisten = /run/php/php8.3-fpm-example.com.sock
; Set resource limitsrlimit_files = 15000rlimit_core = 100
; Disable error display but enable loggingphp_flag[display_errors] = offphp_admin_flag[log_errors] = onphp_admin_value[error_log] = /var/log/fpm-php.example.com.log15.4.3. Hardening Socket Permissions
Section titled “15.4.3. Hardening Socket Permissions”Add these directives to further secure the PHP-FPM socket:
; Set socket ownership and permissionslisten.owner = www-datalisten.group = www-datalisten.mode = 0600These settings:
- Restrict socket access to only the web server user
- Prevent other users on the system from connecting to your PHP-FPM pool
15.4.4. Creating the PHP-FPM Log File
Section titled “15.4.4. Creating the PHP-FPM Log File”# Create the log filesudo touch /var/log/fpm-php.example.com.log
# Set appropriate ownershipsudo chown wordpress_user:www-data /var/log/fpm-php.example.com.log
# Set appropriate permissionssudo chmod 660 /var/log/fpm-php.example.com.log15.4.5. Applying the PHP-FPM Configuration
Section titled “15.4.5. Applying the PHP-FPM Configuration”# Reload PHP-FPM to apply changessudo systemctl reload php8.3-fpm
# Verify the socket path in your configurationsudo grep "listen = /" example.com.conf15.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 configurationsudo vim /etc/nginx/sites-available/example.com.confFind 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 configurationsudo nginx -t
# Reload Nginx if the test is successfulsudo systemctl reload nginx15.5. WordPress Ownership and Permissions
Section titled “15.5. WordPress Ownership and Permissions”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.
15.5.1. Standard Permissions Setup
Section titled “15.5.1. Standard Permissions Setup”For a balance of security and functionality, use these permissions:
# Set ownership to the WordPress usersudo 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 ownersudo chmod 400 /var/www/example.com/public_html/wp-config.php15.5.2. Hardened Permissions Setup
Section titled “15.5.2. Hardened Permissions Setup”For maximum security (may require adjustments for some plugins):
# Set ownership to the WordPress usersudo 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 uploadssudo 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 directorycd /var/www/example.com/sudo mkdir tmp/
# Set proper ownership and permissionssudo 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 configurationsudo vim /etc/php/8.3/fpm/pool.d/example.com.confAdd these lines to the configuration:
; Set custom temporary directoriesphp_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 directoriesphp_admin_value[open_basedir] = /var/www/example.com/public_html/:/var/www/example.com/tmp/Apply the changes:
# Reload PHP-FPMsudo systemctl reload php8.3-fpm15.6.3. Disabling Dangerous PHP Functions
Section titled “15.6.3. Disabling Dangerous PHP Functions”Restrict PHP functions that could be used for malicious purposes:
; Disable potentially dangerous PHP functionsphp_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, system15.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.
15.7.1. Installing Certbot
Section titled “15.7.1. Installing Certbot”Certbot is a tool that automates the process of obtaining and renewing Let’s Encrypt certificates:
# Update package listssudo apt update
# Install Certbot and the Nginx pluginsudo apt install certbot python3-certbot-nginx15.7.2. Obtaining a Certificate
Section titled “15.7.2. Obtaining a Certificate”Use the webroot plugin to obtain a certificate without interrupting your website:
# Obtain a certificate for your domainsudo certbot certonly --webroot -w /var/www/example.com/public_html/ -d example.com -d www.example.comFollow 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”15.8.1. Creating Strong DH Parameters
Section titled “15.8.1. Creating Strong DH Parameters”To strengthen the security of your SSL/TLS implementation, create a strong Diffie-Hellman parameter file:
# Create the SSL directorycd /etc/nginx/sudo mkdir ssl/cd ssl/
# Generate DH parameters (this may take a few minutes)sudo openssl dhparam -out dhparam.pem 204815.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 filesudo vim /etc/nginx/ssl/ssl_certs_example.com.confAdd the following content:
# Certificate pathsssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# SSL stapling certificatessl_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”15.9.1. Creating Global SSL Configuration
Section titled “15.9.1. Creating Global SSL Configuration”Create a global SSL configuration file with optimal security settings:
# Create the global SSL configuration filesudo vim /etc/nginx/ssl/ssl_all_sites.confAdd 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 PROTOCOLSssl_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 linesssl_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 30sresolver 1.1.1.1 1.0.0.1;resolver_timeout 15s;ssl_session_tickets off;# HSTS HEADERSadd_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/3ssl_early_data on;add_header Alt-Svc 'h3=":$server_port"; ma=86400';add_header x-quic 'H3';quic_retry on;15.9.2. Configuring Nginx for HTTPS
Section titled “15.9.2. Configuring Nginx for HTTPS”Update your Nginx server block to use HTTPS:
# Edit your site's Nginx configurationsudo vim /etc/nginx/sites-available/example.com.confAdd a server block to redirect HTTP to HTTPS:
# Redirect HTTP to HTTPSserver { 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 blockserver { # 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 configurationsudo nginx -t
# Reload Nginx if the test is successfulsudo systemctl reload nginx15.9.3. Testing SSL Configuration
Section titled “15.9.3. Testing SSL Configuration”Verify that your SSL configuration is working correctly:
# Test HTTP to HTTPS redirectioncurl -I http://example.com
# Test HTTPS connectioncurl -I https://example.comYou can also test your SSL configuration using online tools:
- SSL Labs - For comprehensive SSL/TLS testing
- HTTP3 Check - To verify HTTP/3 support
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 crontabsudo crontab -eAdd the following lines:
# Renew certificates twice a month and reload Nginx00 1 14,28 * * certbot renew --force-renewal --deploy-hook "systemctl reload nginx" >> /var/log/certbot-renew.log 2>&1Set appropriate permissions for the log file:
# Create the log file if it doesn't existsudo touch /var/log/certbot-renew.log
# Set appropriate permissionssudo chmod 644 /var/log/certbot-renew.log15.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 filecd /etc/nginx/includes/sudo vim http_headers.confAdd the following content:
# Referrer Policy# Controls how much referrer information is included with requestsadd_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Content Type Options# Prevents browsers from MIME-sniffing a response away from the declared content-typeadd_header X-Content-Type-Options "nosniff" always;
# Frame Options# Protects against clickjacking by preventing your page from being displayed in a frameadd_header X-Frame-Options "sameorigin" always;
# XSS Protection# Enables the cross-site scripting (XSS) filter in browsersadd_header X-XSS-Protection "1; mode=block" always;
# Permissions Policy# Controls which browser features and APIs can be usedadd_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 filesudo vim /etc/nginx/includes/browser_caching_security_headers.confAdd the following content:
# Enable browser cachingexpires 30d;etag on;if_modified_since exact;add_header Pragma "public" always;add_header Cache-Control "public, no-transform" always;
# Include security headersinclude /etc/nginx/includes/http_headers.conf;
# Disable access logging for static contentaccess_log off;15.10.3. Implementing Security Headers
Section titled “15.10.3. Implementing Security Headers”Add the security headers to your Nginx server block:
# Edit your site's Nginx configurationsudo vim /etc/nginx/sites-available/example.com.confAdd the include directive above the PHP processing location block:
# Include security headersinclude /etc/nginx/includes/http_headers.conf;Apply the changes:
# Test Nginx configurationsudo nginx -t
# Reload Nginx if the test is successfulsudo systemctl reload nginx15.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 filecd /etc/nginx/includes/sudo vim nginx_security_directives.confAdd the following content:
# Deny access to sensitive WordPress fileslocation = /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 directorieslocation ~* ^/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 filelocation = /.user.ini { deny all; }
# Filter suspicious request methodsif ( $request_method ~* ^(TRACE|DELETE|TRACK)$ ) { return 403; }
# Filter suspicious query strings in the URLset $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 functionalityif ( $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 injectionsset $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 injectionsset $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 exploitsset $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 spamset $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 agentsset $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 toolsif ($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 configurationsudo vim /etc/nginx/sites-available/example.com.confAdd the include directive above the PHP processing location block:
# Include WordPress security directivesinclude /etc/nginx/includes/nginx_security_directives.conf;Apply the changes:
# Test Nginx configurationsudo nginx -t
# Reload Nginx if the test is successfulsudo systemctl reload nginx15.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.
Testing the PHP Execution Block
Section titled “Testing the PHP Execution Block”First, verify that PHP execution is blocked in the plugins directory:
# Create a test PHP file in the plugins directorycd /var/www/example.com/sudo vim public_html/wp-content/plugins/test.phpAdd this content to the test file:
<?phpphpinfo();?>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.
Allowing Specific Plugin Files
Section titled “Allowing Specific Plugin Files”To allow PHP execution for specific plugin files, add a location block for each file:
# Edit your site's Nginx configurationsudo vim /etc/nginx/sites-available/example.com.confAdd a location block for each plugin file that needs PHP execution:
# Allow PHP execution for specific plugin fileslocation = /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 configurationsudo nginx -t
# Reload Nginx and restart PHP-FPMsudo systemctl reload nginx && sudo systemctl restart php8.3-fpmDon’t forget to remove the test file:
# Remove the test PHP filesudo rm /var/www/example.com/public_html/wp-content/plugins/test.php15.12. Nginx DDoS Protection and WordPress
Section titled “15.12. Nginx DDoS Protection and WordPress”15.12.1. Hot Linking Protection
Section titled “15.12.1. Hot Linking Protection”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 configurationsudo vim /etc/nginx/sites-available/example.com.confAdd the following to your server block:
# Prevent hot linking of imageslocation ~* \.(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.
15.13.1. Configuring Rate Limiting
Section titled “15.13.1. Configuring Rate Limiting”First, add the rate limiting zone to your main Nginx configuration:
# Edit the main Nginx configurationcd /etc/nginx/sudo vim nginx.confAdd the following to the http context:
# Rate limiting zone for WordPresslimit_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 filecd /etc/nginx/includes/sudo vim rate_limiting.confAdd the following content:
# Rate limit WordPress login pagelocation = /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;}15.13.3. Implementing Rate Limiting
Section titled “15.13.3. Implementing Rate Limiting”Add the rate limiting configuration to your Nginx server block:
# Edit your site's Nginx configurationsudo vim /etc/nginx/sites-available/example.com.confAdd the include directive:
# Include rate limiting configurationinclude /etc/nginx/includes/rate_limiting.conf;Apply the changes:
# Test Nginx configurationsudo nginx -t
# Reload Nginx if the test is successfulsudo systemctl reload nginx15.14. Disallow File Modifications
Section titled “15.14. Disallow File Modifications”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.phpsudo vim /var/www/example.com/public_html/wp-config.phpAdd the following line before the “That’s all, stop editing!” comment:
/* Disable file modifications */define('DISALLOW_FILE_MODS', true);Apply the changes:
# Reload PHP-FPMsudo systemctl reload php8.3-fpm15.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 MariaDBsudo mysql -u root -pExecute these SQL commands:
-- Revoke all privilegesREVOKE ALL PRIVILEGES ON wordpress_db.* FROM 'wordpress_user'@'localhost';
-- Grant only necessary privilegesGRANT SELECT, INSERT, UPDATE, DELETE ON wordpress_db.* TO 'wordpress_user'@'localhost';
-- Apply the changesFLUSH PRIVILEGES;Exit the MariaDB console:
EXIT;15.16. Hardening the WP REST API
Section titled “15.16. Hardening the WP REST API”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.
15.16.1. Restricting REST API Access
Section titled “15.16.1. Restricting REST API Access”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');15.16.2. Restricting REST API with Nginx
Section titled “15.16.2. Restricting REST API with Nginx”For an additional layer of security, you can also restrict access to the REST API at the Nginx level:
# Edit your site's Nginx configurationsudo vim /etc/nginx/sites-available/example.com.confAdd the following location block:
# Restrict WP REST APIlocation /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 configurationsudo nginx -t
# Reload Nginx if the test is successfulsudo systemctl reload nginx15.17. Using a Web Application Firewall
Section titled “15.17. Using a Web Application Firewall”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 dependenciessudo apt updatesudo apt install -y libmodsecurity3 libmodsecurity-dev nginx-module-security15.17.2. Configuring ModSecurity
Section titled “15.17.2. Configuring ModSecurity”Create a basic ModSecurity configuration:
# Create ModSecurity configuration directorysudo mkdir -p /etc/nginx/modsec
# Download OWASP Core Rule Setsudo git clone https://github.com/coreruleset/coreruleset /etc/nginx/modsec/coreruleset
# Create ModSecurity configuration filesudo vim /etc/nginx/modsec/modsec.confAdd the following content:
# Basic ModSecurity configurationSecRuleEngine OnSecRequestBodyAccess OnSecResponseBodyAccess OnSecResponseBodyMimeType text/plain text/html text/xml application/jsonSecResponseBodyLimit 1024
# Include OWASP Core Rule SetInclude /etc/nginx/modsec/coreruleset/crs-setup.confInclude /etc/nginx/modsec/coreruleset/rules/*.conf15.17.3. Enabling ModSecurity in Nginx
Section titled “15.17.3. Enabling ModSecurity in Nginx”Edit your Nginx configuration to enable ModSecurity:
# Edit the main Nginx configurationsudo vim /etc/nginx/nginx.confAdd the following to the http context:
# Load ModSecurity moduleload_module modules/ngx_http_modsecurity_module.so;Edit your site’s server block:
# Edit your site's Nginx configurationsudo vim /etc/nginx/sites-available/example.com.confAdd ModSecurity directives to your server block:
# Enable ModSecuritymodsecurity on;modsecurity_rules_file /etc/nginx/modsec/modsec.conf;Apply the changes:
# Test Nginx configurationsudo nginx -t
# Reload Nginx if the test is successfulsudo systemctl reload nginx15.18. Conclusion
Section titled “15.18. Conclusion”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:
- Regularly update WordPress core, themes, and plugins
- Monitor your logs for suspicious activity
- Perform regular security audits
- Keep backups of your site and database
- Stay informed about new security threats and best practices