Skip to content

Instantly share code, notes, and snippets.

@wuxmedia
Last active October 4, 2019 19:39
Show Gist options
  • Save wuxmedia/c9fb997f3593fcde33aef68665722ea0 to your computer and use it in GitHub Desktop.
Save wuxmedia/c9fb997f3593fcde33aef68665722ea0 to your computer and use it in GitHub Desktop.
Generic-ish varnish 4 config, with wordpress, cms and other tweaks. courtesy of JT
# Define which version of VCL we're running:
vcl 4; # Import used modules:
import std;
# Default backend definition. Set this to point to your content server. # Varnish can support basic loadbalancing & healthchecks
backend default { .host = "127.0.0.1";
.port = "8080"; .connect_timeout = 60s;
.first_byte_timeout = 90s; .between_bytes_timeout = 60s;
# Define basic healthchecking-probe. # Backend must respond to 3 http GETs within the last 5 for GET /licence.txt with 200 OK to be considered 'healthy'
# The URL we're probing for assumes a WordPress install is available on the backend. # .probe = {
# .url = "/license.txt"; # .timeout = 3s;
# .interval = 5s; # .window = 5;
# .threshold = 3; # }
# Set this if you want to throttle connections to the backend webserver #.max_connections = 40;
}
# List of backend webservers allowed to send PURGE requests on behalf of the site.
# For a modern cluster, you should put the private LAN prod range here, e.g. # "10.10.10.0/24";
acl purgeallow { "127.0.0.1";
}
sub vcl_recv { # Happens before we check if we have this in cache already.
# # Typically you clean up the request here, removing cookies you don't need,
# rewriting the request, etc.
#################
# Required code
#
# The following code is required on every request. It normalizes some data, prepares backend signal, and does some
# very simple defensive action. Don't add *anything* here for normal alterations
#
#########
# Block simple slowloris attack
if (req.url == "400") { return (synth(403, "Permission Denied.")); }
# Block various overt SQLi attacks
if ((req.url ~ "(?i).+SELECT.+FROM" && !req.url ~ "\?cs-from\=") ||
req.url ~ "(?i).+UNION\s+SELECT" ||
req.url ~ "UNION.*SELECT" ||
req.url ~ "(?i).+INSERT.+INTO" ||
req.url ~ "(?i).+DELETE.+FROM" ||
req.url ~ "(?i).+DROP.+TABLE" ||
req.url ~ "(?i).+DROP.+DATABASE" ) {
return(synth(404, "File not found."));
}
# Block overt php remote code execution via URL.
if (req.url ~ "eval\(" ) {
#req.url ~ "(<|\%3C)?\?(php)?.*(php)?\?(>|\%3E)?" || # Was causing problems
return(synth(404, "File not found."));
}
# Ensure HTTPS header signal is set. X-HTTPS is used as a signal to hash function, to differentiate
# objects cached by visitors served via HTTPS
if (req.http.X-Forwarded-Proto == "https") { set req.http.X-HTTPS = "on"; }
if (!req.http.X-HTTPS) { set req.http.X-HTTPS = "off"; }
# Perfom basic device detection (used to differentiate cache based on mobile/pc/smartphone. Calls predefined function
call identify_device;
# Handle incoming PURGE requests. Intergrates with WP plugin "Varnish HTTP Purge", or any PURGE-sending plugin
if (req.method == "PURGE" && client.ip ~purgeallow ) {
if (req.http.X-Purge-Method) {
if (req.http.X-Purge-Method ~ "(?i)regex") { call purge_regex; }
elseif (req.http.X-Purge-Method ~ "(?i)exact") { call purge_exact; }
else { call purge_page; }
}
return(synth(200, "Ban added"));
}
# You can also trigger this by sending a manual CURL command against the active varnish server;
# curl -X PURGE -H 'Host: example.com' -H 'X-Purge-Method: regex' http://10.10.10.1:6081/\.\*
# WARNING: In a multi-varnish environment where 2 varnish servers are hot, you'll need to fire the curl twice,
# once for each cache server.
# Detect/provide end-users' ip. If end-user routed through CloudFlare, use CloudFlare's header
if ( req.http.CF-Connecting-IP ) {
set req.http.X-Forwarded-For = req.http.CF-Connecting-IP;
set req.http.X-Real-IP = req.http.CF-Connecting-IP;
}
# Otherwise, use the IP address previously set by Nginx/CloudFront
elseif ( req.http.X-Forwarded-Proto ) {
set req.http.X-Forwarded-For = req.http.X-Real-IP;
}
# We're getting connected to directly.
else {
set req.http.X-Forwarded-For = client.ip;
set req.http.X-Real-IP = client.ip;
}
set req.http.grace = "none";
# Normalize Accept-Encoding to increase cache hitrate
if (req.http.Accept-Encoding) {
if (req.http.Accept-Encoding ~ "gzip") { set req.http.Accept-Encoding = "gzip"; }
elsif (req.http.Accept-Encoding ~"deflate") { set req.http.Accept-Encoding = "deflate"; }
else { unset req.http.Accept-Encoding; }
}
# Stop Vary header from indicating anything but Accept-Encoding; no user-agent for example
# Some caching plugins/guides errantly recommend adding User-Agent to accept-encoding. This breaks varnish.
if (req.http.Vary) {
if (req.http.Vary ~ "Accept-Encoding") { set req.http.Vary = "Accept-Encoding"; }
else { unset req.http.Vary; }
}
###################
#
# End Required Code. Put overrides below here
#
#################################
# Don't cache ajax request
if (req.url ~ "admin-ajax.php") { return (pass); }
# dont cache work.
if (req.url ~ "work/htdocs") { return (pass); }
# Basic blocking of some common malicious requests
if (req.url ~ "(\.\..+)+" || req.url ~ "\.\.%2F.+$" || req.url ~ "self%2Fenviron" ) {
set req.url = regsub(req.url, "\?.*$", "");
}
# Don't cache dynamic content or AJAX
if (req.method == "POST") { return (pass); }
if (req.url ~ "(xmlrpc.php|wlmanifest.xml)") { return(pass); }
# If the user is logged in, don't cache
if (req.http.Cookie ~ "wordpress_logged_in") { return(pass); }
# If the user is accessing wp-admin, don't cache
if (req.url ~ "wp-admin") { return(pass); }
# Don't cache ajax requests, logins, registrations, or comment posts
if(req.http.X-Requested-With == "XMLHttpRequest" || req.url ~ "(wp-comments-post.php|wp-login.php|register.php)") {
return (pass);
}
# Forcibly cache statically-uploaded/stored media
if (req.url ~ "/wp-content/uploads/") { unset req.http.cookie; return (hash); }
# Force all image/javascript/static-media to come from cache
# This won't affect theme-backend.css.php, as it ends in .php, not .css
if (req.url ~ "\.(css|js|ico|png|gif|svg|jpg|swf|jpeg|zip)$" || req.url ~ ".*(minify).*\.(css|js).*") {
unset req.http.cookie;
return (hash);
}
# Cache requests for various versioned js/css files
if (req.url ~ "\.js?ver") { unset req.http.cookie; return (hash); }
if (req.url ~ "\.css?ver") { unset req.http.cookie; return (hash); }
# If you've commented, don't cache pages, but do cache static media (above)
if (req.http.Cookie ~ "comment_author_") { return(pass); }
# Remove Google Analytics and Piwik cookies everywhere
if (req.http.Cookie) {
set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(__[a-z]+|has_js)=[^;]*", "");
set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(_pk_(ses|id)[\.a-z0-9]*)=[^;]*", "");
}
# Remove the cookie when it's empty
if (req.http.Cookie == "") {
unset req.http.Cookie;
}
# Alter the variables in this stanza to delete *all* cookies other than the named cookies, 'cookie1,cookie2'.
#if (req.http.Cookie) {
# set req.http.Cookie = ";" + req.http.Cookie;
# set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";");
# set req.http.Cookie = regsuball(req.http.Cookie, ";(cookie1|cookie2)=", "; \1=");
# set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", "");
# set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", "");
# if (req.http.Cookie == "") { unset req.http.Cookie; }
#}
# To cache incredibly aggressively, delete all remaining cookies;
# unset req.http.Cookie;
# At this point don't cache if they have a cookie.
if (req.http.Cookie) { return(pass); }
# At this point, attempt to return anything remaining from cache
return(hash);
}
sub vcl_backend_response {
# Happens after we have read the response headers from the backend.
#
# Here you clean the response headers
# Provide settings for use by ban lurker/bans
set beresp.http.x-url = bereq.url;
set beresp.http.x-host = bereq.http.host;
# Set grace; provides for protection against stampeeding horde problem
if (beresp.ttl > 0s) {
set beresp.grace = 1h;
}
# If you're in the process of logging in, activating a basic PHP session, *don't* cache
if (beresp.http.Set-Cookie ~ "(wordpress_|PHPSESS)") {
set beresp.uncacheable = true;
return (deliver);
}
# If they're reporting login issues, or issues with redirect, uncomment the following to stop caching redirects
#if (beresp.status == 302 || beresp.status == 301) {
# set beresp.uncacheable = true;
# return (deliver);
#}
# Mostly dynamic areas. admin-ajax *can* be used in a form that is cacheable, but no-one does that without us telling them to
if (bereq.url ~ "(wp-admin|wp-login.php|admin-ajax.php|wp-cron.php)" ||
bereq.http.cookie ~ "logged_in" ||
beresp.status == 403 ) {
set beresp.ttl = 120s;
set beresp.uncacheable = true;
return (deliver);
}
# Force redirects to be cached
#if (beresp.status == 301 || beresp.status == 302) {
# unset beresp.http.Set-Cookie;
# unset beresp.http.Cookie;
# set beresp.ttl = 2m;
# return(deliver);
#}
#wlmanifest.xml is used for windows live writer to write to WordPress. Remove it if they're unlikely to use Windows Live Writer.
# Don't cache ajax requests or fully dynamic tools
if ( bereq.http.X-Requested-With == "XMLHttpRequest" ||
bereq.url ~ "(wp-comments-post.php|wp-login.php|register.php|xmlrpc.php|wlmanifest.xml)") {
# set beresp.ttl = 120s;
set beresp.uncacheable = true;
return (deliver);
}
# Item requested with POST, uncacheable response
if (bereq.method == "POST") {
set beresp.uncacheable = true;
return (deliver);
}
####################
# Cache TTLs
# WARNING: We set the browser to keep static media items for 7 days, the Google recommended default. This will be too long
# For development or staging work, make sure to bypass cache for those.
# Override cache times for statically-uploaded media
if (bereq.url ~ "/wp-content/uploads/") {
unset beresp.http.Set-Cookie; # dis-allow cookie setting for static media
unset beresp.http.expires;
unset beresp.http.pragma;
# Set how long the client should keep the item by default
set beresp.http.cache-control = "max-age=604800";
set beresp.ttl = 4h;
return (deliver);
}
# Set cache time for static media. Will not catch theme-backend.css.php.
if ((bereq.method == "GET" && bereq.url ~ "\.(css|js|xml|gif|jpg|jpeg|swf|svg|png|zip|ico|img|wmf|txt|flv)$") ||
bereq.url ~ "\.(minify).*.(css|js).*" ||
bereq.url ~ "\.(css|js|svg)\?ver") {
unset beresp.http.Set-Cookie; # dis-allow cookie setting
unset beresp.http.expires;
unset beresp.http.pragma;
# Set how long the client should keep the item by default
set beresp.http.cache-control = "max-age=604800";
set beresp.ttl = 4h;
return (deliver);
}
# If it's a not found, or a server error, cache it for 1 minute
if (beresp.status == 404 || beresp.status >= 500) { set beresp.ttl = 1m; }
# If you're logged in to wordpress, don't cache.
if (bereq.http.Cookie ~ "wordpress_") {
set beresp.ttl = 120s;
set beresp.uncacheable = true;
return (deliver);
}
# if you're still here, override to default minimum ttl for object storage
if (beresp.ttl < 30s) {
set beresp.ttl=900s;
}
# Reset any remaining outbound HTTP headers
unset beresp.http.expires;
unset beresp.http.pragma;
# Set how long the client should keep the item by default
set beresp.http.cache-control = "max-age=600";
# Override browsers to keep styling and dynamics to Google recommended 1 week
if (bereq.url ~ ".minify.*\.(css|js).*") { set beresp.http.cache-control = "max-age=604800"; }
if (bereq.url ~ ".*(css|js)$") { set beresp.http.cache-control = "max-age=604800"; }
# Override browsers to keep static media for 1 week
if (bereq.url ~ ".(xml|gif|jpg|jpeg|swf|png|zip|ico|img|wmf|txt)$") {
set beresp.http.cache-control = "max-age=604800";
}
# Block setting a cookie we've not handled previously.
unset beresp.http.Set-Cookie;
# Deliver data to visitor
return(deliver);
}
# HTTPS Handling
sub vcl_hash {
# HTTPs differentiation
if ( req.http.X-HTTPS == "on" ) { hash_data("ssl"); }
hash_data(req.url);
if (req.http.host) { hash_data(req.http.host); }
else { hash_data(server.ip); }
# Device detection
hash_data(req.http.X-Device);
return (lookup);
}
sub vcl_deliver {
# Happens when we have all the pieces we need, and are about to send the
# response to the client.
#
# You can do accounting or modifying the final object here.
# Define grace
set resp.http.grace = req.http.grace;
# Set hit indicator headers, for cache tuning
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
} else {
set resp.http.X-Cache = "MISS";
}
# Remove the temp headers used for ban-luker friendly bans
unset resp.http.x-url;
unset resp.http.x-host;
# Optionally enforce HSTS
#set resp.http.Strict-Transport-Security = "max-age=31536000; includeSubDomains";
}
sub vcl_hit {
if (obj.ttl >= 0s) {
# normal hit
return (deliver);
}
# We have no fresh objects. Check stale ones.
if (std.healthy(req.backend_hint)) {
# Backend is healthy. Limit age to 10s.
if (obj.ttl + 10s > 0s) {
return (deliver);
} else {
# No candidate for grace. Fetch a fresh object.
return(miss);
}
} else {
# backend is sick - use full grace
if (obj.ttl + obj.grace > 0s) {
return (deliver);
} else {
# no graced object.
return (miss);
}
}
}
sub vcl_backend_error {
# If we're about to return an error, and we've not retried three times, retry
if (bereq.retries < 4) {
return (retry);
}
}
# Routine to identify and classify a device based on User-Agent
sub identify_device {
# Default to classification as a PC
set req.http.X-Device = "pc";
if (req.http.User-Agent ~ "iPad" ) {
# The User-Agent indicates it's a iPad - so classify as a tablet
set req.http.X-Device = "mobile-tablet";
}
elsif (req.http.User-Agent ~ "iP(hone|od)" || req.http.User-Agent ~ "Android" ) {
# The User-Agent indicates it's a iPhone, iPod or Android - so let's classify as a touch/smart phone
set req.http.X-Device = "mobile-smart";
}
elsif (req.http.User-Agent ~ "SymbianOS" || req.http.User-Agent ~ "^BlackBerry" || req.http.User-Agent ~ "^SonyEricsson" || req.http.User-Agent ~ "^Nokia" || req.http.User-Agent ~ "^SAMSUNG" || req.http.User-Agent ~ "^LG") {
# The User-Agent indicates that it is some other mobile devices, so let's classify it as such.
set req.http.X-Device = "mobile-other";
}
}
# Define ways of purging based on regex.
# Treat the request URL as a regular expression.
sub purge_regex {
ban("obj.http.x-url ~ " + req.url + " && obj.http.x-host == " + regsub(req.http.host, "(.*):8080", "\1"));
#ban("req.url ~ " + req.url + " && req.http.host == " + regsub(req.http.host, "(.*):81", "\1"));
}
# Exact purging
# Use the exact request URL (including any query params)
sub purge_exact {
ban("obj.http.x-url == " + req.url + " && obj.http.x-host == " + regsub(req.http.host, "(.*):8080", "\1"));
#ban("req.url == " + req.url + " && req.http.host == " + regsub(req.http.host, "(.*):81", "\1"));
}
# Page purging (default)
# Use the exact request URL, but ignore any query params
sub purge_page {
set req.url = regsub(req.url, "\?.*$", "");
ban("obj.http.x-url == " + req.url + " && obj.http.x-host == " + regsub(req.http.host, "(.*):8080", "\1"));
#ban("req.url == " + req.url + " && req.http.host == " + regsub(req.http.host, "(.*):81", "\1"));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment