🔧 Nginx & PHP-FPM Configuration Guide

Section 15: Comprehensive Server Setup & Security Hardening

📋 Nginx Logs Management

Nginx logs provide critical information for troubleshooting, monitoring traffic, and identifying security issues. The logs are typically stored in the /var/log/nginx directory.

Viewing Nginx Logs

cd /var/log/nginx
ls
sudo cat log_file_name.log
sudo less log_file_name.log
💡 Tip: Use less instead of cat for large log files. It allows you to navigate through the file more efficiently with pagination support.

Access Logs

Contains records of all requests made to the server, including IP addresses, requested URLs, response codes, and user agents.

Error Logs

Records server errors, warnings, and notices. Essential for troubleshooting configuration issues and application errors.

🏊 PHP-FPM Pool Configuration

PHP-FPM (FastCGI Process Manager) pools allow you to isolate PHP processes for different websites or applications. This provides better security, resource management, and the ability to customize PHP settings per site.

PHP-FPM Pool Architecture
Nginx Web Server
PHP-FPM Master Process
Pool 1 (Site A)
User: siteA
Pool 2 (Site B)
User: siteB
Pool 3 (Site C)
User: siteC

Step 1: Create System User

sudo useradd username

Step 2: Configure User Groups

sudo usermod -a -G username_group www-data
sudo usermod -a -G www-data username_group
sudo usermod -a -G username_group $USER
⚠️ Important: After adding a user to a new group, you must log out and log back in for the group changes to take effect. Use the exit command and reconnect to the server.

Step 3: Create Pool Configuration

cd /etc/php/8.3/fpm/pool.d/
ls
sudo cp www.conf example.com.conf
sudo nano example.com.conf

Pool Configuration Example

; Start a new pool named 'example' [example] ; Unix user/group of the child processes user = username group = username ; The address on which to accept FastCGI requests listen = /run/php/php8.3-fpm-example.com.sock ; Set open file descriptor rlimit rlimit_files = 15000 ; Set max core size rlimit rlimit_core = 100 ; PHP configuration flags php_flag[display_errors] = off php_admin_value[error_log] = /var/log/fpm-php.example.com.log php_admin_flag[log_errors] = on

Step 4: Create FPM Log File

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

Step 5: Reload PHP-FPM

sudo systemctl reload php8.3-fpm

Step 6: Configure Nginx to Use the Pool

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

Add or modify the FastCGI pass directive:

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

Step 7: Test and Reload Nginx

sudo nginx -t
sudo systemctl reload nginx

Customizing PHP Settings Per Pool

The pool configuration file allows you to override PHP settings on a per-site basis. This is more granular than the global php.ini file.

Example: Setting Memory Limit

; Uncomment and modify as needed php_admin_value[memory_limit] = 256M

Example: Enabling allow_url_fopen

⚠️ Security Warning: The allow_url_fopen directive allows PHP to access remote files via URLs. This can introduce security vulnerabilities. Only enable it for specific sites that absolutely require it.
php_admin_flag[allow_url_fopen] = on

Example: 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/
cd /var/www/example.com/
sudo mkdir tmp/
sudo chown username:username tmp/
sudo chmod 770 tmp/

Example: Setting open_basedir Restriction

💡 Security Enhancement: The open_basedir directive restricts PHP file operations to specified directories, preventing access to sensitive system files.
php_admin_value[open_basedir] = /var/www/example.com/public_html/:/var/www/example.com/tmp/

Disabling Dangerous PHP Functions

For enhanced security, you should disable PHP functions that are rarely needed and could be exploited by attackers. The following configuration disables numerous potentially dangerous functions while keeping essential ones like disk_free_space enabled.

; ENABLED FUNCTIONS ; disk_free_space ; DISABLED 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
🔴 Important: Keep track of disabled functions. If a plugin requires a specific function, remove it from the disabled functions list to enable it. Always document which functions you've enabled and why.
Function Category Examples Security Risk
System Execution shell_exec, exec, system Can execute arbitrary system commands
Process Control pcntl_exec, pcntl_fork Can spawn new processes
File Operations popen, proc_open Can open processes and pipes
POSIX Functions posix_kill, posix_setuid Can manipulate processes and users

