๐Ÿ”ง NGINX Server Configuration Guide

Section 15: Advanced Security & Performance Configuration

๐Ÿ“‹ Managing NGINX Logs

NGINX logs are essential for monitoring server activity, debugging issues, and analyzing traffic patterns. Log files provide detailed information about requests, errors, and server performance.

Accessing Log Files

Navigate to the NGINX log directory:

cd /var/log/nginx

List all log files:

ls

View a specific log file:

sudo cat log_file_name.log

View logs with pagination (recommended for large files):

sudo less log_file_name.log
๐Ÿ’ก Tip: Use less for better navigation in large log files. You can search within the file using /search_term and navigate with arrow keys.

โš™๏ธ PHP-FPM Pool Configuration

PHP-FPM (FastCGI Process Manager) pools allow you to run multiple PHP applications with different configurations, users, and resource limits. This provides better isolation and security for multi-site hosting environments.

PHP-FPM Pool Architecture

NGINX
Web Server
โ†’
PHP-FPM
Process Manager
โ†’
Pool 1
example.com
user: example
โ†’
Pool 2
site2.com
user: site2

Creating a System User

First, create a dedicated user for the website:

sudo useradd username

User Group Configuration

Add users to appropriate groups for proper permissions:

sudo usermod -a -G username_group www-data
sudo usermod -a -G www-data username_group
sudo usermod -a -G username_group $USER

Creating a New Pool Configuration

Navigate to the PHP-FPM pool directory:

cd /etc/php/8.3/fpm/pool.d/

List existing pool configurations:

ls

Copy the default pool configuration as a template:

sudo cp www.conf example.com.conf

Edit the new pool configuration:

sudo nano example.com.conf

Essential Pool Configuration Parameters

; Pool name [example] ; User and group for the pool user = username group = username ; Socket location for communication with NGINX listen = /run/php/php8.3-fpm-example.com.sock ; Resource limits rlimit_files = 15000 rlimit_core = 100 ; PHP configuration php_flag[display_errors] = off php_admin_value[error_log] = /var/log/fpm-php.example.com.log php_admin_flag[log_errors] = on

Creating the PHP-FPM Log File

Create the log file:

sudo touch /var/log/fpm-php.example.com.log

Set proper ownership:

sudo chown user:www-data /var/log/fpm-php.example.com.log

Set appropriate permissions:

sudo chmod 660 /var/log/fpm-php.example.com.log

Verifying Socket Configuration

Verify the socket path in your pool configuration:

sudo grep "listen = /" example.com.conf

Updating NGINX Configuration

Edit your NGINX site configuration:

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

Update the fastcgi_pass directive to use your new socket:

fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock;

Testing and Applying Changes

Test NGINX configuration:

sudo nginx -t

Reload PHP-FPM and NGINX:

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

Advanced PHP Configuration

Enabling URL File Operations

php_admin_flag[allow_url_fopen] = on

Disabling Dangerous Functions

For security, 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

Configuring Temporary Directories

php_admin_value[upload_tmp_dir] = /var/www/example.com/tmp/ php_admin_value[sys_temp_dir] = /var/www/example.com/tmp/

Create and configure the temporary directory:

cd /var/www/example.com/
sudo mkdir tmp/
sudo chown username:username tmp/
sudo chmod 770 tmp/

Restricting File Access with open_basedir

Limit PHP file access to specific directories:

php_admin_value[open_basedir] = /var/www/example.com/public_html/:/var/www/example.com/tmp/
โš ๏ธ Warning: After changing PHP-FPM configuration, always reload the service and test your website to ensure everything works correctly.

๐Ÿ”’ SSL/TLS Certificate Configuration

SSL/TLS certificates encrypt data transmitted between your server and clients, providing security and trust. Modern web browsers require HTTPS for many features and rank HTTPS sites higher in search results.

Installing Certbot

Update package lists:

sudo apt update

Install Certbot with Cloudflare DNS plugin:

sudo apt install certbot python3-certbot-dns-cloudflare

Obtaining SSL Certificates

Request a certificate using webroot validation:

