NGINX Server Configuration Guide

Section 15: Comprehensive Server Security & Optimization

1. Log Management

Proper log management is essential for monitoring server health, troubleshooting issues, and maintaining security. NGINX logs provide valuable insights into server activity and potential security threats.

Viewing NGINX Logs

Navigate to the NGINX log directory and view log files:

cd /var/log/nginx
ls
sudo cat log_file_name.log
sudo less log_file_name.log

Log File Tips

cat - Displays entire log file content (suitable for small files)

less - Allows scrolling through large log files with search capabilities

2. PHP-FPM Pool Configuration

PHP-FPM pools allow you to run multiple websites with different users and configurations, enhancing security through isolation. Each site can have its own dedicated pool with customized settings.

User Management

Create system users and configure group memberships:

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

Creating Pool Configuration

1Navigate to PHP-FPM pool directory:

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

2Copy default configuration and create site-specific pool:

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

Creating PHP-FPM Log File

sudo touch /var/log/fpm-php.example.com.log
sudo chown username:www-data /var/log/fpm-php.example.com.log
sudo chmod 660 /var/log/fpm-php.example.com.log
sudo systemctl reload php8.3-fpm

Configuring NGINX to Use Custom Pool

Verify socket path:

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

Update NGINX site configuration:

sudo nano /etc/nginx/sites-available/example.com.conf
fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock;

Test and reload NGINX:

sudo nginx -t
sudo systemctl reload nginx

Setting File Permissions

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

Testing PHP Configuration

Create a PHP info file:

cd public_html/
sudo nano filename.php
<?php
phpinfo();
?>

PHP Security Configuration

Enable specific PHP functions:

sudo nano /etc/php/8.3/pool.d/example.com.conf
php_admin_flag[allow_url_fopen] = on

Disable dangerous functions:

; 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
sudo systemctl reload php8.3-fpm

Configuring Temporary Directories

Edit pool configuration:

cd /etc/php/8.3/fpm/pool.d/
sudo nano example.com.conf
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 temporary directory:

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

Open_basedir Restriction

Restrict PHP file access to specific directories:

php_admin_value[open_basedir] = /var/www/example.com/public_html/:/var/www/example.com/tmp/
sudo systemctl reload php8.3-fpm

PHP-FPM Pool Architecture

NGINX
Unix Socket
PHP-FPM Pool
PHP Process

3. SSL Certificate Configuration

SSL/TLS certificates encrypt data transmission between servers and clients, ensuring secure communications. Let's Encrypt provides free, automated SSL certificates.

Installing Certbot

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

Obtaining SSL Certificate

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

Generating DH Parameters

Diffie-Hellman parameters enhance SSL security:

cd /etc/nginx/
sudo mkdir ssl/
cd ssl/
sudo openssl dhparam -out dhparam.pem 2048

SSL Certificate Configuration File

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 Note

Let's Encrypt removed support for SSL stapling on May 7, 2025. The ssl_stapling directives should be commented out or removed.

Secure Server Block Configuration

HTTP to HTTPS redirect:

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}

HTTPS configuration with HTTP/2 and HTTP/3:

# First server (with reuseport)
listen 443 ssl;
http2 on;
listen 443 quic reuseport;
http3 on;

# Second server (without reuseport)
listen 443 ssl;
http2 on;
listen 443 quic;
http3 on;

# Include SSL configuration files
include /etc/nginx/ssl/ssl_example.com.conf;
include /etc/nginx/ssl/ssl_all_sites.conf;

Testing Configuration

sudo nginx -t
sudo systemctl reload nginx

Testing HTTP/3 and QUIC

Add to location / context to test (temporary):

# PREVENTS BROWSER CACHING - USED TO TEST HTTP/3
# COMMENT OR REMOVE AFTER CONFIRMING HTTP/3 IS ENABLED
add_header Cache-Control 'no-cache,no-store';

Add HTTP_HOST parameter in PHP processing block:

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_param HTTP_HOST $host;
    fastcgi_pass unix:/run/php/php8.3-fpm-example.sock;
    include /etc/nginx/includes/fastcgi_optimize.conf;
}

Testing SSL with cURL

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

SSL Testing Resources

SSL Labs: https://www.ssllabs.com/ssltest/ (Should receive A+ rating)

HTTP/3 Check: https://http3check.net/

Browser Console: Network tab → Right-click → Enable "Protocol" column → Look for "h3"

Certbot Management Commands

sudo certbot certificates
sudo certbot delete
sudo certbot renew
sudo certbot renew --force-renewal

SSL Certificate Auto-Renewal

Configure cron for automatic certificate renewal:

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

