TL;DR#
Assuming you’ve already got your reverse proxy running, in wp-config.php
add
the following:
1
2
3
4
5
6
7
8
9
| <?php
/** TLS/HTTPS fixes **/
// in some setups HTTP_X_FORWARDED_PROTO might contain a comma-separated list
// e.g. http,https so check for https existence.
if (strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false) {
// update HTTPS server variable to always 'pretend' incoming requests were
// performed via the HTTPS protocol.
$_SERVER['HTTPS']='on';
}
|
If you’re getting desperate:
1
2
3
4
5
6
7
| // If you ever get stuck, you can override the database set site URIs as well.
define( 'WP_HOME', 'https://example.com' );
define( 'WP_SITEURL', 'https://example.com' );
// FORCE_SSL_ADMIN is for when you want to secure logins and the admin area so
// that both passwords and cookies are never sent in the clear.
define( 'FORCE_SSL_ADMIN', true );
|
Introduction#
If you are using a reverse proxy that performs SSL termination for you, you may
find yourself in a redirect loop or getting a lot of mixed content as Wordpress,
quite rightly, acts as if it’s a normal HTTP request.
PHP uses server variables[1] that scripts
can hook into to learn about their world. One of these magic variables,
$_SERVER['HTTPS']
, is set to a non-empty value if the script was queried
through the HTTPS protocol.
Wordpress uses this variable ($_SERVER['HTTPS']
) to detect if an incoming
request should mutate links, hence leading to a nasty redirect loop when behind
a reverse proxy with SSL termination.
⚠️ Warning! There are a lot of example configuration files ahead!
Architecture Overview#
In order to fix it, we can programmatically make use of that standardised
X-Forwarded-Proto
header that can be forwarded via a proxy pass configuration
in nginx, so we need to configure a few things. In this configuration we have
the following architecture:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
+---------------------------+
| |
| +-----------+ |
| | Varnish | |
| +--^----+---+ |
| | | |
+---------------+ | | | |
| Client | | +--+----v-+ |
| +-------->| Nginx | |
| (ノಠ ∩ಠ)ノ彡 | | +-------+-+ |
+---------------+ | | |
| | |
| +--------v----+ |
| | Wordpress | |
| +-------------+ |
| |
| (づ。◕‿‿◕。)づ server |
| |
+---------------------------+
client -> Nginx (https) -> Varnish --(on cache miss)--> Nginx (http) -> wordpress
client -> Nginx (https) -> Varnish (on cache hit)
|
nginx (https) -> varnish configuration#
This configures the client->varnish flow using SSL termination, but setting a
number of X-Forwarded-*
headers that we can use to pick up on.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| // /etc/nginx/conf.d/example.com.https.conf
# nginx (TLS termination) -> varnish cache
server {
listen 443 ssl;
listen [::]:443 ssl;
## Your website name goes here e.g. example.com *.example.com
server_name example.com;
server_name_in_redirect off;
port_in_redirect off;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://varnish;
proxy_set_header Host $http_host;
proxy_set_header HTTPS "on";
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Real-IP $remote_addr;
}
}
upstream varnish {
server varnish:80;
}
|
varnish -> nginx (http) configuration#
Thanks to Linode[2] for the
reference implementation (which has been extended to add a X-Cache
header for
tracking cache hits).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
| // default.vcl
vcl 4.0;
sub vcl_hit {
set req.http.X-Cache = "hit";
}
sub vcl_miss {
set req.http.X-Cache = "miss";
}
sub vcl_pass {
set req.http.X-Cache = "pass";
}
sub vcl_pipe {
set req.http.X-Cache = "pipe uncacheable";
}
// Specify that the backend (nginx) is listening on port 8080.
// This is our internal backend route to wordpress.
backend default {
.host = "nginx";
.port = "8080";
}
// Allow cache-purging requests only from the following hosts.
acl purger {
"localhost";
"127.0.0.1";
}
sub vcl_recv {
unset req.http.X-Cache;
// Allow cache-purging requests only from the IP addresses in the above acl purger.
// If a purge request comes from a different IP address, an error message will be produced.
if (req.method == "PURGE") {
if (!client.ip ~ purger) {
return(synth(405, "This IP is not allowed to send PURGE requests."));
}
return (purge);
}
// Change the X-Forwarded-For header
if (req.restarts == 0) {
if (req.http.X-Forwarded-For) {
set req.http.X-Forwarded-For = client.ip;
}
}
// Exclude POST requests or those with basic authentication from caching
if (req.http.Authorization || req.method == "POST") {
return (pass);
}
// Exclude RSS feeds from caching
if (req.url ~ "/feed") {
return (pass);
}
// Don't cache the WordPress admin and login pages:
if (req.url ~ "wp-admin|wp-login") {
return (pass);
}
// Remove has_js and CloudFlare/Google Analytics __* cookies and statcounter is_unique
set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(_[_a-z]+|has_js|is_unique)=[^;]*", "");
// Remove a ";" prefix, if present.
set req.http.Cookie = regsub(req.http.Cookie, "^;\s*", "");
// WordPress sets many cookies that are safe to ignore.
set req.http.cookie = regsuball(req.http.cookie, "wp-settings-\d+=[^;]+(; )?", "");
set req.http.cookie = regsuball(req.http.cookie, "wp-settings-time-\d+=[^;]+(; )?", "");
set req.http.Cookie = regsuball(req.http.Cookie, "wordpress_test_cookie=[^;]+(; )?", "");
if (req.http.cookie == "") {
unset req.http.cookie;
}
}
// Cache-purging for a particular page must occur each time we make edits
// to that page.
sub vcl_purge {
set req.method = "GET";
set req.http.X-Purger = "Purged";
return (restart);
}
// The sub vcl_backend_response directive is used to handle communication
// with the backend server, nginx. We use it to set the amount of time
// the content remains in the cache.
// We can also set a grace period, which determines how Varnish will serve
// content from the cache even if the backend server is down.
// - Time can be set in seconds (s), minutes (m), hours (h) or days (d).
// Here, we’ve set the caching time to 24 hours, and the grace period to
// 1 hour, but you can adjust these settings based on your needs.
sub vcl_backend_response {
set beresp.ttl = 24h;
set beresp.grace = 1h;
if (bereq.url !~ "wp-admin|wp-login|product|cart|checkout|my-account|/?remove_item=") {
unset beresp.http.set-cookie;
}
}
// Change the headers for purge requests.
sub vcl_deliver {
if (req.http.X-Purger) {
set resp.http.X-Purger = req.http.X-Purger;
}
if (obj.uncacheable) {
set req.http.X-Cache = req.http.X-Cache + " uncacheable" ;
} else {
set req.http.X-Cache = req.http.X-Cache + " cached" ;
}
// set X-Cache header in response
set resp.http.X-Cache = req.http.X-Cache;
}
|
nginx (http) -> wordpress configuration#
Here we use a generic nginx+php-fpm fastcgi_pass configuration.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
| // /etc/nginx/conf.d/example.com.http.conf
# nginx (TLS termination) -> varnish cache -> nginx http -> php-fpm
server {
listen 8080;
listen [::]:8080;
server_name example.com;
server_name_in_redirect off;
port_in_redirect off;
access_log /var/log/nginx/example.com/access.log;
error_log /var/log/nginx/example.com/error.log info;
## Your only path reference.
root /var/www/example.com;
location = /favicon.ico {
log_not_found off;
access_log off;
}
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}
# deny running scripts inside writable directories
location ~* /(images|cache|media|logs|tmp)/.*\.(php|pl|py|jsp|asp|sh|cgi)$ {
return 403;
error_page 403 /403_error.html;
}
location / {
# This is cool because no php is touched for static content.
# include the "?$args" part so non-default permalinks doesn't break when using query string
try_files $uri $uri/ /index.php?$is_args$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_intercept_errors on;
#NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
include /etc/nginx/fastcgi.conf;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass php-fpm;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
expires max;
log_not_found off;
}
}
upstream php-fpm {
server php-fpm:9000;
}
|
Forcing ‘Faux’TTPS for PHP (wp-config.php configuration)#
Woo! Now we have all the above bootstrapped, we can finally grab the
X-Forwarded-Proto
header that’s been passed along, and force PHP to think that
$_SERVER['HTTPS'] = 'on';
by creating an override in wp-config.php
[3]
1
2
3
4
5
6
7
8
9
10
| // wp-config.php
<?php
/** TLS/HTTPS fixes **/
// in some setups HTTP_X_FORWARDED_PROTO might contain a comma-separated list
// e.g. http,https so check for https existence.
if (strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false) {
// update HTTPS server variable to always 'pretend' incoming requests were
// performed via the HTTPS protocol.
$_SERVER['HTTPS']='on';
}
|
If you get stuck, or redirects are still happening, you can use the site
variables to revert to HTTP, or enforce an HTTPS root uri, which will override
the database configuration:
1
2
3
4
5
| // wp-config.php
// If you ever get stuck, you can override the database set site URIs as well.
define( 'WP_HOME', 'https://example.com' );
define( 'WP_SITEURL', 'https://example.com' );
|
Lastly, you can enforce that admin must be visited over HTTPS (FauxTTPS?) by
setting FORCE_SSL_ADMIN
[4] to stop bad
actors trying to jump in over HTTP… ¯\_(ツ)_/¯
1
2
3
4
5
| // wp-config.php
// FORCE_SSL_ADMIN is for when you want to secure logins and the admin area so
// that both passwords and cookies are never sent in the clear.
define( 'FORCE_SSL_ADMIN', true );
|
Addendum#
- ASCII chart created by: asciiflow
- Time to SSL Termination frustration: 7+ hours
- Time to Blog: 1h:39m