I’ve been working with one of my clients the last month on migrating his iron- based architecture to a cloud-based provider. In this transition, we are going from one or two physical servers to multiple cloud servers and separating out parts to better scale each individual service.
As part of this, we are moving a significant library of images and videos away from being served off the same web server as the application and to a server tuned to handle requests for these static assets. The problem is that a lot of these assets (the videos and full-size images) are for paying members only. We need a way to secure those resources across physical servers.
My first inclination was to use the secure URL functionality in nginx. But this is sub-optimal for a few reasons. The big one is that it generates a unique URL for each requests, which completely negates any browser caching for subsequent requests. It also requires you to either generate all the URLs at page time, or use redirects.
Secure URLs work great if you have to make a secure requests across different
domains. But, from the browser’s point of view, we will be under the same
domain. Instead of www.example.com
the assets will be stored under
assets.example.com
. But we’re still under the example.com
domain. So that
gives us another option: cookies. We can set a some cookies on login, and use
nginx and Lua to verify the cookie signature on the other server before serving
a static asset.
The first thing we need to do is set up a simple algorithm that determines what our bounds are for serving an asset. An example would be three pieces of information:
-
A user identifier.
-
An expiration time.
-
A secret token that is shared between both the application server and the asset server.
So you might do something like this (in pseudocode):
var secret_token = "your secret token here";
var expire = time() + 3600; // Expire in 1 hour.
var asset_hash = md5(secret_token + user_id + expiration_time);
setcookie("user_id", user_id, expire, "/", "example.com");
setcookie("expire", expire, expire, "/", "example.com");
setcookie("asset_hash", asset_hash, expire, "/", "example.com");
So what we’re doing here is setting three cookies: the user_id, the expiration
timestamp, and the asset hash, which is an md5
hash of the three pieces of
information, only two of which are also set as cookies. The third piece of
information, the secret token, is only known on the servers.
Quick note here, I’m using md5
because it’s fast, but you can use any hashing
algorithm you’d like as long as you do it the same in both places. md5
is
insecure, but this method should be sufficient to stop all but the most
determined adversary who would try to take the hash against a rainbow table.
So, now we have these cookies set on our application server, we need to jump over to our asset server and make some changes. This is where Lua comes into play.
nginx is a very stripped down HTTP server that is very fast. But, nicely, it provides you with the ability to implement some things within nginx using Lua. And we can use this functionality to verify our cookies before serving an asset.
Notice what we’re reading here. ngx
is provided by the nginx Lua machine, and
cookies are available using ngx.var.cookie_<cookie_name>
. So we first check
that all the cookies are present, then we check if the hash is valid, then we
check if the expire time has passed. If any of these conditions fail, we exit by
setting the proper responses on the ngx
object.
So the last thing to do is to tell nginx to run this script before serving any
protected assets. We do that by using the access_by_lua_file
nginx directive:
Now, if the user edits any of those cookies using the browser console, the computed hash won’t match and nginx won’t serve the asset. And they can’t recompute the hash on their own because they lack the secret token. But from the browser’s perspective, it’s just a standard HTTP request with the appropriate caching headers. So it will happily cache the asset for the specified period of time, thus reducing subsequent requests.
And, as an added bonus, the URL cannot be shared at all with non-members. With “secure URLs” that use a hash as part of the URL, for as long as that URL is valid anyone can use it. This method requres the approprate cookies on the request and anyone wanting to share the URL would have to have either create a page that sets the appropriate cookies before redirecting a user, or have the user manually enter the cookies.