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"));
}
}
@matthewjackowski
Copy link
Author

@makmour I reduced the logic on that condition and also set an X-param to be able to see when it triggers. Seems to be ok for me on my local docker setup. Keep in mind that to enable https you have todo alot of tomfoolery in Wordpress too.

@oralunal
Copy link

It works great, thank you sir

@marco41
Copy link

marco41 commented Jul 14, 2017

Hi, it works great with https. But not all my sites are https. I can not log in to my sites with http. How do I add a code for this to help you?

@danhere
Copy link

danhere commented Sep 4, 2017

Thanks for this! There's a surprising lack of good/up to date configs out there.

I did run into small issue though: Have you noticed that Wordpress' Visual Editor doesn't load with this config? I disabled Varnish all together and the Visual Editor comes right back. I can't seem to figure it out. Any ideas?

@marco41
Copy link

marco41 commented Sep 24, 2017

Hello,
Normally : http://prntscr.com/gp860w
But it looks like this. : http://prntscr.com/gp85ws

How can my overcome this problem?

@dockrize
Copy link

@marco41 @danhere
Check the HTTP requests at your browser (developer console). I saw this problem when doing HTTP requests to a HTTPS port.

@marco41
Copy link

marco41 commented Sep 28, 2017

@dockrize what should I do. Please tell me in detail.

@marco41
Copy link

marco41 commented Oct 8, 2017

@matthewjackowski If you know, please answer me?

@informdev
Copy link

So far this is excellent. I keep reading about issues people have ran into with CloudFlare and Varnish -- I'm still new to this stuff but so far it SEEMS to be working? Any thoughts, ideas? fixes if an issue?

@informdev
Copy link

informdev commented Dec 13, 2017

To everyone else @Marco31 @danhere -- you can fix your editor issues by bypassing caching if logged in.
Under sub vcl_recv { add the following:
if (req.url ~ "wp-(login|admin|comments-post.php|cron.php)" ||
req.url ~ "preview=true" ||
req.url ~ "xmlrpc.php") {
return (pass);
}

@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