When I first set up Nginx for a WordPress site, I focused entirely on making things work—getting
PHP-FPM connected, configuring the WordPress permalinks, setting up SSL. Security hardening came
later, almost as an afterthought. That approach cost me when a site I managed got compromised
through a file upload vulnerability that wouldn’t have worked if I’d properly blocked PHP execution
in the uploads directory.

Nginx sits at the front of your WordPress stack, receiving every request before PHP ever sees it.
This position makes Nginx configuration critical for security—many attacks can be stopped at the
Nginx level before they reach WordPress at all. Block access to sensitive files, limit rate of
attack attempts, require secure protocols, and you’ve eliminated entire categories of threats.

This guide covers the Nginx hardening configuration I now apply to every WordPress server I manage.
You’ll learn how to protect sensitive WordPress files, implement security headers, rate limit brute
force attacks, configure secure TLS, and create layered defenses that work together. Every
configuration shown is production-tested and explained so you understand not just what to do but
why.

Understanding Nginx’s Security Role

Nginx handles incoming HTTP requests and routes them appropriately—static files get served directly,
PHP requests go to PHP-FPM, and some requests get rejected outright. This request routing gives
Nginx powerful security capabilities.

Defense at the Edge

Think of Nginx as a security checkpoint before your application. Attackers probing your WordPress
installation send requests that Nginx sees first. Requests for wp-config.php, attempts to access
.git directories, SQL injection probes in query strings—Nginx can recognize and block these before
they ever reach PHP.

This matters because PHP execution is relatively expensive and potentially dangerous. Every request
that reaches PHP runs application code with all its complexity. Requests that Nginx blocks consume
minimal resources and can’t exploit application vulnerabilities because they never reach the
application.

What Nginx Can and Can’t Protect

Nginx excels at access control (blocking requests to sensitive paths), protocol security (enforcing
HTTPS, modern TLS), rate limiting (slowing attack attempts), and header injection (adding security
headers to responses).

Nginx cannot protect against vulnerabilities within your PHP code, WordPress plugins, or application
logic. If a plugin has an SQL injection vulnerability, Nginx can’t inspect PHP’s database queries.
That’s application-layer security requiring different defenses.

The goal is layered defense: Nginx blocks what it can, reducing the attack surface that reaches your
application. Your application handles what passes Nginx’s checks.

Hiding Server Information

The first hardening step is reducing information disclosure. Attackers probe for version numbers to
identify known vulnerabilities. Don’t help them.

Nginx Version Disclosure

By default, Nginx includes its version in the Server header and error pages. An attacker seeing
“nginx/1.18.0” might know that version has specific vulnerabilities. Removing this information adds
obscurity that slows reconnaissance.

In your Nginx configuration’s http block, add: server_tokens off;

This removes version information from headers and error pages. The Server header changes from
“nginx/1.18.0” to just “nginx”. Some administrators prefer hiding even that, using more_set_headers
(from nginx-extras) to replace the header entirely.

PHP Version Disclosure

PHP similarly exposes version information via the X-Powered-By header. This isn’t controlled by Nginx
but by PHP configuration. In php.ini, set: expose_php = Off

After restarting PHP-FPM, responses no longer include “X-Powered-By: PHP/8.3.0”. Attackers must probe
more actively to determine your PHP version.

Protecting Sensitive WordPress Files

WordPress includes files that should never be served to the public. Direct access to some files
reveals configuration; access to others enables attack vectors.

wp-config.php Protection

wp-config.php contains database credentials, authentication keys, and other sensitive configuration.
If served as text (due to PHP misconfiguration or file extension manipulation), all your secrets are
exposed.

Block all access with a location block: location = /wp-config.php { deny all; }

Some configurations also block access to any file starting with “wp-config” to catch backup files
like wp-config.php.bak: location ~* ^/wp-config { deny all; }

Hidden Files and Directories

Development artifacts like .git directories, .env files, or .htaccess (from Apache migrations) should
never be public. Block all dotfiles: location ~ /. { deny all; }

This single rule blocks .git, .svn, .env, .htaccess, and any other hidden file or directory. The
exception you might need is .well-known for Let’s Encrypt verification: location ~ /.well-known {
allow all; }

Readme and License Files

WordPress readme.html and license.txt reveal your WordPress version. Plugin and theme readme files
reveal their versions. Block access to these: location ~* (readme.html|readme.txt|license.txt)$ {
deny all; }

PHP Execution in Uploads

The wp-content/uploads directory stores user-uploaded files—images, documents, media. Legitimate
uploads are never PHP files, but attackers try to upload malicious PHP disguised as images. If that
PHP executes, the attacker owns your server.

Block PHP execution in uploads entirely: location ~* /wp-content/uploads/.*.php$ { deny all; }

This returns 403 for any .php request under uploads, whether uploaded legitimately or through a
vulnerability. Static files still serve normally; only PHP execution is blocked.

PHP Execution in wp-includes

WordPress wp-includes contains core library files not meant for direct access. Block direct PHP
requests: location ~* /wp-includes/.*.php$ { deny all; }

You might need exceptions for specific files required by some plugins: location ~*
/wp-includes/js/tinymce/wp-tinymce.php$ { allow all; }

Rate Limiting Attack Attempts

WordPress login (wp-login.php) and XML-RPC (xmlrpc.php) are constant brute force targets. Attackers
try thousands of password combinations hoping to find weak credentials. Rate limiting slows these
attacks to a crawl.

Defining Rate Limit Zones

First, define rate limit zones in the http block. These zones track request rates by client IP:
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;

This creates a zone named “login” with 10MB of storage (tracking ~160,000 IPs) allowing 1 request per
second per IP. Adjust the rate based on your legitimate usage—1r/s is aggressive but appropriate for
login pages.

Applying Rate Limits to Login

Apply the limit to wp-login.php: location = /wp-login.php { limit_req zone=login burst=5 nodelay;
include fastcgi_params; fastcgi_pass unix:/var/run/php/php8.3-fpm.sock; fastcgi_param
SCRIPT_FILENAME $document_root$fastcgi_script_name; }

The burst=5 allows brief bursts (a legitimate user might trigger multiple requests during login),
while nodelay processes burst requests immediately rather than queuing them. Beyond the burst,
excess requests get 503 errors.

Handling XML-RPC

XML-RPC is a legacy API that most modern sites don’t need. If you don’t use it (or Jetpack, which
requires it), block it entirely: location = /xmlrpc.php { deny all; }

If you need XML-RPC, apply even stricter rate limiting than login—XML-RPC allows multiple
authentication attempts per request, making it more efficient for brute force: limit_req_zone
$binary_remote_addr zone=xmlrpc:10m rate=1r/m; location = /xmlrpc.php { limit_req zone=xmlrpc
burst=2 nodelay; … }

One request per minute is extremely restrictive but appropriate for XML-RPC’s legitimate (minimal)
usage patterns.

Connection Limits

Beyond request rate, limit concurrent connections per IP: limit_conn_zone $binary_remote_addr
zone=addr:10m; limit_conn addr 10;

This prevents a single IP from opening excessive simultaneous connections, defending against
slowloris-style attacks and resource exhaustion.

TLS/SSL Configuration

HTTPS isn’t just enabled or disabled—the quality of your TLS configuration matters for both security
and compatibility.

Modern Protocol Selection

TLS 1.0 and 1.1 have known vulnerabilities and are deprecated by all major browsers. Only enable TLS
1.2 and 1.3: ssl_protocols TLSv1.2 TLSv1.3;

TLS 1.3 is the current standard with improved security and performance (faster handshakes). TLS 1.2
maintains compatibility with slightly older clients.

Cipher Suite Selection

Cipher suites determine the specific encryption algorithms used. Server preference ensures your
preferred (stronger) ciphers are used: ssl_prefer_server_ciphers on;

For cipher suite selection, Mozilla’s SSL Configuration Generator provides current recommendations. A
modern configuration might look like: ssl_ciphers
ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;

This prioritizes ECDHE key exchange (forward secrecy), AEAD ciphers (authenticated encryption), and
excludes weak algorithms. The specific configuration evolves as cryptography advances.

Session Optimization

TLS session caching improves performance for returning connections: ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d; ssl_session_tickets off;

Session tickets are disabled because their implementation in some configurations undermines forward
secrecy. Session cache provides similar performance benefits without that risk.

HSTS Implementation

HTTP Strict Transport Security tells browsers to always use HTTPS. Add the header: add_header
Strict-Transport-Security “max-age=31536000; includeSubDomains” always;

Only add this after confirming HTTPS works perfectly across your entire site and all subdomains. Once
browsers see this header, they refuse HTTP connections for the specified duration—recovery from
HTTPS problems becomes difficult.

Security Headers Configuration

Security headers instruct browsers to enable protective behaviors. They’re defense-in-depth measures
that work even if application code has vulnerabilities.

Core Security Headers

Add these headers in your server block or http block: add_header X-Frame-Options “SAMEORIGIN” always;
add_header X-Content-Type-Options “nosniff” always; add_header X-XSS-Protection “1; mode=block”
always; add_header Referrer-Policy “strict-origin-when-cross-origin” always;

X-Frame-Options prevents clickjacking by controlling framing. X-Content-Type-Options prevents MIME
type sniffing. X-XSS-Protection enables browser XSS filtering (legacy but harmless). Referrer-Policy
controls information leakage to third-party sites.

Permissions-Policy

Disable browser features you don’t use: add_header Permissions-Policy “accelerometer=(), camera=(),
geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()” always;

If compromised code tries to access the camera or location, the browser refuses based on your policy.
Limit the features available to limit potential abuse.

Content-Security-Policy

CSP is the most powerful header but requires careful configuration for WordPress. Start with
report-only mode to understand what would be blocked: add_header Content-Security-Policy-Report-Only
“default-src ‘self’; script-src ‘self’ ‘unsafe-inline’; style-src ‘self’ ‘unsafe-inline’
https://fonts.googleapis.com; font-src ‘self’ https://fonts.gstatic.com; img-src ‘self’ data:;
frame-ancestors ‘self’;”

Monitor the browser console for violations, adjust the policy to allow legitimate resources, and
eventually switch to enforcing mode.

Request Filtering

Beyond blocking specific files, filter obviously malicious request patterns.

Query String Filtering

Block requests with SQL injection or XSS patterns in query strings: if ($query_string ~* “(
|%3E)”) { return 403; } if ($query_string ~* “UNION.*SELECT”) { return 403; }

These patterns catch obvious attacks but won’t stop sophisticated attackers who encode or obfuscate
payloads. They’re useful as first-line defense, not as complete protection.

User Agent Filtering

Block known malicious user agents: if ($http_user_agent ~* (wget|curl|libwww-perl|nikto|sf|sqlmap)) {
return 403; }

Legitimate users rarely use these tools. Blocking them stops casual scanning. Sophisticated attackers
will spoof user agents, so this is defense in depth rather than primary protection.

Request Method Restriction

Most WordPress sites only need GET, POST, and HEAD requests: if ($request_method !~
^(GET|POST|HEAD)$) { return 405; }

This blocks PUT, DELETE, OPTIONS, and other methods that might be used in attacks or probing.

Complete Configuration Example

Here’s a complete security-focused Nginx server block for WordPress:

The configuration combines all hardening measures: server_tokens hidden, security headers, file
access restrictions, rate limiting on login endpoints, proper TLS configuration, and protection of
sensitive WordPress files. Each component contributes to defense in depth.

Testing Your Configuration

Syntax Verification

Before reloading, always test configuration syntax: nginx -t

This catches typos and misconfigurations before they affect your live site. Never reload without
passing syntax check.

Functionality Testing

After applying hardening, test core WordPress functions: login works, posts save, media uploads
succeed, admin pages load. Overly aggressive restrictions can break legitimate functionality.

Security Verification

Verify your hardening works. Try accessing blocked paths directly and confirm 403/404 responses. Test
rate limiting by exceeding limits intentionally. Run security scanners like Mozilla Observatory or
Security Headers to grade your configuration.

Check TLS configuration at SSL Labs (ssllabs.com/ssltest). Aim for A or A+ rating with no warnings
about weak protocols or ciphers.

Maintaining Security

Security configuration isn’t set-and-forget. Maintain it over time.

Keep Nginx updated for security patches. Monitor logs for blocked requests that might indicate attack
patterns. Revisit TLS configuration periodically as cryptographic best practices evolve. Test
changes in staging before production.

When adding new plugins or functionality, verify they work with your restrictions. Some plugins
require specific file access that your hardening might block—adjust rules as needed while
maintaining security.

Conclusion

Nginx hardening creates a security layer before your WordPress application. By blocking access to
sensitive files, limiting attack rates, enforcing secure protocols, and adding security headers,
you’ve eliminated entire categories of threats.

The configurations in this guide are production-tested starting points. Your specific site might need
adjustments—plugins requiring specific access, third-party services needing CSP exceptions,
different rate limits based on your traffic patterns. Start conservative, test thoroughly, and
adjust based on what breaks.

Remember that Nginx security is one layer. Application security (WordPress updates, secure plugins,
strong passwords) and hosting security (firewall, server hardening, monitoring) are equally
important. Together, these layers provide robust protection.