sudo certbot certonly --webroot -w /var/www/example.com/public_html/ -d example.com -d www.example.com
๐Ÿ’ก Note: The --webroot method requires that your site is already accessible via HTTP. Certbot will place verification files in your webroot directory.

Generating Diffie-Hellman Parameters

Create SSL directory:

cd /etc/nginx/
sudo mkdir ssl/
cd ssl/

Generate DH parameters (this may take several minutes):

sudo openssl dhparam -out dhparam.pem 2048

Creating SSL Configuration Files

Site-Specific SSL Configuration

Create a configuration file for your site's certificates:

sudo nano /etc/nginx/ssl/ssl_certs_example.com.conf
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;

Global SSL Configuration

Create a global SSL configuration for all sites:

sudo nano /etc/nginx/ssl/ssl_all_sites.conf
# SSL A+ RATING CONFIGURATION # UPDATED: MAY 2025 # 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 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; # DNS resolver (Cloudflare) 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; # HTTP/3 and QUIC support ssl_early_data on; add_header Alt-Svc 'h3=":$server_port"; ma=86400'; add_header x-quic 'H3'; quic_retry on;
โœ… Note: This configuration achieves an A+ rating on SSL Labs. Let's Encrypt removed SSL stapling support on May 7, 2025, so those directives are commented out.

Configuring NGINX Server Blocks for 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)

server { listen 443 ssl; http2 on; listen 443 quic reuseport; http3 on; server_name example.com www.example.com; # Include SSL configurations include /etc/nginx/ssl/ssl_certs_example.com.conf; include /etc/nginx/ssl/ssl_all_sites.conf; # Rest of your configuration... }

Additional HTTPS Server Blocks

For additional sites, omit the reuseport parameter:

listen 443 ssl; http2 on; listen 443 quic; http3 on;

Adding HTTP_HOST Parameter

In your PHP processing block, add:

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; }

Testing HTTP/3 Configuration

Add this directive temporarily to prevent browser caching:

# TESTING ONLY - Remove after verification add_header Cache-Control 'no-cache,no-store';

Testing Configuration

Test NGINX syntax:

sudo nginx -t

Reload NGINX:

sudo systemctl reload nginx

Test with curl commands:

curl -I http://example.com
curl -I https://example.com

SSL Testing Resources

  • SSL Labs: Test your SSL configuration at ssllabs.com (expect A+ rating)
  • HTTP/3 Check: Verify HTTP/3 support at http3check.net
  • Browser DevTools: Open Network tab, right-click on column headers, enable "Protocol" column to see "h3" for HTTP/3 connections

Certificate Management Commands

Command Description
sudo certbot certificates List all certificates
sudo certbot delete Delete a certificate
sudo certbot renew Renew certificates due for renewal
sudo certbot renew --force-renewal Force renewal of all certificates

Automated Certificate Renewal

Edit the root crontab:

sudo crontab -e

Add these lines to renew certificates twice monthly:

# Renew certificates on the 14th and 28th of each month at 1:00 AM 00 1 14,28 * * certbot renew --force-renewal # Reload NGINX after renewal at 2:00 AM 00 2 14,28 * * systemctl reload nginx

๐Ÿ›ก๏ธ HTTP Security Headers

HTTP response headers are a crucial component of web security. They provide the browser with instructions on how to handle content, control security policies, and protect against various types of attacks. These headers are sent by the server in response to HTTP requests made by clients (typically web browsers).

HTTP Header Communication Flow

1. Client Request
Browser sends request
to server
โ†’
2. Server Processing
Server processes request
& adds headers
โ†’
3. Response
Server sends content
with security headers
โ†’
4. Browser Action
Browser enforces
header policies

Understanding HTTP Header Types

HTTP headers contain metadata about connections, requested resources, and returned resources. There are several types of headers:

  • General Headers: Apply to both requests and responses
  • Request Headers: Sent by the client to the server
  • Response Headers: Sent by the server to the client
  • Entity Headers: Describe the content body
๐Ÿ“š Resource: For comprehensive HTTP header documentation, visit the Mozilla Developer Network (MDN), which provides excellent resources, tutorials, and guides on web development standards.

Essential Security Headers

1. X-XSS-Protection

This header configures the XSS (Cross-Site Scripting) auditor built into older browsers. When an XSS attack is detected, the browser can block the page or sanitize it.

Value Description
0 Disables XSS filtering
1 Enables XSS filtering (sanitizes page)
1; mode=block Enables XSS filtering (blocks entire page)
โš ๏ธ Note: While Mozilla documentation suggests this header is deprecated, it's still recommended for defense-in-depth. Eventually, Content Security Policy will replace this header.

2. X-Frame-Options

Controls whether your site can be embedded in frames or iframes. This prevents clickjacking attacks where malicious sites trick users into clicking on concealed elements.

Value Description
DENY Site cannot be displayed in frames
SAMEORIGIN Site can only be framed by same origin
ALLOW-FROM uri Site can be framed by specified origin (deprecated)

3. X-Content-Type-Options

Prevents browsers from MIME-sniffing a response away from the declared content-type. This stops browsers from trying to interpret files as a different MIME type than what's specified.

X-Content-Type-Options: nosniff

The only valid value is nosniff, which forces browsers to respect the declared Content-Type.

4. Referrer-Policy

Controls how much referrer information is sent when navigating from your site to other sites. This is crucial for privacy and may affect analytics or affiliate tracking.

Policy Description
no-referrer Never send referrer information
no-referrer-when-downgrade Send referrer unless downgrading from HTTPS to HTTP
origin Send only the origin (no path/query)
origin-when-cross-origin Send full URL for same-origin, only origin for cross-origin
strict-origin Send origin unless downgrading HTTPS to HTTP
strict-origin-when-cross-origin Default: full URL for same-origin, origin for cross-origin (not on downgrade)
unsafe-url Always send full URL (may leak private information)
โš ๏ธ Important: If your site relies on affiliate commissions or link tracking, contact your affiliates to determine the recommended policy. Use strict-origin-when-cross-origin by default, or unsafe-url if tracking requires it.

5. Permissions-Policy (formerly Feature-Policy)

Controls which browser features and APIs can be used in your site and in embedded iframes. This provides granular control over powerful browser capabilities.

Common Directives:

  • accelerometer - Control access to device accelerometer
  • camera - Control access to device camera
  • geolocation - Control access to GPS/location
  • microphone - Control access to device microphone
  • payment - Control Payment Request API
  • usb - Control WebUSB API
  • fullscreen - Control fullscreen capability

6. Content-Security-Policy (CSP)

The most powerful security header, CSP helps protect against Cross-Site Scripting (XSS), data injection attacks, and other code injection vulnerabilities. It defines trusted sources for content loading and execution.

Key Benefits:

  • Prevents execution of malicious scripts
  • Blocks unauthorized resource loading
  • Mitigates XSS and data injection attacks
  • Provides detailed violation reporting
๐Ÿ’ก Tip: CSP configuration can be complex. Start with a basic policy and gradually refine it. Use CSP reporting to identify violations before enforcing strict policies.

Implementing Security Headers in NGINX

Navigate to the NGINX includes directory:

cd /etc/nginx/includes/

Create the HTTP headers configuration file:

sudo nano http_headers.conf

Add the following security headers:

# Referrer Policy - Choose appropriate option #add_header Referrer-Policy "no-referrer"; add_header Referrer-Policy "strict-origin-when-cross-origin"; #add_header Referrer-Policy "unsafe-url"; # Content Security add_header X-Content-Type-Options "nosniff"; add_header X-Frame-Options "sameorigin"; add_header X-XSS-Protection "1; mode=block"; # Permissions Policy add_header Permissions-Policy 'accelerometer=(), camera=(), clipboard-read=(), clipboard-write=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), fullscreen=(self "https://www.youtube.com")';

Including Headers in Site Configuration

Navigate to sites-available directory:

cd /etc/nginx/sites-available/

Edit your site configuration:

sudo nano example.com.conf

Add the include directive above your PHP processing block:

include /etc/nginx/includes/http_headers.conf;

Verifying Headers

Test NGINX configuration:

sudo nginx -t

Reload NGINX:

sudo systemctl reload nginx

Check headers with curl:

curl -I https://example.com

Enhanced Browser Caching with Security Headers

Create an updated browser caching configuration:

sudo nano /etc/nginx/includes/browser_caching_security_headers.conf
expires 30d; etag on; if_modified_since exact; add_header Pragma "public"; add_header Cache-Control "public, no-transform"; try_files $uri $uri/ /index.php?$args; include /etc/nginx/includes/http_headers.conf; access_log off;

This configuration:

  • Sets 30-day cache expiration
  • Enables ETags for cache validation
  • Uses exact if-modified-since checking
  • Includes security headers for cached resources
  • Disables access logging for static assets
โœ… Best Practice: Always test security headers on a development server first. Some headers may break functionality if misconfigured, particularly CSP and Permissions-Policy.

๐Ÿ” WordPress-Specific Security Configuration

WordPress requires specific security measures to prevent unauthorized access and code execution. NGINX provides powerful tools to harden WordPress installations against common attacks.

Creating the Security Directives File

Navigate to includes directory:

cd /etc/nginx/includes/

Create the security directives file:

sudo nano nginx_security_directives.conf

Add comprehensive WordPress security rules:

# WORDPRESS-SAFE NGINX FIREWALL RULESET # Based on NGINX 8G Firewall - Updated December 2026 # Low false-positive risk # 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 WordPress 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 plugins 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 location ~* (composer\.(json|lock)|package\.json|yarn\.lock|/vendor/|/node_modules/) { deny all; } # Block dangerous HTTP methods if ($request_method ~* ^(TRACE|DELETE|TRACK)$) { return 403; } # Block vulnerability scanners if ($http_user_agent ~* (nikto|sqlmap|masscan|nmap|dirbuster|acunetix|openvas)) { return 444; }

WordPress Security Layer Architecture

Layer 1
Block Scanner Bots
& Bad Methods
โ†’
Layer 2
Deny Sensitive
Config Files
โ†’
Layer 3
Block PHP Execution
in Uploads
โ†’
Layer 4
Rate Limiting
on wp-login

Including Security Rules in Site Configuration

Edit your site configuration:

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

Add above your PHP processing block:

include /etc/nginx/includes/nginx_security_directives.conf;

Allowing Specific Plugin PHP Execution

Some plugins require PHP execution. By default, we block all PHP in the plugins directory. To allow specific files:

Testing PHP Execution Block

Create a test PHP file:

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

Add this content:

<?php phpinfo(); ?>

Access via browser: https://example.com/wp-content/plugins/test556.php

Result: NGINX returns 403 Forbidden (PHP execution blocked)

Allowing Specific Plugin Files

If a plugin needs PHP execution, add a specific location block after your main PHP processing block:

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; }

Test and reload:

sudo nginx -t
sudo systemctl reload nginx && sudo systemctl restart php8.3-fpm
โœ… Security Best Practice: This approach provides defense-in-depth. All PHP execution is blocked by default, but you can selectively allow specific trusted files. Always specify the full path to the PHP file.

Cleanup

Remove the test file:

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

Disabling WordPress File Modifications

Add this to wp-config.php to prevent plugin/theme installation via admin panel:

define('DISALLOW_FILE_MODS', true);

After adding, reload PHP-FPM:

sudo systemctl reload php8.3-fpm
๐Ÿ’ก Note: With DISALLOW_FILE_MODS enabled, updates must be done via command line or SFTP. This prevents attackers from installing malicious plugins even if they compromise the admin account.

๐Ÿ“ File Ownership and Permissions

Proper file permissions are critical for WordPress security. Too permissive settings allow unauthorized modifications, while too restrictive settings break functionality. We'll cover both standard and hardened configurations.

Permission Strategy

