WordPress Behind Nginx Reverse Proxy with SSL Termination
Whenever you run a WordPress website behind a reverse proxy with SSL termination, you encounter a fundamental configuration challenge: the reverse proxy handles HTTPS connections from clients but forwards plain HTTP traffic to the WordPress backend. This creates a mismatch where WordPress, configured with HTTPS URLs (e.g., https://yourdomain.com
), receives HTTP requests and becomes confused about the actual protocol being used.
This configuration mismatch typically results in redirect loops, mixed content warnings, broken admin functionality, and WordPress health check failures. This guide provides the complete solution to properly configure both the reverse proxy and WordPress backend to work seamlessly together.
Prerequisites:
- Functioning Nginx reverse proxy server with SSL certificates and basic configuration
- WordPress installation on backend server (Nginx or Apache)
- WordPress configured with HTTPS site URL
The Problem
Client (HTTPS) → Reverse Proxy (SSL termination) → WordPress Backend (HTTP)
What happens:
- Client connects to
https://yourdomain.com
- Reverse proxy terminates SSL, forwards HTTP to WordPress backend
- WordPress thinks it’s being accessed via HTTP but is configured for HTTPS
- WordPress generates incorrect URLs or redirects, causing loops and mixed content
- Result: Site broken, admin inaccessible, or security warnings
Solution Strategy
The solution involves a coordinated configuration between three components: the reverse proxy must send the correct headers indicating the original HTTPS request, the backend web server must be configured to recognize and process these headers, and WordPress must be configured to detect the SSL status from the forwarded headers rather than the direct connection.
The key is using HTTP headers like X-Forwarded-Proto
to communicate the original request protocol from the reverse proxy to WordPress, while also ensuring real client IP addresses are preserved for logging and security purposes. This approach maintains the security benefits of SSL while allowing the backend to operate over HTTP.
Step 1: Configure Nginx Reverse Proxy Location Block
In your reverse proxy’s server block, configure the location directive with the essential headers:
location / {
# Forward to WordPress backend server
proxy_pass http://10.0.10.100:80; # Replace with your WordPress server IP
# Essential headers for SSL termination
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # Critical for HTTPS detection
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Connection optimization
proxy_set_header Connection "";
proxy_http_version 1.1;
# WordPress-specific settings
client_max_body_size 100M; # File upload limit
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 300s; # Important for admin operations
# Performance optimization
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
# Preserve original redirects
proxy_redirect off;
}
Critical Headers Explained:
X-Forwarded-Proto $scheme
– Tells backend the original protocol (https)X-Real-IP $remote_addr
– Original client IP for logging and securityX-Forwarded-For $proxy_add_x_forwarded_for
– Complete IP chain for real IP detectionX-Forwarded-Host $host
– Original domain name requested
Step 2: Configure WordPress Backend Server
For Nginx Backend
Add this to your WordPress server’s Nginx configuration:
server {
listen 80;
server_name yourdomain.com;
root /var/www/html;
index index.php index.html;
# Enable real IP logging from reverse proxy
set_real_ip_from 10.0.10.1; # Your reverse proxy IP
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# Custom log format to capture real client IPs
log_format real_ip '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'"$http_x_forwarded_for"';
access_log /var/log/nginx/yourdomain.com-access.log real_ip;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
# Pass reverse proxy headers to PHP
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for;
fastcgi_param HTTP_X_REAL_IP $http_x_real_ip;
# Standard PHP-FPM configuration
fastcgi_pass unix:/run/php/php8.4-fpm.sock;; # Adjust for your setup
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
For Apache Backend
Add this to your VirtualHost configuration:
<VirtualHost *:80>
ServerName yourdomain.com
DocumentRoot /var/www/html
# Enable required modules (mod_remoteip, mod_setenvif)
# a2enmod remoteip setenvif
# Handle reverse proxy headers BEFORE any processing
SetEnvIf X-Forwarded-Proto "^https$" HTTPS=on
SetEnvIf X-Forwarded-Proto "^https$" SERVER_PORT=443
SetEnvIf X-Forwarded-Host "^(.+)$" HTTP_HOST=$1
# Real IP detection for logging
RemoteIPHeader X-Forwarded-For
RemoteIPTrustedProxy 10.0.10.1 # Your reverse proxy IP
RemoteIPInternalProxy 10.0.10.1
# Custom log format to show real client IPs
LogFormat "%a %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\" \"%{X-Forwarded-For}i\"" real_ip
CustomLog ${APACHE_LOG_DIR}/yourdomain.com-access.log real_ip
# Standard WordPress configuration
<Directory /var/www/html>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
Step 3: Configure WordPress (wp-config.php)
Add this code to your WordPress wp-config.php
file before the /* That's all, stop editing! */
line:
<?php
/**
* Handle SSL termination at reverse proxy
* This configuration ensures WordPress correctly detects HTTPS
* and uses real client IP addresses
*/
// Detect HTTPS from reverse proxy
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
$_SERVER['SERVER_PORT'] = 443;
// Force WordPress admin to use HTTPS
define('FORCE_SSL_ADMIN', true);
}
// Handle real client IP address for security plugins and logging
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$forwardedForHeaders = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$_SERVER['REMOTE_ADDR'] = trim($forwardedForHeaders[0]);
}
// Alternative: Use X-Real-IP if available (more direct)
if (isset($_SERVER['HTTP_X_REAL_IP']) && !empty($_SERVER['HTTP_X_REAL_IP'])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_REAL_IP'];
}
// Optional: Explicitly set WordPress URLs if database still contains HTTP URLs
// Uncomment and modify if WordPress has persistent URL issues
/*
define('WP_HOME', 'https://yourdomain.com');
define('WP_SITEURL', 'https://yourdomain.com');
*/
// Security: Only trust these headers from known proxy (optional but recommended)
$trusted_proxies = ['10.0.3.1']; // Add your reverse proxy IP
if (!in_array($_SERVER['REMOTE_ADDR'] ?? '', $trusted_proxies)) {
// Don't process forwarded headers if not from trusted proxy
// This prevents header spoofing attacks
}
?>
Step 4: Fix WordPress Self-Connection Issues
WordPress makes internal HTTP requests for cron jobs, updates, and health checks. Configure the WordPress server to route these through your reverse proxy.
On your WordPress backend server, edit /etc/hosts
:
# Ensure WordPress connects to itself via reverse proxy
10.0.10.1 yourdomain.com # Replace with your reverse proxy IP
Why this is essential:
- WordPress Site Health tool requires loopback connectivity
- Cron jobs and automatic updates need proper SSL handling
- Plugin API calls must follow the same SSL-terminated path
Step 5: Testing and Verification
Test 1: Check HTTPS Detection
Create a temporary PHP file to verify configuration:
<?php
// test-ssl.php - Remove after testing
echo "<h3>SSL Detection Test</h3>";
echo "HTTPS: " . ($_SERVER['HTTPS'] ?? 'not set') . "<br>";
echo "SERVER_PORT: " . ($_SERVER['SERVER_PORT'] ?? 'not set') . "<br>";
echo "is_ssl(): " . (function_exists('is_ssl') && is_ssl() ? 'true' : 'false') . "<br>";
echo "X-Forwarded-Proto: " . ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? 'not set') . "<br>";
echo "Real IP: " . $_SERVER['REMOTE_ADDR'] . "<br>";
?>
Expected results:
- HTTPS: on
- SERVER_PORT: 443
- is_ssl(): true
- X-Forwarded-Proto: https
- Real IP: (actual client IP, not proxy IP)
Test 2: WordPress Site Health
- Go to WordPress Admin → Tools → Site Health
- Info tab → Server section → “Loopback request” should show ✓
- No SSL-related errors should appear
Test 3: Log File Verification
Check your web server logs to confirm real client IPs are being logged instead of the reverse proxy IP.
Test 4: Security Plugin Compatibility
If using security plugins (Wordfence, Sucuri, etc.), verify they detect real client IPs for rate limiting and blocking.
Common Issues and Solutions
Issue: WordPress Admin Redirect Loops
Solution: Ensure FORCE_SSL_ADMIN
is defined and X-Forwarded-Proto detection is working
Issue: Mixed Content Warnings
Solution: Check database for hardcoded HTTP URLs, use search-replace tools if needed
Issue: Real IPs Not Logged
Solution: Verify web server real IP configuration and trusted proxy settings
Issue: Plugin API Failures
Solution: Confirm /etc/hosts entry routes domain to reverse proxy, not localhost
Issue: File Upload Problems
Solution: Check client_max_body_size
in reverse proxy and backend server limits
Security Considerations
- Header Validation: Only process X-Forwarded headers from trusted proxy IPs
- Firewall Rules: Ensure WordPress backend is only accessible via reverse proxy
- Log Monitoring: Monitor logs for real client IPs to detect abuse patterns
- SSL Enforcement: Use HSTS headers at reverse proxy level for additional security
This configuration ensures WordPress operates correctly behind SSL-terminating reverse proxies while maintaining security and proper logging of real client IP addresses.