Serving static contents from S3 is common, but using Varnish in front is a bit tricky. Especially if you want to keep the bucket secure and only serve from Varnish, here is a simple Varnish file to solve this problem.
First secure your bucket via IP policy:
{
"Version": "2012-10-17",
"Id": "S3PolicyId1",
"Statement": [
{
"Sid": "IPAllow",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::example.bucket/*",
"Condition": {
"IpAddress": {
"aws:SourceIp": [
"5.6.7.8/32" //varnish ip
]
}
}
},
{
"Sid": "Explicit deny to ensure requests are allowed only from specific referer.",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::example.bucket/*",
"Condition": {
"StringNotLike": {
"aws:Referer": [
"https://example.com/*"
]
}
}
}
]
}
The Varnish file:
vcl 4.0;
acl cloudflare {
"103.21.244.0"/22;
"103.22.200.0"/22;
"103.31.4.0"/22;
"104.16.0.0"/12;
"108.162.192.0"/18;
"131.0.72.0"/22;
"141.101.64.0"/18;
"162.158.0.0"/15;
"172.64.0.0"/13;
"173.245.48.0"/20;
"188.114.96.0"/20;
"190.93.240.0"/20;
"197.234.240.0"/22;
"198.41.128.0"/17;
"2400:cb00::"/32;
"2405:8100::"/32;
"2405:b500::"/32;
"2606:4700::"/32;
"2803:f800::"/32;
"2c0f:f248::"/32;
"2a06:98c0::"/29;
}
backend example {
.host = "1.2.3.4";
.port = "80";
}
backend s3_bucket {
.host = "s3.bucket.us-east-1.amazonaws.com";
.port = "80";
}
sub vcl_recv {
if (req.http.host ~ "bucket.example.com") {
if(req.method != "GET" && req.method != "HEAD") {
return(synth(900)); //method not allowed
}
if(req.http.referer !~ "^https://example\.com"){
return(synth(800)); //hotlinking
}
//clean up for S3
unset req.http.cookie;
unset req.http.cache-control;
unset req.http.pragma;
unset req.http.expires;
unset req.http.etag;
unset req.http.X-Forwarded-For;
unset req.http.CF-Connecting-IP;
//set correct backend
set req.backend_hint = s3_bucket;
set req.http.host = "s3.bucket.us-east-1.amazonaws.com";
//done with this request, forward it to backend
return(hash);
}
//normal request...
//pick user IP from Cloudflare request
if (client.ip ~ cloudflare && req.http.CF-Connecting-IP) {
set req.http.X-Forwarded-For = req.http.CF-Connecting-IP;
}
if (req.http.host ~ "^(.*\.)?example\.com") {
set req.backend_hint = example;
}
if (req.url ~ "\.(png|gif|jpg|ico|txt|swf|css|js)$") {
unset req.http.Cookie;
}
return(hash);
}
sub vcl_backend_response {
// strip the cookie before the image is inserted into cache
if (bereq.url ~ "\.(png|gif|jpg|ico|txt|swf|css|js)$") {
unset beresp.http.Set-Cookie;
set beresp.ttl = 1d;
}
}
sub vcl_deliver {
//clean up for delivery
unset resp.http.Via;
unset resp.http.X-Whatever;
unset resp.http.X-Powered-By;
unset resp.http.X-Varnish;
unset resp.http.Age;
unset resp.http.Server;
//s3 stuff
unset resp.http.X-Amz-Id-2;
unset resp.http.X-Amz-Meta-Group;
unset resp.http.X-Amz-Meta-Owner;
unset resp.http.X-Amz-Meta-Permissions;
unset resp.http.X-Amz-Request-Id;
if (req.http.host ~ "s3.bucket.us-east-1.amazonaws.com") {
if(resp.status == 404){
return(synth(700)); //not found
//convert all other errors from S3 into "520 Unknown Error"
}elseif(resp.status != 200){
return(synth(600));
}
}
}
sub vcl_backend_error {
set beresp.http.Content-Type = "text/html; charset=utf-8";
synthetic( {"Error! Please retry later.."} );
return(deliver);
}
sub vcl_synth {
if (resp.status > 550) {
set resp.http.Content-Type = "text/plain; charset=utf-8";
if (resp.status == 900) {
set resp.status = 405;
synthetic({"Method Not Allowed"});
}elseif (resp.status == 800) {
set resp.status = 403;
synthetic({"Hotlinking Not Allowed"});
}elseif (resp.status == 700) {
set resp.status = 404;
synthetic({"Not Found"});
}elseif (resp.status == 600) {
set resp.status = 520;
synthetic({"Unknown Error"});
}
}
return(deliver);
}