GadElKareem

Varnish with secure AWS S3 bucket as backend

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);
}