📋 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
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
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:
# 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
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
- PHP Pools: Isolate sites for security and customization
- SSL/TLS: Implement HTTPS with HTTP/3 support for maximum performance
- Security Headers: Add comprehensive HTTP headers to protect against common
attacks
- Rate Limiting: Protect wp-login.php and xmlrpc.php from brute force
- File Permissions: Use hardened permissions (550/440) for production sites
- PHP Functions: Disable dangerous functions and enable only what's needed
- Database Privileges: Grant minimum necessary permissions
- 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