SSL/TLS Connection Flow

Client Request
TLS Handshake
Certificate Validation
Encrypted Connection

4. HTTP Security Headers

HTTP security headers protect websites from common attacks like XSS, clickjacking, and MIME-type sniffing. Implementing proper security headers is crucial for modern web security.

Creating Security Headers Configuration

cd /etc/nginx/includes/
sudo nano http_headers.conf
# -------------------------------------------------------
# Add Header Referrer-Policy - Uncomment desired directive
# -------------------------------------------------------
#add_header Referrer-Policy "no-referrer";
add_header Referrer-Policy "strict-origin-when-cross-origin";
#add_header Referrer-Policy "unsafe-url";

# ------------------------------------------------------
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "sameorigin";
add_header X-XSS-Protection "1; mode=block";
add_header Permissions-Policy 'accelerometer=(), camera=(), clipboard-read=(), clipboard-write=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), fullscreen=(self "https://www.youtube.com")';
Header Purpose
Referrer-Policy Controls how much referrer information is shared
X-Content-Type-Options Prevents MIME-type sniffing
X-Frame-Options Prevents clickjacking attacks
X-XSS-Protection Enables browser XSS filtering
Permissions-Policy Controls browser features and APIs

Enabling Security Headers

Include in NGINX site configuration (place ABOVE PHP processing block):

cd /etc/nginx/sites-available/
sudo nano example.com.conf
include /etc/nginx/includes/http_headers.conf;
sudo nginx -t
sudo systemctl reload nginx

Browser Caching and Security Headers

Create optimized 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;

5. File Ownership and Permissions

Proper file permissions are critical for security, preventing unauthorized access while allowing necessary operations. We implement two permission schemes: standard and hardened.

Standard Permissions

Suitable for development environments or sites requiring frequent updates:

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
Permission Value Description
Directories 770 Owner and group can read, write, execute
Files 660 Owner and group can read and write
wp-config.php 400 Owner can only read

Hardened Permissions

Recommended for production environments requiring maximum security:

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 {} \;
Location Type Permission Description
Root directories Directory 550 Read and execute only
Root files File 440 Read only
wp-content directories Directory 770 Full access for uploads/plugins
wp-content files File 660 Read and write for content

Permission Hierarchy

Root: 550/440
wp-content: 770/660
wp-config: 400

6. WordPress NGINX Security Directives

This comprehensive firewall ruleset protects WordPress installations from common attacks and vulnerabilities with low false-positive risks.

Creating Security Configuration

cd /etc/nginx/includes/
sudo nano nginx_security_directives.conf
##
# WORDPRESS-SAFE NGINX 8G (based) FIREWALL Ruleset
# Low false-positive risks
# 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 plugin directories
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/dirs
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; }

Enabling WordPress Security

Include in NGINX site configuration (place ABOVE PHP processing block):

cd /etc/nginx/sites-available/
sudo nano example.com.conf
include /etc/nginx/includes/nginx_security_directives.conf;
sudo nginx -t
sudo systemctl reload nginx

Allowing Specific Plugin PHP Execution

Some plugins require PHP execution. Here's how to allow specific files:

1Create test PHP file:

cd /var/www/example.com/
sudo nano public_html/wp-content/plugins/test556.php
<?php
phpinfo();
?>

2Test in browser (should return 403):

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

3Create exception for specific plugin:

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.sock;
    include /etc/nginx/includes/fastcgi_optimize.conf;
}

4Test and reload:

sudo nginx -t
sudo systemctl reload nginx && sudo systemctl restart php8.3-fpm

5Clean up test file:

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

Security Best Practice

Only create exceptions for trusted plugins that absolutely require PHP execution. Each exception potentially opens a security vulnerability.

7. Rate Limiting Configuration

Rate limiting protects your server from brute-force attacks, especially on login pages and XML-RPC endpoints. It restricts the number of requests from a single IP address.

Global Rate Limit Configuration

Edit main NGINX configuration:

cd /etc/nginx/
sudo nano nginx.conf

Add to http block:

##
# Rate Limiting
limit_req_zone $binary_remote_addr zone=wp:10m rate=30r/m;

Rate Limit Explanation

zone=wp:10m - Creates 10MB memory zone named "wp"

rate=30r/m - Allows 30 requests per minute per IP address

burst=20 - Allows burst of 20 additional requests

nodelay - Process burst requests immediately

Creating Site-Specific Rate Limit Configuration

cd /etc/nginx/includes/
sudo nano rate_limiting_example.com.conf
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-MODIFY.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-MODIFY.sock;
    include /etc/nginx/includes/fastcgi_optimize.conf;
}