🔒 SSL Certificate Configuration

SSL/TLS certificates encrypt data transmitted between the server and clients, ensuring secure communications. Let's Encrypt provides free SSL certificates with automated renewal.

Installing Certbot

sudo apt update
sudo apt install certbot python3-certbot-dns-cloudflare

Obtaining SSL Certificates

sudo certbot certonly --webroot -w /var/www/example.com/public_html/ -d example.com -d www.example.com

Generating Diffie-Hellman Parameters

The Diffie-Hellman key exchange provides forward secrecy for SSL/TLS connections.

cd /etc/nginx/
sudo mkdir ssl/
cd ssl/
sudo openssl dhparam -out dhparam.pem 2048
💡 Note: Generating a 2048-bit DH parameter can take several minutes. For enhanced security, you can use 4096-bit, but it will take significantly longer to generate.

Creating SSL Configuration Files

Site-Specific Certificate Configuration

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

sudo nano /etc/nginx/ssl/ssl_all_sites.conf
# CONFIGURATION RESULTS IN A+ RATING AT SSLLABS.COM # DATE: 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; # Resolver set to 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; # 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;
⚠️ SSL Stapling Update: Let's Encrypt removed support for SSL stapling on May 7, 2025. The ssl_stapling directives should be commented out or removed from your configuration.

Configuring Server Blocks for HTTPS

HTTP to HTTPS Redirect