Permission Directories Files Access Level
770 โœ… Standard writable โŒ Owner + Group: Full
660 โŒ โœ… Standard writable Owner + Group: Read/Write
550 โœ… Hardened read-only โŒ Owner + Group: Read/Execute
440 โŒ โœ… Hardened read-only Owner + Group: Read only
400 โŒ โœ… Sensitive config Owner: Read only

Standard Permission Configuration

Standard permissions allow WordPress to update itself and manage uploads while maintaining reasonable security.

Navigate to your site directory:

cd /var/www/example.com/

Set ownership to your site user:

sudo chown -R username:username public_html/

Set directory permissions (770 = rwxrwx---):

sudo find /var/www/example.com/public_html/ -type d -exec chmod 770 {} \;

Set file permissions (660 = rw-rw----):

sudo find /var/www/example.com/public_html/ -type f -exec chmod 660 {} \;

Secure wp-config.php (400 = r--------):

sudo chmod 400 public_html/wp-config.php
๐Ÿ’ก Standard Configuration: This setup allows WordPress automatic updates and plugin installations. The owner and group (www-data) can read, write, and execute. Others have no access.

Hardened Permission Configuration

Hardened permissions prevent WordPress from modifying core files while allowing uploads and content management. This significantly improves security but requires manual updates.

Set ownership:

sudo chown -R username:username public_html/

Set read-only directory permissions (550 = r-xr-x---):

sudo find /var/www/example.com/public_html/ -type d -exec chmod 550 {} \;

Set read-only file permissions (440 = r--r-----):

sudo find /var/www/example.com/public_html/ -type f -exec chmod 440 {} \;

Set writable permissions for wp-content (uploads, cache, etc.):

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 {} \;
โš ๏ธ Hardened Configuration: With these permissions, WordPress cannot update itself or install plugins via the admin panel. All updates must be done via command line or SFTP. This provides maximum security by preventing attackers from modifying core files even if they compromise WordPress.

Permission Breakdown

Octal Symbolic Description Use Case
770 rwxrwx--- Owner & group: full control Writable directories (standard)
660 rw-rw---- Owner & group: read/write Writable files (standard)
550 r-xr-x--- Owner & group: read/execute Read-only directories (hardened)
440 r--r----- Owner & group: read only Read-only files (hardened)
400 r-------- Owner only: read Sensitive config files

Verifying Permissions

Check current permissions:

ls -l /var/www/example.com/public_html/

Check specific file permissions:

ls -l /var/www/example.com/public_html/wp-config.php

When to Use Each Configuration

  • Standard Permissions:
    • Development environments
    • Sites requiring automatic updates
    • Sites managed by non-technical users
    • Trusted single-user environments
  • Hardened Permissions:
    • Production servers
    • High-value or high-traffic sites
    • Multi-tenant hosting environments
    • Sites handling sensitive data
    • When paired with professional security monitoring
โœ… Best Practice: Start with standard permissions during development and testing. Once your site is stable and in production, switch to hardened permissions. Create a deployment script to automate permission changes when pushing updates.

โฑ๏ธ Rate Limiting Configuration

Rate limiting protects your WordPress site from brute force attacks, particularly on wp-login.php and xmlrpc.php. By restricting the number of requests from a single IP address, you can significantly reduce the effectiveness of automated attacks.

Rate Limiting Protection Flow

Normal Traffic
5-10 requests/min
โœ… Allowed
โ†’
Rate Limit
30 requests/min
burst: 20
โ†’
Attack Traffic
100+ requests/min
โŒ Blocked (444)

Understanding Rate Limit Zones

Rate limit zones define how requests are tracked and limited. They're defined globally in nginx.conf and applied to specific locations.

Parameter Description Example Value
zone name Identifier for the rate limit zone wp
zone size Memory allocated for tracking IPs 10m (stores ~160,000 IPs)
rate Allowed requests per time period 30r/m (30 requests per minute)
burst Excess requests allowed in short burst 20
nodelay Process burst requests immediately enabled

Configuring Global Rate Limit Zone

Edit the main NGINX configuration:

cd /etc/nginx/
sudo nano nginx.conf

Add the rate limit zone in the http block:

## # Rate Limiting limit_req_zone $binary_remote_addr zone=wp:10m rate=30r/m;
๐Ÿ’ก Explanation:
  • $binary_remote_addr - Uses binary IP format (saves memory)
  • zone=wp:10m - Creates a 10MB shared memory zone named "wp"
  • rate=30r/m - Allows 30 requests per minute per IP

Creating Site-Specific Rate Limiting

Create a rate limiting configuration file:

cd /etc/nginx/includes/
sudo nano rate_limiting_example.com.conf

Add rate limiting for WordPress login and XML-RPC:

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; } 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; }
โš ๏ธ Important: Replace php8.3-fpm-example.com.sock with your actual PHP-FPM socket path. Each site should have its own socket.

Including Rate Limiting in Site Configuration

Edit your site's NGINX configuration:

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

Include the rate limiting configuration:

# Rate Limiting Include include /etc/nginx/includes/rate_limiting_example.com.conf;

Testing and Applying Configuration

Test NGINX configuration:

sudo nginx -t

Reload NGINX:

sudo systemctl reload nginx

Understanding Rate Limit Behavior

Example Scenario:

  • Zone configured: 30 requests/minute with burst=20
  • Average rate: 1 request every 2 seconds (30/60 = 0.5 req/sec)
  • Burst allows up to 20 additional requests immediately
  • After burst is exhausted, requests are limited to 0.5/sec
  • Exceeded requests return status code 444 (connection closed)

Monitoring Rate Limiting

Check NGINX error logs for rate limit violations:

sudo tail -f /var/log/nginx/error.log | grep limiting

Typical rate limit log entry:

2026/01/29 10:30:15 [error] limiting requests, excess: 20.500 by zone "wp", client: 192.168.1.100, server: example.com, request: "POST /wp-login.php HTTP/2.0"

Adjusting Rate Limits

If legitimate users are being blocked, you can adjust the limits:

Use Case Recommended Rate Burst
Very strict (high security) 10r/m 5
Strict (recommended) 30r/m 20
Moderate 60r/m 30
Lenient (multiple users) 120r/m 50

Whitelisting Trusted IPs

To exempt specific IPs from rate limiting:

geo $limit { default 1; 192.168.1.100 0; # Your office IP 10.0.0.0/8 0; # Internal network } map $limit $limit_key { 0 ""; 1 $binary_remote_addr; } limit_req_zone $limit_key zone=wp:10m rate=30r/m;
โœ… Best Practice: Start with strict rate limiting and monitor logs. Adjust based on legitimate traffic patterns. Rate limiting is one of the most effective defenses against brute force attacks with minimal impact on legitimate users.

Additional Rate Limiting Targets

Consider applying rate limiting to these endpoints as well:

  • /wp-cron.php - Prevent cron exhaustion
  • /wp-json/ - Protect REST API endpoints
  • /wp-comments-post.php - Prevent comment spam
  • Custom admin AJAX endpoints

๐Ÿ’พ Database Security and Privileges

WordPress database security is often overlooked but crucial. By limiting database privileges, you can reduce the potential damage from SQL injection attacks and compromised WordPress installations.

Principle of Least Privilege

WordPress typically doesn't need full database privileges. Most operations only require SELECT, INSERT, UPDATE, and DELETE. Administrative operations like CREATE TABLE and ALTER TABLE are only needed during plugin installations or updates.

Revoking All Privileges

First, remove all existing privileges from your WordPress database user:

REVOKE ALL PRIVILEGES ON site_db.* FROM 'site_user'@'hostname';

Granting Minimal Required Privileges

Grant only the essential privileges for normal WordPress operation:

GRANT SELECT, INSERT, UPDATE, DELETE ON site_db.* TO 'site_user'@'hostname';

Granting Additional Privileges When Needed

When installing plugins or updating WordPress, temporarily grant additional privileges:

GRANT CREATE, ALTER, INDEX ON site_db.* TO 'site_user'@'hostname';

After completing the installation or update, revoke these privileges:

REVOKE CREATE, ALTER, INDEX ON site_db.* FROM 'site_user'@'hostname';

Applying Changes

After modifying privileges, flush the privilege tables:

FLUSH PRIVILEGES;

Database Privilege Reference

Privilege Required For Always Needed?
SELECT Reading data โœ… Yes
INSERT Creating new records โœ… Yes
UPDATE Modifying existing records โœ… Yes
DELETE Removing records โœ… Yes
CREATE Creating new tables โŒ Only during setup/plugin install
ALTER Modifying table structure โŒ Only during updates
INDEX Managing indexes โŒ Only during optimization
DROP Deleting tables โŒ Not recommended
โš ๏ธ Important: Never grant DROP, GRANT, or FILE privileges to your WordPress database user. These privileges can be exploited to cause severe damage or compromise other databases on the same server.

Automation Script for Privilege Management

Create a script to easily toggle privileges during maintenance:

#!/bin/bash # wp-db-privileges.sh # Usage: ./wp-db-privileges.sh [enable|disable] DB_NAME="site_db" DB_USER="site_user" DB_HOST="localhost" if [ "$1" == "enable" ]; then mysql -e "GRANT CREATE, ALTER, INDEX ON ${DB_NAME}.* TO '${DB_USER}'@'${DB_HOST}'; FLUSH PRIVILEGES;" echo "Additional privileges granted. Remember to disable after updates!" elif [ "$1" == "disable" ]; then mysql -e "REVOKE CREATE, ALTER, INDEX ON ${DB_NAME}.* FROM '${DB_USER}'@'${DB_HOST}'; FLUSH PRIVILEGES;" echo "Additional privileges revoked. Normal operations only." else echo "Usage: $0 [enable|disable]" fi
โœ… Security Best Practice: Run WordPress with minimal database privileges during normal operation. Only grant additional privileges when performing updates or installations, and immediately revoke them afterward. This dramatically reduces the attack surface for SQL injection and other database-based attacks.

๐Ÿงช Testing and Verification

After implementing all security configurations, thorough testing ensures everything works correctly without breaking functionality.

NGINX Configuration Testing

sudo nginx -t

This command checks for syntax errors. Always run before reloading NGINX.

Testing HTTP Headers

Verify all security headers are present:

curl -I https://example.com

Test specific static asset headers:

curl -I https://example.com/wp-content/themes/your-theme/style.css

External Security Testing Tools

  • SSL Labs: SSL Server Test - Comprehensive SSL/TLS analysis (aim for A+ rating)
  • Security Headers: SecurityHeaders.com - Analyzes HTTP security headers (aim for A+ rating)
  • HTTP/3 Test: HTTP3Check.net - Verify HTTP/3 and QUIC support
  • Mozilla Observatory: Observatory - Overall security scan and recommendations

Functional Testing Checklist

  • โœ… WordPress admin panel accessible
  • โœ… Posts and pages display correctly
  • โœ… Media uploads work (images, videos, documents)
  • โœ… Plugin installation/updates (with appropriate privileges)
  • โœ… Theme customization functions
  • โœ… Contact forms submit properly
  • โœ… Comment system works (if enabled)
  • โœ… Search functionality operational
  • โœ… RSS feeds accessible
  • โœ… AJAX requests complete successfully

Performance Verification

Check server response times:

curl -w "@curl-format.txt" -o /dev/null -s https://example.com

Create curl-format.txt:

time_namelookup: %{time_namelookup}s\n time_connect: %{time_connect}s\n time_appconnect: %{time_appconnect}s\n time_pretransfer: %{time_pretransfer}s\n time_redirect: %{time_redirect}s\n time_starttransfer: %{time_starttransfer}s\n time_total: %{time_total}s\n
๐Ÿ’ก Monitoring Tip: Set up regular automated tests using these commands in a cron job. Alert yourself if response times exceed acceptable thresholds or if security headers are missing.

๐Ÿ” Troubleshooting Common Issues

NGINX Configuration Errors

Issue: NGINX fails to reload after configuration change

Solution: Check error logs for specific issues:

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

File Permission Issues

Issue: WordPress cannot upload files or update

Solution: Verify ownership and permissions:

ls -la /var/www/example.com/public_html/wp-content/uploads/ sudo chown -R username:www-data /var/www/example.com/public_html/wp-content/ sudo chmod 775 /var/www/example.com/public_html/wp-content/uploads/

PHP-FPM Pool Issues

Issue: 502 Bad Gateway errors

Solution: Check PHP-FPM status and logs:

sudo systemctl status php8.3-fpm sudo tail -f /var/log/php8.3-fpm.log sudo tail -f /var/log/fpm-php.example.com.log

Rate Limiting False Positives

Issue: Legitimate users being blocked

Solution: Increase rate limits or add IP whitelist:

# In nginx.conf limit_req_zone $binary_remote_addr zone=wp:10m rate=60r/m; # Increase from 30r/m

SSL Certificate Issues

Issue: Certificate verification failures

Solution: Check certificate validity and renewal:

sudo certbot certificates sudo certbot renew --dry-run
โš ๏ธ Emergency Recovery: If your site becomes completely inaccessible after configuration changes, restore your previous working configuration from backup and reload NGINX. Always keep backups of working configurations before making changes.

๐Ÿ”ง Ongoing Maintenance

Regular Security Tasks

  • Weekly:
    • Review NGINX access and error logs
    • Check for failed login attempts
    • Monitor disk space and resource usage
  • Monthly:
    • Update WordPress core, themes, and plugins
    • Review and update security headers
    • Test SSL certificate renewal
    • Audit user accounts and permissions
  • Quarterly:
    • Full security scan with external tools
    • Review and update firewall rules
    • Optimize database and check backups
    • Update documentation

Log Rotation

Ensure logs don't fill disk space. Configure log rotation:

sudo nano /etc/logrotate.d/nginx
/var/log/nginx/*.log { daily missingok rotate 14 compress delaycompress notifempty create 0640 www-data adm sharedscripts postrotate [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid` endscript }

Backup Strategy

Essential files and directories to backup:

  • /etc/nginx/ - All NGINX configurations
  • /var/www/example.com/ - Website files
  • /etc/php/8.3/fpm/pool.d/ - PHP-FPM pool configs
  • Database dumps (using mysqldump)
  • /etc/letsencrypt/ - SSL certificates
โœ… Automation Tip: Create shell scripts to automate common maintenance tasks. Schedule them with cron for hands-off management while maintaining security and performance.

๐ŸŽฏ Conclusion

This comprehensive guide has covered advanced NGINX configuration for WordPress hosting with a focus on security, performance, and maintainability. By implementing these configurations, you've created a robust, hardened environment that protects against common web attacks while maintaining excellent performance.

Key Achievements

SSL/TLS
A+ Rating
HTTP/3 & QUIC
Security Headers
Multiple Layers
CSP Protection
PHP-FPM Isolation
Per-Site Pools
Resource Limits
Rate Limiting
Brute Force Protection
DDoS Mitigation

Security Layers Implemented

  1. Network Layer: SSL/TLS encryption, HTTP/3, secure ciphers
  2. Application Layer: Security headers, CSP, Referrer Policy
  3. WordPress Layer: File blocking, upload restrictions, disabled functions
  4. Rate Limiting: Login protection, XML-RPC restrictions
  5. File System: Hardened permissions, restricted access
  6. Database Layer: Minimal privileges, access control

Next Steps

  • Monitor logs regularly for suspicious activity
  • Keep all software updated (NGINX, PHP, WordPress)
  • Implement automated backups
  • Consider additional security tools (fail2ban, ModSecurity)
  • Set up monitoring and alerting (Prometheus, Grafana)
  • Document your specific configuration customizations
๐Ÿ“š Further Learning: Security is an ongoing process, not a one-time setup. Stay informed about new vulnerabilities, NGINX updates, and WordPress security best practices. Join security mailing lists and follow reputable security researchers in the web hosting community.

Remember: Security is about layers. No single measure provides complete protection, but together, these configurations create a defense-in-depth strategy that makes your WordPress installation significantly more resistant to attacks.