Securing Nginx and PHP

Connection Limiting in Nginx

PHP-FPM can't handle nearly as many concurrent requests as Nginx. Without limits, one aggressive user can exhaust your PHP pool and serve 502/504 errors to everyone else.

In your nginx.conf:

limit_conn_zone $binary_remote_addr zone=phplimit:1m;

In your PHP location block:

location ~ \.php$ {
    try_files $uri =404;
    limit_conn phplimit 5;
    include fastcgi_params;
    fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
}

This caps any single IP to 5 concurrent PHP requests. Legitimate users behind shared IPs (schools, offices) still get through while an attacker hammering your contact form gets throttled. Adjust the number up or down based on your traffic patterns.


Rate Limiting

For endpoints that do expensive work — search, login forms, API endpoints — rate limiting prevents abuse. Define a zone in nginx.conf:

limit_req_zone $binary_remote_addr zone=searchlimit:10m rate=1r/s;

Apply it to a specific location:

location /search {
    limit_req zone=searchlimit burst=3 nodelay;
    try_files $uri /index.php$is_args$args;
}

This allows one search request per second with a burst of 3. For more on this, see Preventing Search Overload.


WordPress-Specific Blocks

If you run WordPress, add these in the WordPress server block. All of them must appear before your general location ~ \.php$ handler — Nginx processes regex locations in order, and the first match wins.

Block PHP execution in the uploads directory:

location ~* ^/wp-content/uploads/.*\.php$ {
    deny all;
    access_log off;
    log_not_found off;
}

There is no legitimate reason for PHP files to live in the uploads folder. If one appears, it's a compromised plugin or theme that allowed an attacker to upload a backdoor.

Block access to sensitive WordPress files:

location ~* ^/(wp-config\.php|readme\.html|license\.txt) {
    deny all;
    access_log off;
    log_not_found off;
}

wp-config.php contains your database credentials, readme.html leaks your WordPress version, and license.txt confirms you're running WordPress. WordPress does attempt to protect these internally, but by the time the request reaches PHP it has already consumed a PHP-FPM worker, spun up the WordPress bootstrap, and burned CPU cycles just to return a 403. Letting Nginx drop the request before PHP ever sees it saves that overhead — and during a bot swarm hammering /wp-config.php, that adds up fast.

Block xmlrpc.php:

location = /xmlrpc.php {
    deny all;
    access_log off;
    log_not_found off;
}

XML-RPC is a persistent attack vector — it's used for brute-force login attempts and pingback-based DDoS amplification. Most modern WordPress sites don't use it; if you rely on the WordPress mobile app or Jetpack, you may need to leave it open. For everyone else, blocking it eliminates a major source of noise.

Block the setup-config installer on existing sites:

location = /wp-admin/setup-config.php {
    deny all;
    access_log off;
    log_not_found off;
}

This file runs the initial WordPress database configuration. Once your site is installed, there's zero reason for it to be reachable. Leaving it open invites someone to try connecting your database to theirs.

Set appropriate headers on /wp-admin:

location /wp-admin {
    expires 0;
    client_max_body_size 8M;
}

Force no caching on the admin area — stale admin pages cause more problems than they solve. The client_max_body_size inside this block ensures media uploads through the admin panel aren't rejected by a lower global limit.

These blocks are the essentials. For the full production wp_security.conf — including sitemap caching rules, Nginx configuration, static caching, rate limiting, and PHP-FPM tuning — see the WordPress on Nginx installation guide.


Further Reading

Technical Audit Summary

This guide is maintained as part of a modular, SSL-first framework. Each configuration is audited for production stability and modern security standards.

Last Audit: May 2026
Environment: Debian Trixie (13)
Nginx: 1.31.2
PHP-FPM: 8.5.6

Compatibility: Tested against current stable releases. While optimized for the stack above, core logic remains relevant for Nginx 1.26+ and PHP 8.2+ environments.

2026-05-26: Added database security hygiene note — per-site MySQL users scoped to a single database as the complement to open_basedir filesystem isolation.

2026-05-21: Production audit — synced stack versions (Nginx 1.30.1, PHP-FPM 8.5.6). Added open_basedir restriction via fastcgi_param PHP_ADMIN_VALUE as defense-in-depth against cross-site filesystem access. Added botstop rate-limiting pattern on deny blocks to throttle scanners between the first blocked request and fail2ban jail thresholds. Added tfstate/tfvars to sensitive extension list. Added client_max_body_size 8M to /wp-admin location for media uploads. Linked to the full WordPress on Nginx installation guide for sitemap caching rules and expanded wp_security.conf.