server { listen 80; server_name example.com www.example.com; # 301 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 certificates include /etc/nginx/ssl/ssl_certs_example.com.conf; include /etc/nginx/ssl/ssl_all_sites.conf; # Additional configuration... }

HTTPS Server Block (Secondary Sites)

💡 Important: Only the first server block should use reuseport. Additional server blocks should omit this directive.
server { listen 443 ssl; http2 on; listen 443 quic; http3 on; server_name secondsite.com www.secondsite.com; # Include SSL certificates include /etc/nginx/ssl/ssl_certs_secondsite.com.conf; include /etc/nginx/ssl/ssl_all_sites.conf; # Additional configuration... }

Testing HTTP/3 Support

To verify that HTTP/3 is working, you can temporarily disable browser caching:

# PREVENTS BROWSER CACHING - USED TO TEST HTTP3 # REMOVE AFTER TESTING location / { add_header Cache-Control 'no-cache,no-store'; }

Adding HTTP_HOST Parameter

This ensures proper hostname handling in PHP applications:

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

Verifying SSL Configuration

curl -I http://example.com
curl -I https://example.com
sudo nginx -t
sudo systemctl reload nginx
✅ Testing Resources:

Certbot Management Commands

Command Description
sudo certbot certificates List all installed 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 SSL Renewal with Cron

Set up automatic certificate renewal to prevent expiration:

sudo crontab -e
# Renew certificates twice per month (14th and 28th) 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

🛡️ Security Hardening

Implementing comprehensive security measures protects your server from common vulnerabilities and attacks.

HTTP Security Headers

cd /etc/nginx/includes/
sudo nano http_headers.conf
# Referrer Policy - Controls information sent in Referer header add_header Referrer-Policy "strict-origin-when-cross-origin"; # Prevents MIME type sniffing add_header X-Content-Type-Options "nosniff"; # Prevents clickjacking attacks add_header X-Frame-Options "sameorigin"; # XSS Protection (legacy, but still useful) add_header X-XSS-Protection "1; mode=block"; # Permissions Policy - Controls browser features add_header Permissions-Policy 'accelerometer=(), camera=(), clipboard-read=(), clipboard-write=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), fullscreen=(self "https://www.youtube.com")';
Header Purpose Impact
Referrer-Policy Controls referrer information disclosure Privacy protection
X-Content-Type-Options Prevents MIME type sniffing Prevents execution of misidentified files
X-Frame-Options Controls iframe embedding Prevents clickjacking attacks
Permissions-Policy Controls browser feature access Reduces attack surface

WordPress Security Directives

cd /etc/nginx/includes/
sudo nano nginx_security_directives.conf
# WORDPRESS-SAFE NGINX FIREWALL RULESET # Updated December 2026 # Disable favicon logging location = /favicon.ico { access_log off; log_not_found off; } # Deny access to sensitive core and config files location = /wp-config.php { deny all; } location = /wp-admin/install.php { deny all; } location ~* ^/(readme|license|licence)\.(txt|html)$ { deny all; } location ~* \.ini$ { deny all; } # Harden WP core location ~* ^/wp-includes/[^/]+\.php$ { deny all; } location ~* ^/wp-includes/js/tinymce/langs/.+\.php$ { deny all; } location ~* ^/wp-includes/theme-compat/ { deny all; } # Prevent PHP execution in uploads, themes and 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 known vulnerability scanners if ($http_user_agent ~* (nikto|sqlmap|masscan|nmap|dirbuster|acunetix|openvas)) { return 444; }

Allowing Selective PHP Execution in Plugins

Some WordPress plugins require PHP execution. Here's how to allow specific files while maintaining security:

Plugin PHP Execution Strategy
Block ALL PHP in /wp-content/plugins/
Identify Required Plugin Files
Create Specific Location Block
Allow ONLY That File
# Allow specific plugin file to execute PHP location = /wp-content/plugins/plugin-name/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; }

Rate Limiting

Rate limiting protects against brute-force attacks and resource exhaustion.

Global Rate Limit Configuration

sudo nano /etc/nginx/nginx.conf
# Add in http block limit_req_zone $binary_remote_addr zone=wp:10m rate=30r/m;

Per-Site Rate Limiting

cd /etc/nginx/includes/
sudo nano rate_limiting_example.com.conf
# Rate limit wp-login.php 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 xmlrpc.php 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; }
💡 Rate Limit Parameters:
  • rate=30r/m - 30 requests per minute
  • burst=20 - Allow burst of 20 requests
  • nodelay - Process requests immediately
  • limit_req_status 444 - Return 444 (connection closed) when limit exceeded

WordPress Configuration Hardening

Add the following to your wp-config.php file to disable file modifications from the admin panel:

define('DISALLOW_FILE_MODS', true);
sudo systemctl reload php8.3-fpm

Database Privilege Hardening

Restrict database user privileges to only what's necessary:

# Revoke all privileges REVOKE ALL PRIVILEGES ON site_db.* FROM 'site_user'@'hostname'; # Grant only necessary privileges GRANT SELECT, INSERT, UPDATE, DELETE ON site_db.* TO 'site_user'@'hostname'; # Apply changes FLUSH PRIVILEGES;
⚠️ Database Privileges: Only grant CREATE, ALTER, and INDEX privileges temporarily when installing plugins or themes that require schema changes. Remove these privileges afterward.
# Grant additional privileges temporarily GRANT CREATE, ALTER, INDEX ON database_name.* TO 'username'@'localhost'; FLUSH PRIVILEGES; # Revoke after installation REVOKE CREATE, ALTER, INDEX ON database_name.* FROM 'username'@'localhost'; FLUSH PRIVILEGES;

⚡ Performance Optimization

Browser Caching Headers

sudo nano /etc/nginx/includes/browser_caching_security_headers.conf
# Browser caching configuration 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 security headers include /etc/nginx/includes/http_headers.conf; # Disable access logging for static assets access_log off;
Browser Caching Flow
Browser Requests Resource
Check If-Modified-Since Header
Not Modified
304 Response
Modified
200 Response + Content
Cache for 30 Days
Directive Function Benefit
expires 30d Sets cache expiration to 30 days Reduces server requests
etag on Enables entity tags for cache validation Efficient cache invalidation
if_modified_since exact Precise timestamp comparison Accurate cache validation
access_log off Disables logging for static assets Reduces I/O overhead

🔐 File Ownership & Permissions

Proper file permissions are critical for security. WordPress requires specific permissions for different directories and files.

Permission Structure
Directories (770): Owner & Group can read/write/execute
Files (660): Owner & Group can read/write
wp-config.php (400): Owner can only read

Understanding Permission Numbers

Number Permission Binary Meaning
7 rwx 111 Read, Write, Execute
6 rw- 110 Read, Write
5 r-x 101 Read, Execute
4 r-- 100 Read only
0 --- 000 No permissions

Standard Permissions (Development/Testing)

cd /var/www/example.com/
sudo chown -R username:username public_html/
sudo find /var/www/example.com/public_html/ -type d -exec chmod 770 {} \;
sudo find /var/www/example.com/public_html/ -type f -exec chmod 660 {} \;
sudo chmod 400 public_html/wp-config.php
💡 Standard Permissions Use Case: These permissions are suitable for development environments where you need to frequently modify files and install plugins/themes through the WordPress admin interface.

Hardened Permissions (Production)

cd /var/www/example.com/
sudo chown -R username:username public_html/
sudo find /var/www/example.com/public_html/ -type d -exec chmod 550 {} \;
sudo find /var/www/example.com/public_html/ -type f -exec chmod 440 {} \;
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 Permissions: These restrictive permissions significantly enhance security but prevent file modifications through the WordPress admin panel. Use for production sites where changes are made via SFTP or SSH.

Permission Comparison

Standard Permissions

Directories: 770

Files: 660

wp-config.php: 400

Use: Development, frequent updates

Hardened Permissions

Core Directories: 550

Core Files: 440

wp-content Dirs: 770

wp-content Files: 660

Use: Production, maximum security

Understanding Ownership

ls -l

Output example:

drwxrwx--- 5 username username 4096 Jan 28 10:30 public_html -rw-r----- 1 username username 3128 Jan 28 10:25 wp-config.php

Reading the output:

  • First character: d = directory, - = file
  • Next 3 characters: Owner permissions (rwx = 7)
  • Next 3 characters: Group permissions (rwx = 7)
  • Last 3 characters: Other permissions (--- = 0)
  • First name: Owner (username)
  • Second name: Group (username)

Why Group Membership Matters

⚠️ Critical: When you add a user to a group using usermod, you MUST log out and log back in for the changes to take effect. Otherwise, permission changes won't work as expected.
User-Group-Permissions Relationship
PHP-FPM User (siteuser)
Member of Group (siteuser)
Files Owned by siteuser:siteuser
Group Permissions (rw- or rwx) Allow Access

Testing Permissions

After setting permissions, verify by attempting to list directory contents:

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

If you receive "Permission denied," you likely need to log out and back in to activate group membership changes.

Temporary Permission Changes

When installing plugins or themes through WordPress admin, you may need to temporarily relax permissions:

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

After installation, restore hardened permissions:

sudo find /var/www/example.com/public_html/ -type d -exec chmod 550 {} \;
sudo find /var/www/example.com/public_html/ -type f -exec chmod 440 {} \;
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 {} \;

📚 Configuration Summary

Key Takeaways

  1. PHP Pools: Isolate sites for security and customization
  2. SSL/TLS: Implement HTTPS with HTTP/3 support for maximum performance
  3. Security Headers: Add comprehensive HTTP headers to protect against common attacks
  4. Rate Limiting: Protect wp-login.php and xmlrpc.php from brute force
  5. File Permissions: Use hardened permissions (550/440) for production sites
  6. PHP Functions: Disable dangerous functions and enable only what's needed
  7. Database Privileges: Grant minimum necessary permissions
  8. Browser Caching: Optimize performance with proper cache headers

Testing Checklist

Test Command/Tool Expected Result
Nginx Configuration sudo nginx -t Syntax OK, test successful
SSL Rating ssllabs.com A+ Rating
HTTP/3 Support http3check.net HTTP/3 Enabled
Security Headers securityheaders.com A Rating
File Permissions ls -la Correct ownership & permissions
PHP Pool phpinfo() file Correct user & settings

Regular Maintenance Tasks

Daily

  • Monitor error logs
  • Check for failed login attempts

Weekly

  • Review access logs
  • Update WordPress & plugins
  • Check disk space

Monthly

  • Review security headers
  • Test SSL configuration
  • Audit user accounts

Quarterly

  • Review disabled PHP functions
  • Audit database privileges
  • Test backup restoration