Skip to content

Instantly share code, notes, and snippets.

@matthewjackowski
Last active December 27, 2022 02:56
Show Gist options
  • Save matthewjackowski/062be03b41a68edbadfc to your computer and use it in GitHub Desktop.
Save matthewjackowski/062be03b41a68edbadfc to your computer and use it in GitHub Desktop.
Varnish 4 VCL configuration for WordPress. Also allows purging
# A heavily customized VCL to support WordPress
# Some items of note:
# Supports https
# Supports admin cookies for wp-admin
# Caches everything
# Support for custom error html page
vcl 4.0;
import directors;
import std;
# Assumed 'wordpress' host, this can be docker servicename
backend default {
.host = "wordpress";
.port = "80";
}
acl purge {
"localhost";
"127.0.0.1";
}
sub vcl_recv {
# Only a single backend
set req.backend_hint= default;
# Setting http headers for backend
set req.http.X-Forwarded-For = client.ip;
set req.http.X-Forwarded-Proto = "https";
# Unset headers that might cause us to cache duplicate infos
unset req.http.Accept-Language;
unset req.http.User-Agent;
# The purge...no idea if this works
if (req.method == "PURGE") {
if (!client.ip ~ purge) {
return(synth(405,"Not allowed."));
}
ban("req.url ~ /");
return (purge);
}
if ( std.port(server.ip) == 6080) {
set req.http.x-redir = "https://" + req.http.host + req.url;
return (synth(750, "Moved permanently"));
}
# drop cookies and params from static assets
if (req.url ~ "\.(gif|jpg|jpeg|swf|ttf|css|js|flv|mp3|mp4|pdf|ico|png)(\?.*|)$") {
unset req.http.cookie;
set req.url = regsub(req.url, "\?.*$", "");
}
# drop tracking params
if (req.url ~ "\?(utm_(campaign|medium|source|term)|adParams|client|cx|eid|fbid|feed|ref(id|src)?|v(er|iew))=") {
set req.url = regsub(req.url, "\?.*$", "");
}
# pass wp-admin urls
if (req.url ~ "(wp-login|wp-admin)" || req.url ~ "preview=true" || req.url ~ "xmlrpc.php") {
return (pass);
}
# pass wp-admin cookies
if (req.http.cookie) {
if (req.http.cookie ~ "(wordpress_|wp-settings-)") {
return(pass);
} else {
unset req.http.cookie;
}
}
}
sub vcl_backend_response {
# retry a few times if backend is down
if (beresp.status == 503 && bereq.retries < 3 ) {
return(retry);
}
if (bereq.http.Cookie ~ "(UserID|_session)") {
# if we get a session cookie...caching is a no-go
set beresp.http.X-Cacheable = "NO:Got Session";
set beresp.uncacheable = true;
return (deliver);
} elsif (beresp.ttl <= 0s) {
# Varnish determined the object was not cacheable
set beresp.http.X-Cacheable = "NO:Not Cacheable";
} elsif (beresp.http.set-cookie) {
# You don't wish to cache content for logged in users
set beresp.http.X-Cacheable = "NO:Set-Cookie";
set beresp.uncacheable = true;
return (deliver);
} elsif (beresp.http.Cache-Control ~ "private") {
# You are respecting the Cache-Control=private header from the backend
set beresp.http.X-Cacheable = "NO:Cache-Control=private";
set beresp.uncacheable = true;
return (deliver);
} else {
# Varnish determined the object was cacheable
set beresp.http.X-Cacheable = "YES";
# Remove Expires from backend, it's not long enough
unset beresp.http.expires;
# Set the clients TTL on this object
set beresp.http.cache-control = "max-age=900";
# Set how long Varnish will keep it
set beresp.ttl = 1w;
# marker for vcl_deliver to reset Age:
set beresp.http.magicmarker = "1";
}
# unset cookies from backendresponse
if (!(bereq.url ~ "(wp-login|wp-admin)")) {
set beresp.http.X-UnsetCookies = "TRUE";
unset beresp.http.set-cookie;
set beresp.ttl = 1h;
}
# long ttl for assets
if (bereq.url ~ "\.(gif|jpg|jpeg|swf|ttf|css|js|flv|mp3|mp4|pdf|ico|png)(\?.*|)$") {
set beresp.ttl = 365d;
}
set beresp.grace = 1w;
}
sub vcl_hash {
if ( req.http.X-Forwarded-Proto ) {
hash_data( req.http.X-Forwarded-Proto );
}
}
sub vcl_backend_error {
# display custom error page if backend down
if (beresp.status == 503 && bereq.retries == 3) {
synthetic(std.fileread("/etc/varnish/error503.html"));
return(deliver);
}
}
sub vcl_synth {
# redirect for http
if (resp.status == 750) {
set resp.status = 301;
set resp.http.Location = req.http.x-redir;
return(deliver);
}
# display custom error page if backend down
if (resp.status == 503) {
synthetic(std.fileread("/etc/varnish/error503.html"));
return(deliver);
}
}
sub vcl_deliver {
# oh noes backend is down
if (resp.status == 503) {
return(restart);
}
if (resp.http.magicmarker) {
# Remove the magic marker
unset resp.http.magicmarker;
# By definition we have a fresh object
set resp.http.age = "0";
}
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
} else {
set resp.http.X-Cache = "MISS";
}
set resp.http.Access-Control-Allow-Origin = "*";
}
sub vcl_hit {
if (req.method == "PURGE") {
return(synth(200,"OK"));
}
}
sub vcl_miss {
if (req.method == "PURGE") {
return(synth(404,"Not cached"));
}
}
@marco41
Copy link