Important

Replace "MODIFY" with your actual PHP-FPM socket name (e.g., example.com)

Including Rate Limiting in Site Configuration

cd /etc/nginx/sites-available/
sudo nano example.com.conf
# Rate Limiting Include
include /etc/nginx/includes/rate_limiting_example.com.conf;

Test and reload:

sudo nginx -t
sudo systemctl reload nginx

Rate Limiting Flow

Request
Check Zone
Within Limit?
Allow/Deny

Additional WordPress Hardening

Disable file modifications in wp-config.php:

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

8. Database Privilege Hardening

By default, WordPress database users are granted ALL PRIVILEGES. For enhanced security, we restrict privileges to only what's essential for normal operations.

⚠️ Critical Warnings

  • Backup First: Always backup your site and database before making privilege changes
  • Test on Development: Test privilege changes on a development server first
  • WooCommerce Exception: Do NOT restrict privileges for WooCommerce sites - they require full database access
  • Plugin Compatibility: Some plugins may require additional privileges - consult plugin documentation

Understanding Database Privileges

Privilege Description Essential
SELECT Read data from tables ✓ Yes
INSERT Add new records to tables ✓ Yes
UPDATE Modify existing records ✓ Yes
DELETE Remove records from tables ✓ Yes
CREATE Create new tables Optional
ALTER Modify table structure Optional
INDEX Create/drop indexes Optional
DROP Delete tables No

Step-by-Step Privilege Restriction Process

1Locate database credentials:

cd /var/www/example.com/public_html/
grep DB_ wp-config.php

Look for these values:

DB_NAME - Your database name

DB_USER - Your database username

2Log into MariaDB:

sudo mysql

3Revoke all existing privileges:

REVOKE ALL PRIVILEGES ON database_name.* FROM 'database_user'@'localhost';
Example:
REVOKE ALL PRIVILEGES ON mysite_db.* FROM 'mysite_user'@'localhost';

Important Notes

• Ensure no space between database name and period

• Ensure no space between period and asterisk

• Use single quotes around username

4Grant essential privileges:

GRANT SELECT, INSERT, UPDATE, DELETE ON database_name.* TO 'database_user'@'localhost';
Example:
GRANT SELECT, INSERT, UPDATE, DELETE ON mysite_db.* TO 'mysite_user'@'localhost';

5Verify new privileges:

SHOW GRANTS FOR 'database_user'@'localhost';

Expected Output

You should see grants for SELECT, INSERT, UPDATE, and DELETE only

Granting Additional Privileges (If Needed)

If plugins require additional privileges, grant them selectively:

GRANT CREATE, ALTER, INDEX ON database_name.* TO 'database_user'@'localhost';
Example:
GRANT CREATE, ALTER, INDEX ON mysite_db.* TO 'mysite_user'@'localhost';

Verify updated privileges:

SHOW GRANTS FOR 'database_user'@'localhost';

Finalizing Changes

6Flush privileges and exit:

FLUSH PRIVILEGES;
exit

Complete Example Session

-- Clear screen (Ctrl+L)

-- Revoke all privileges
REVOKE ALL PRIVILEGES ON mysite_db.* FROM 'mysite_user'@'localhost';

-- Grant essential privileges
GRANT SELECT, INSERT, UPDATE, DELETE ON mysite_db.* TO 'mysite_user'@'localhost';

-- Verify privileges
SHOW GRANTS FOR 'mysite_user'@'localhost';

-- (Optional) Grant additional privileges
GRANT CREATE, ALTER, INDEX ON mysite_db.* TO 'mysite_user'@'localhost';

-- Verify updated privileges
SHOW GRANTS FOR 'mysite_user'@'localhost';

-- Apply changes
FLUSH PRIVILEGES;

-- Exit MariaDB
exit

Post-Implementation Testing

  1. Test all site functionality thoroughly
  2. Check plugin operations
  3. Verify user registrations (if applicable)
  4. Test content creation and editing
  5. Monitor error logs for privilege-related issues

When to Grant Additional Privileges

CREATE & ALTER: Required by some cache plugins, backup plugins, or plugins that create custom tables

INDEX: Needed by performance optimization plugins that manage database indexes

DROP: Generally not recommended; only grant if absolutely necessary and you trust the plugin

Database Privilege Restriction Process

Backup Database
Revoke ALL
Grant Essential
Test Site
Add Optional (if needed)

✓ Security Benefits

  • Reduced attack surface - malicious code can't create backdoor tables
  • Limited damage from SQL injection attacks
  • Prevents unauthorized table modifications
  • Principle of least privilege applied
  • Better compliance with security standards