October 25, 2017
8 min read time

Edge Logic: Securing Varnish with per-user JSON data

cdn-1.jpg

When using a traditional CDN or caching system, creating user-centric security and access policies can be a complex and performance challenging undertaking. Not only do you have to pull user data from a backend, but you then have to apply the security policies from that data onto the request. VCL is an excellent candidate for the latter problem of applying security policies to requests. This leaves the problem of how you get user data (JSON) into VCL? How do you do that on a user-by-user basis? And how do you do that in a way where you keep backend communication to an absolute minimum, or put another way, serve as much data from cache as possible?

Before we jump in, let's examine why you would want to do this. Since Varnish usually sits in front of the entire software stack, it has the ability to apply security and access policies more effectively, before requests even reach the backend*. Because Varnish is serving cached data, to maintain that high level of performance, we have to bring our security policies to Varnish. By doing so, we can distribute our Varnish nodes, i.e. content, closer in proximity to the users and still retain cache hit performance. This leads to lower response latencies and possibly greater user engagement and interaction. This kind of distributed positioning is called edge logic and embracing edge logic is key to achieving higher levels of performance, scale and security.

Varnish Plus Edge CDN

Varnish Plus Edge Network (aka a CDN)

Going back to how you accomplish this, Varnish Plus is the answer. Varnish Plus contains the tooling required to solve the problem of per-user security and access policies. We can use vmod_http to trigger out-of-band metadata fetches, vmod_edgestash and vmod_json to read and parse the JSON data, and vmod_kvstore to store user-specific metadata so it's accessible in VCL. We can also leverage VHA to distribute the JSON data across a cluster of Varnish nodes, minimizing backend round trips.

So in our example, we have a simple access control field, called type, and it contains the different user types: guest, user, premium, and admin. The idea here is that each of the different user types has different levels of access to content. That could be done with a different rendering of the content or by just blocking access to restricted content. This user type field is served from a backend API service as a JSON formatted payload.

 Users JSON NEW.png

User types with JSON

We also need to have some kind of client security built into our requests, and in this example it is done using plain HTTP cookies. VCL is only concerned about properly identifying users using an attribute which is consistent with our backend. This could be a session token or some kind of user identifier. We will not be performing any kind of actual authentication, we rely on the backend to do that. We are only concerned with correctly associating user identification in Varnish with backend user metadata.

Finally, in this example, we will simplify the VCL a bit and ignore some corner cases for the sake of simplicity and illustration. At the bottom of this post is the full VCL with all considerations taken.

We start our VCL with the following Varnish Plus VMOD imports:

vcl 4.0;

import cookieplus;
import edgestash;
import http;
import json;
import kvstore;

Next we define a kvstore (line 11) to hold our JSON metadata. We also add a default entry to capture non logged in sessions as guests (line 14).

sub vcl_init
{
  // Init a new kvstore in slot 0
  kvstore.init(0, 25000);

  // Add an entry for _guest
  kvstore.set(0, "_guest", {"
      {
        "type": "guest",
        "access": 0
      }
  "});
}

We identify the user via a session cookie (line 24) and pull out their associated JSON data from the above kvstore (line 27). If the user does not have any JSON associated with them, we call a function to retrieve the JSON from the backend (line 30).

sub vcl_recv
{
  // Get the auth-id from a session cookie, default to _guest
  set req.http.X-auth-id = cookieplus.get("sessid", "_guest");
  
  // Load the user's JSON data
  json.parse(kvstore.get(0, req.http.X-auth-id, ""));
  
  if (!json.is_valid() || json.get("type", "") == "") {
    call pull_user_json;
  }
  
  set req.http.X-auth-type = json.get("type", "guest");
  set req.http.X-auth-access = json.get("access", "0");
  
  // Continue on with access control using req.http.X-auth-*
}

At this point, we are free to implement our access policy in VCL using our user type (X-auth-type) or any other metadata that exists in the JSON payload. One way to do it is to add X-auth-type to the content hash (line 42):

sub vcl_hash
{
  // Add X-auth-type to the hash
  // This is better done with a Vary response
  hash_data(req.http.X-auth-type);
}

This is done just to illustrate how we can serve multiple forms of content depending on the user type. Adding this field to the hash is easy, but will have a few negative side effects like a lower hit rate, larger cache footprint, and harder invalidation. A better way to accomplish this would be to have the backend return a Vary header on X-auth-type. Having the backend dictate access control on the content itself via response headers is the best approach here. 

Here is the implementation for pull_user_json:

sub pull_user_json
{
  // Make an HTTP request back to Varnish
  http.init(0);
  http.req_copy_headers(0);
  http.req_set_method(0, "HEAD");
  http.req_set_url(0, http.varnish_url("/json/auth"));
  http.req_set_header(0, "Host", "api.company.com");
  http.req_send(0);
  http.resp_wait(0);

  // Load the JSON
  json.parse(kvstore.get(0, req.http.X-auth-id, ""));
}

If you notice in the above snippet, we make the JSON request but then load the JSON from the kvstore (line 56) and not the actual vmod_http response (line 53). This is because the above callback goes back through Varnish (line 50) and the JSON payload is put into the kvstore during that VCL traversal (line 78). Here is the VCL for this:

sub vcl_hash
{
  if (req.http.Host == "api.company.com") {
    hash_data(req.http.X-auth-id);
  }
}

sub vcl_backend_response
{
  // Stage the JSON in Edgestash
  if (bereq.http.Host == "api.company.com") {
    edgestash.index_json();
  }
}

sub vcl_deliver
{
  // Pull the JSON from Edgestash, save into kvstore with TTL
  if (req.http.Host == "api.company.com") {
    json.parse_resp_index();
    kvstore.set(0, req.http.X-auth-id, json.get(".", ""), 1h);
  }
}

The reason we go back through Varnish instead of pulling the JSON directly from vmod_http is so we can leverage VHA to replicate this data to other nodes. VHA will automatically replicate anything stored into cache. The above VCL will execute remotely on the replicated JSON, staging the data into the kvstore on all nodes. This means the user's access policy is ready and waiting in memory on all Varnish servers.

Finally, we can also trigger an async JSON fetch, which will update the JSON without impacting user performance. This can be done after a successful access and we can even pass additional metadata to the backend if we wish to log the access for auditing or metering reasons. This will also refresh the JSON data with a new TTL.

sub vcl_deliver
{
  // This request resulted in an authorized view
  // Notify the backend and get new authorization (async)
  if (req.http.X-auth-used == "true") {
    call pull_user_json_async;
  }
}

sub pull_user_json_async
{
  // Make an HTTP request back thru to Varnish
  http.init(1);
  http.req_copy_headers(1);
  http.req_set_method(1, "HEAD");
  http.req_set_url(1, http.varnish_url("/json/auth?content=" +
    req.http.X-auth-location));
  http.req_set_header(1, "Host", "api.company.com");
  http.req_send_and_finish(1);
}

The JSON response to this async update will push through VHA and land on all Varnish nodes. The full VCL for this example can be found here.

One of the things to highlight from this post is that the combination of VCL and VMODs gives Varnish Plus tremendous power when it comes to advanced caching policies and security. However, as with a lot of things in software, security and access control policies are never alike, especially across different organizations and companies. If you are interested in the ideas and concepts in this post, please do not hesitate in reaching out to us so we can provide more information into how Varnish Plus can be used to deliver and enhance your security and access control policies. 

* Access control and security in Varnish should coexist with and not substitute for actual backend security.

Learn more - contact us