marco41 commented Jan 8, 2018

@informdev thank you so much. My problem is resolved.

@hazhayder
Copy link

It shows apache default page when i ran this vcl file.

@xoroz
Copy link

xoroz commented Apr 12, 2018

Tried all configurations I still have the same problem as Marco41...
TinyMCE does not work once varnish is activated. Without it, it works!

Please someone help us out.

I noticed without varnish these URLs

http://felipeferreira.net/wp-content/themes/bandana/css/editor-style.css?wp-mce-4607-20180123-tadv-4.6.7
http://felipeferreira.net/wp-includes/js/tinymce/plugins/compat3x/plugin.min.js?ver=4607-20180123-tadv-4.6.7
http://felipeferreira.net/wp-includes/js/tinymce/tinymce.min.js?ver=4607-20180123-tadv-4.6.7
http://felipeferreira.net/wp-includes/js/tinymce/skins/wordpress/wp-content.css?ver=4.9.5&wp-mce-4607-20180123-tadv-4.6.7

Then with varnish ON, I no longer see those, and I see something trunked like:
http://felipeferreira.net/wp-includes/js/mce-view.min.js?ver=4.9.5

have been struginling with this problem for over 2 years now, I have tried
if (req.url ~ "wp-(login|admin|comments-post.php|cron.php)" ||
req.url ~ "preview=true" ||
req.url ~ "xmlrpc.php") {
return (pass);
}
if (req.url ~ "(tinymce|wp-mce|plugin.min.js)" ||
req.url ~ "preview=true" ||
req.url ~ "xmlrpc.php") {
return (pass);
}

            if (req.http.Cookie ~ "(wordpress_|comment_|wp-settings-)") {
                    return (pass);
            }

but did not work...

@arpan-jain
Copy link

arpan-jain commented May 4, 2018

Hey, @xoroz, I had the same problem with the WordPress visual editor, But adding the code snippet as the very first condition to evaluated in vcl_recv fixed it for me.

Pasting the same code snippet here for reference.

# Added later to fix visual editor issues
  if (req.url ~ "wp-(login|admin|comments-post.php|cron.php)" || req.url ~ "preview=true" || req.url ~ "xmlrpc.php") {
             return (pass);
   }

Hope it helps.

@Hetann
Copy link

Hetann commented Sep 7, 2018

Why are u removing the User-Agent header from the request?
Maybe i'm missing something but i don't get how that info could be duplicated and in addition to that removing the UA is a loss of control on many security things you can do based on the application that made the request, or at least the most of these.

@djeraseit
Copy link

Watch out for this booby trap!

set resp.http.Access-Control-Allow-Origin = "*";

@danielslyman
Copy link

Hey there,

could you point me in the right direction in order to make the WP backend work? I'm getting a redirect loop error.

@nnimis
Copy link

nnimis commented Feb 20, 2019

