📋 Table of Contents
🎯 Introduction to WordPress Hardening
WordPress hardening is a critical security practice that involves implementing multiple layers of protection to safeguard your website from cyber threats. As one of the most popular content management systems globally, WordPress is frequently targeted by attackers due to its widespread adoption.
Security Architecture Overview
Multi-Layer Security Approach
Firewall & DDoS Protection
Nginx & SSL/TLS
PHP-FPM & WordPress
Database Security
Key Security Components
📊 Log Monitoring & Analysis
Regular log monitoring is essential for identifying security issues, performance problems, and potential attacks before they cause serious damage.
Log File Locations
| Log Type | Location | Purpose |
|---|---|---|
| Nginx Access Log | /var/log/nginx/access.log.example.com | All HTTP requests to your site |
| Nginx Error Log | /var/log/nginx/error.log.example.com | Server errors and warnings |
| PHP-FPM Error Log | /var/log/fpm-php.example.com.log | PHP execution errors |
Viewing Log Files
xmlrpc.php
or wp-login.php from the same IP addresses, you're experiencing a brute force attack.
Rate limiting (covered later) will mitigate this.
Common Log File Patterns
🏊 PHP-FPM Pool Configuration
PHP-FPM pools allow you to isolate different websites running on the same server. Each site gets its own pool with dedicated resources, user permissions, and security settings. This prevents one compromised site from affecting others.
Pool Isolation Benefits
How PHP Pools Isolate Sites
User: siteA
Socket: php-siteA.sock
User: siteB
Socket: php-siteB.sock
User: siteC
Socket: php-siteC.sock
Each pool runs independently with its own user permissions and resource limits
Step-by-Step Pool Setup
-
Create a System User
First, create a dedicated system user for your website. This user will own the site files and run the PHP-FPM process.
# Create new system user sudo useradd username # Add user to www-data group (and vice versa) sudo usermod -a -G username www-data sudo usermod -a -G www-data username -
Create Pool Configuration File
Copy the default pool configuration and customize it for your site.
# Navigate to pool directory cd /etc/php/8.3/fpm/pool.d/ # Copy default configuration sudo cp www.conf example.com.conf # Edit the new configuration sudo nano example.com.conf -
Configure Pool Settings
Edit the pool configuration file with these critical settings:
; Pool name - change from [www] to your site identifier [example] ; User and group for this pool user = username group = username ; Unix socket for this pool (unique per site) listen = /run/php/php8.3-fpm-example.com.sock ; Resource limits rlimit_files = 15000 rlimit_core = 100 ; PHP settings specific to this pool php_flag[display_errors] = off php_admin_value[error_log] = /var/log/fpm-php.example.com.log php_admin_flag[log_errors] = on -
Create PHP Error Log File
# Create log file sudo touch /var/log/fpm-php.example.com.log # Set ownership sudo chown username:www-data /var/log/fpm-php.example.com.log # Set permissions (read/write for owner and group) sudo chmod 660 /var/log/fpm-php.example.com.log -
Update Nginx Configuration
Point Nginx to use the new PHP-FPM socket for this site.
sudo nano /etc/nginx/sites-available/example.com.confUpdate the fastcgi_pass directive:
location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock; fastcgi_param HTTP_HOST $host; include /etc/nginx/includes/fastcgi_optimize.conf; } -
Reload Services
# Test Nginx configuration sudo nginx -t # Reload PHP-FPM to activate new pool sudo systemctl reload php8.3-fpm # Reload Nginx sudo systemctl reload nginx
Advanced Pool Security Settings
1. Disable Dangerous PHP Functions
Prevent execution of functions that could be exploited by attackers:
2. Configure Temporary Directories
Add to pool configuration:
3. Implement open_basedir Restriction
Limit PHP file access to specific directories only:
🔐 SSL Certificate Setup & HTTPS Configuration
Implementing SSL/TLS certificates encrypts data transmission between your server and visitors, preventing man-in-the-middle attacks and building trust with your users.
SSL/TLS Security Flow
HTTPS Connection Process
Browser requests HTTPS connection
Server presents SSL certificate
Client validates certificate
Secure communication established
Installing Let's Encrypt Certificates
-
Install Certbot
# Update package list sudo apt update # Install Certbot and Cloudflare DNS plugin sudo apt install certbot python3-certbot-dns-cloudflare -
Obtain SSL Certificate
# Using webroot validation method sudo certbot certonly --webroot \ -w /var/www/example.com/public_html/ \ -d example.com \ -d www.example.com💡 Note: Ensure your domain is already pointing to your server and Nginx is configured before running this command. -
Generate Strong DH Parameters
Diffie-Hellman parameters enhance SSL/TLS security:
# Create SSL directory cd /etc/nginx/ sudo mkdir ssl/ cd ssl/ # Generate 2048-bit DH parameters (takes several minutes) sudo openssl dhparam -out dhparam.pem 2048 -
Create SSL Certificate Include File
This file references your site-specific certificates:
sudo nano /etc/nginx/ssl/ssl_certs_example.com.confAdd the following content:
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem; -
Create Global SSL Configuration
This configuration achieves A+ rating on SSL Labs:
sudo nano /etc/nginx/ssl/ssl_all_sites.conf# A+ RATING CONFIGURATION - Updated May 2025 # SSL session settings ssl_session_cache shared:SSL:20m; ssl_session_timeout 180m; # Enable only secure protocols ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; # Strong cipher suites (single line in actual config) 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; # DH parameters for forward secrecy ssl_dhparam /etc/nginx/ssl/dhparam.pem; # Cloudflare DNS resolver resolver 1.1.1.1 1.0.0.1; resolver_timeout 15s; # Disable session tickets ssl_session_tickets off; # HSTS header (HTTP Strict Transport Security) add_header Strict-Transport-Security "max-age=31536000;" always; # Enable HTTP/3 and QUIC ssl_early_data on; add_header Alt-Svc 'h3=":$server_port"; ma=86400'; add_header x-quic 'H3'; quic_retry on; -
Update Nginx Server Block
Configure your site to use HTTPS:
# HTTP to HTTPS redirect server { listen 80; server_name example.com www.example.com; # Permanent redirect to HTTPS return 301 https://example.com$request_uri; } # HTTPS server block (primary site - includes reuseport) server { listen 443 ssl; http2 on; listen 443 quic reuseport; http3 on; server_name example.com www.example.com; root /var/www/example.com/public_html; # Include SSL certificates and configuration include /etc/nginx/ssl/ssl_certs_example.com.conf; include /etc/nginx/ssl/ssl_all_sites.conf; # ... rest of configuration ... }⚠️ Important: Only usereuseporton ONE server block. Additional sites should omit this directive. -
Test and Reload Configuration
# Test Nginx configuration sudo nginx -t # Reload Nginx if test passes sudo systemctl reload nginx # Verify HTTPS is working curl -I https://example.com
Automated Certificate Renewal
Let's Encrypt certificates expire after 90 days. Set up automatic renewal:
Certificate Management Commands
Verify Your SSL Configuration
🛡️ HTTP Security Headers
HTTP security headers are directives sent from the server to the browser, instructing it how to handle various security aspects of the website. These headers provide defense-in-depth protection against common web vulnerabilities.
Essential Security Headers
| Header | Purpose | Protection Against |
|---|---|---|
| X-Content-Type-Options | Prevents MIME type sniffing | Drive-by downloads, malicious file execution |
| X-Frame-Options | Controls iframe embedding | Clickjacking attacks |
| X-XSS-Protection | Enables browser XSS filter | Cross-site scripting attacks |
| Referrer-Policy | Controls referrer information | Information leakage |
| Permissions-Policy | Controls browser features | Unauthorized feature access |
| Strict-Transport-Security | Enforces HTTPS | Protocol downgrade attacks |
Implementing Security Headers
-
Create Headers Configuration File
cd /etc/nginx/includes/ sudo nano http_headers.conf -
Add Header Directives
# ------------------------------------------------------- # Referrer Policy - Choose one option # ------------------------------------------------------- # add_header Referrer-Policy "no-referrer"; add_header Referrer-Policy "strict-origin-when-cross-origin"; # add_header Referrer-Policy "unsafe-url"; # Prevent MIME type sniffing add_header X-Content-Type-Options "nosniff"; # Prevent clickjacking - only allow same origin iframes add_header X-Frame-Options "sameorigin"; # Enable XSS filter in browsers add_header X-XSS-Protection "1; mode=block"; # Control browser feature permissions add_header Permissions-Policy 'accelerometer=(), camera=(), clipboard-read=(), clipboard-write=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), fullscreen=(self "https://www.youtube.com")'; -
Include Headers in Site Configuration
cd /etc/nginx/sites-available/ sudo nano example.com.confAdd this line BEFORE your PHP processing block:
# Include security headers include /etc/nginx/includes/http_headers.conf; -
Test and Apply Changes
sudo nginx -t sudo systemctl reload nginx
Browser Caching with Security Headers
Combine caching directives with security headers for optimal performance:
🔒 File Ownership & Permissions
Proper file permissions are critical for WordPress security. Incorrect permissions can allow unauthorized access or prevent your site from functioning correctly.
Permission Number System
Understanding File Permissions
Read: 4
Write: 2
Execute: 1
Read: 4
Write: 2
Execute: 1
Read: 4
Write: 2
Execute: 1
Example: 770 = Owner (7=rwx), Group (7=rwx), Others (0=---)
Permission Levels Explained
| Permission | Numeric | Description | Use Case |
|---|---|---|---|
| rwx (Full) | 7 | Read, Write, Execute | Owner on writable directories |
| rw- (Read/Write) | 6 | Read and Write only | Standard files, config files |
| r-x (Read/Execute) | 5 | Read and Execute | Hardened directories |
| r-- (Read only) | 4 | Read access only | Sensitive files (wp-config.php) |
| --- (No access) | 0 | No permissions | Block public access |
Standard Permission Configuration
Use this configuration for development or when plugins require write access:
Hardened Permission Configuration
Maximum security configuration - use after site setup is complete:
Permission Strategy Comparison
Cons: Slightly less secure
Use when: Actively developing or frequently updating
Cons: Manual permission changes needed for updates
Use when: Site is stable and changes are infrequent
Temporary Permission Changes
When you need to update plugins with hardened permissions:
🚧 Nginx Security Directives
Nginx security directives act as a firewall layer, blocking common attack patterns and protecting WordPress core files from unauthorized access.
Create Security Configuration File
WordPress Security Ruleset
Apply Security Configuration
Allowing Plugin PHP Execution (When Needed)
Some plugins require PHP execution. Create specific exceptions:
Content of test file:
Access via browser: https://example.com/wp-content/plugins/test556.php
Result: Should return 403 Forbidden (PHP execution blocked ✓)
Creating Exceptions for Specific Plugins
If a plugin legitimately needs PHP execution, add an exception:
Security Directives Impact
⏱️ Rate Limiting & Brute Force Protection
Rate limiting restricts the number of requests a client can make in a given time period, effectively preventing brute force attacks on login pages and XML-RPC.
How Rate Limiting Works
Request Flow with Rate Limiting
Allowed (within limit)
Burst queue (delayed)
Blocked (444 error)
Zone: 30 requests/minute, Burst: 20 additional requests
Configure Global Rate Limit Zone
-
Edit Main Nginx Configuration
cd /etc/nginx/ sudo nano nginx.conf -
Add Rate Limit Zone
Add this in the
httpcontext (before any server blocks):## # Rate Limiting Configuration ## # Creates a 10MB zone tracking client IPs # Allows 30 requests per minute per IP limit_req_zone $binary_remote_addr zone=wp:10m rate=30r/m;
Create Site-Specific Rate Limit Rules
Add the following content (customize the socket path):
Apply Rate Limiting to Your Site
Rate Limiting Parameters Explained
| Parameter | Value | Explanation |
|---|---|---|
| zone=wp | wp | Name of the rate limit zone defined in nginx.conf |
| rate | 30r/m | 30 requests per minute per IP address |
| burst | 20 | Allow 20 additional requests in queue |
| nodelay | - | Process burst requests immediately (no delay) |
| limit_req_status | 444 | Return 444 (close connection) when exceeded |
Testing Rate Limiting
You can verify rate limiting is working by monitoring your logs during a simulated attack or by using tools like Apache Bench:
Next 20 requests: Delayed but processed
Remaining requests: Blocked with 444 status
Decrease burst for tighter security
Monitor logs to find optimal settings
🗄️ Database Security Hardening
WordPress database users often have excessive privileges. Restricting database permissions follows the principle of least privilege, reducing the impact of SQL injection vulnerabilities.
Database Permission Levels
| Privilege | Required | Purpose |
|---|---|---|
| SELECT | ✅ Yes | Read data from tables |
| INSERT | ✅ Yes | Add new records (posts, comments, etc.) |
| UPDATE | ✅ Yes | Modify existing records |
| DELETE | ✅ Yes | Remove records from tables |
| CREATE | ⚠️ Optional | Only needed for plugin installation |
| ALTER | ⚠️ Optional | Only needed for plugin updates |
| INDEX | ⚠️ Optional | Only needed for performance optimization |
| DROP | ❌ No | Dangerous - can delete entire tables |
Restrict Database Privileges
-
Connect to MySQL
# Log in to MySQL as root sudo mysql -u root -p -
Revoke All Current Privileges
-- Revoke all privileges from database user REVOKE ALL PRIVILEGES ON site_db.* FROM 'site_user'@'localhost'; -
Grant Minimal Required Privileges
-- Grant only SELECT, INSERT, UPDATE, DELETE GRANT SELECT, INSERT, UPDATE, DELETE ON site_db.* TO 'site_user'@'localhost'; -- Apply changes FLUSH PRIVILEGES; -
Temporary Privileges for Plugin Installation
When installing plugins that create tables, temporarily grant additional privileges:
-- Temporarily add CREATE, ALTER, INDEX for plugin installation GRANT CREATE, ALTER, INDEX ON site_db.* TO 'site_user'@'localhost'; FLUSH PRIVILEGES; -- After installation, revoke again: REVOKE CREATE, ALTER, INDEX ON site_db.* FROM 'site_user'@'localhost'; FLUSH PRIVILEGES;
Verify Database Privileges
Expected output for secure configuration:
Additional WordPress Security Measures
Disable File Modifications from Dashboard
Add this constant to wp-config.php to prevent file editing through WordPress admin:
Reload PHP-FPM After wp-config Changes
Security Hardening Summary
SSL/TLS Encryption ✓
Security Headers ✓
File Permissions ✓
Nginx Firewall ✓
Rate Limiting ✓
Database Restrictions ✓
Update SSL certificates
Review permissions after updates
Audit database privileges
Test backups regularly
🎓 Conclusion
By implementing these comprehensive security measures, you have significantly hardened your WordPress installation against common and advanced threats. This multi-layered approach ensures defense in depth, where even if one security measure is bypassed, others remain in place to protect your site.
Ongoing Security Practices
- Review log files weekly for suspicious activity
- Keep WordPress core, plugins, and themes updated
- Regularly test backups and disaster recovery procedures
- Monitor SSL certificate expiration and renewal
- Audit file permissions after major changes
- Review and update security headers as standards evolve
- Test rate limiting effectiveness periodically
- Document all custom configurations for future reference
Your Security Stack
Layer 1: Cloudflare / CDN (DDoS protection, WAF)
Layer 2: Nginx (SSL/TLS, Security headers, Rate limiting)
Layer 3: PHP-FPM (Pool isolation, Function restrictions)
Layer 4: WordPress (File permissions, Constants)
Layer 5: Database (Restricted privileges)
Layer 6: Monitoring (Log analysis, Alerts)
Remember: Security is not a one-time task but an ongoing process. Stay informed about new threats and continuously improve your security posture.