Hi! I would add this for certbot passthrough (for people using Let's Encrypt SSL/TLS):

    if (req.url ~ "^/\.well-known/acme-challenge/") {
        return (pass);
    }

@kenny-nt
Copy link

Is that VCL compatible with the latest wordpress and woocommerce?

@tschirmer
Copy link

tschirmer commented Apr 8, 2019

I've just debugged this for a site I was working on:

please replace the purge method there:

if (req.method == "PURGE") {
    if (!client.ip ~ purge) {
        return(synth(405,"Not allowed."));
    }
    return (purge);
}

with

if (req.method == "PURGE") {
    if (!client.ip ~ purge) {
        return(synth(405,"Not allowed."));
    }
    ban("req.url ~ /");
    return (purge);
}

The purge is done with

ban("req.url ~ /");

otherwise the purge isn't actually conducted

@che9992
Copy link

che9992 commented Apr 19, 2019

if you have a specific url to login
you have to find this and add the

	# unset cookies from backendresponse
	if (!(bereq.url ~ "(wp-login|wp-admin| your specific url to login here)"))  {
		set beresp.http.X-UnsetCookies = "TRUE";
    		unset beresp.http.set-cookie;
    		set beresp.ttl = 1h;
	}

@toby1kenobi
Copy link

I've just debugged this for a site I was working on:

please replace the purge method there:

if (req.method == "PURGE") {
    if (!client.ip ~ purge) {
        return(synth(405,"Not allowed."));
    }
    return (purge);
}

with

if (req.method == "PURGE") {
    if (!client.ip ~ purge) {
        return(synth(405,"Not allowed."));
    }
    ban("req.url ~ /");
    return (purge);
}

The purge is done with

ban("req.url ~ /");

otherwise the purge isn't actually conducted

Thanks for this, this worked for me too

@matthewjackowski
Copy link
Author

matthewjackowski commented Jun 7, 2019

Hi everyone! OP here, I had no idea so many people were using this as I have moved away from using Varnish for my Wordpress sites in favor of static publishing but am happy to maintain this config.
@tschirmer - Thanks for the update, I'll make that addition!

@matthewjackowski
Copy link
Author

Hey there,

could you point me in the right direction in order to make the WP backend work? I'm getting a redirect loop error.

Just to be clear, my original intent is to NOT to have the Wordpress BE work through Varnish. There is just so much to think about/can go wrong. Additionally I have security concerns around this because many Wordpress attacks target BE urls.
Instead I would recommend having a completely separate route setup for site admins, operationally it helps so much to have separated activity logs, IP security, etc.

@matthewjackowski
Copy link
Author

Watch out for this booby trap!

set resp.http.Access-Control-Allow-Origin = "*";

Certainly there is no intent for this to be a "booby trap". The intent is to cache external assets that CORS would otherwise block. Unfortunately I'm not sure how to make this less "scary". Maybe put a url condition on it like this: if (req.url ~ "/fonts/")
I'm open to any suggestions/feedback here.

@matthewjackowski
Copy link
Author

Why are u removing the User-Agent header from the request?
Maybe i'm missing something but i don't get how that info could be duplicated and in addition to that removing the UA is a loss of control on many security things you can do based on the application that made the request, or at least the most of these.

Great comment! This is definitely a rather "opinionated" setting.
I completely agree with your analysis, but my thinking goes along this line:
The goal is to let Varnish quickly give out the cached version to whoever asks for it, I shouldn't really care if it's a browser, a bot whatever (its so simple to impersonate browsers that I'm not sure how valuable it is from a security point of view)
Additionally I really really don't want Varnish keeping separate cached copies for every different User-Agent. I did try to get it to ignore it instead of dropping it altogether, but I couldn't make it work reliably (I kept seeing dups in the cache file).

@toby1kenobi
Copy link

Why are u removing the User-Agent header from the request?
Maybe i'm missing something but i don't get how that info could be duplicated and in addition to that removing the UA is a loss of control on many security things you can do based on the application that made the request, or at least the most of these.

Great comment! This is definitely a rather "opinionated" setting.
I completely agree with your analysis, but my thinking goes along this line:
The goal is to let Varnish quickly give out the cached version to whoever asks for it, I shouldn't really care if it's a browser, a bot whatever (its so simple to impersonate browsers that I'm not sure how valuable it is from a security point of view)
Additionally I really really don't want Varnish keeping separate cached copies for every different User-Agent. I did try to get it to ignore it instead of dropping it altogether, but I couldn't make it work reliably (I kept seeing dups in the cache file).

For me this line stops WP's visual editor loading

@matthewjackowski
Copy link
Author

Why are u removing the User-Agent header from the request?
Maybe i'm missing something but i don't get how that info could be duplicated and in addition to that removing the UA is a loss of control on many security things you can do based on the application that made the request, or at least the most of these.

Great comment! This is definitely a rather "opinionated" setting.
I completely agree with your analysis, but my thinking goes along this line:
The goal is to let Varnish quickly give out the cached version to whoever asks for it, I shouldn't really care if it's a browser, a bot whatever (its so simple to impersonate browsers that I'm not sure how valuable it is from a security point of view)
Additionally I really really don't want Varnish keeping separate cached copies for every different User-Agent. I did try to get it to ignore it instead of dropping it altogether, but I couldn't make it work reliably (I kept seeing dups in the cache file).

For me this line stops WP's visual editor loading
This should fix your issue: https://benjaminhorn.io/code/wordpress-visual-editor-not-visible-because-of-user-agent-sniffing/

@cultd3ad
Copy link

Hello Matthewjackowski,

this line creates an endless loop from https to http and back again. Ok i use varnish 5.2.1 , is the Code ok?

if ( std.port(server.ip) == 6080) {

		set req.http.x-redir = "https://" + req.http.host + req.url;
                return (synth(750, "Moved permanently"));
        }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment