A quick-and-easy CloudFront mock for locally testing signed URLs

3 minute read

Security is a major concern in building a dating app. Part of that is ensuring sensitive customer data, including photos, are only shared with users with proper access. (That restriction might just be “registered users,” but that still requires access checking, e.g., when users block one another.) Meet Kinksters will also launch with a unique feature allowing your photos to be blurred until you show interest in or match with someone, further complicating our access regime.

To protect user photos, and improve our performance, we leverage Amazon CloudFront’s signed URL feature. This allows us to cache user photos in Amazon POPs closer to clients as well as limit access based on signed query parameters. But how to test this locally?

CloudFront is, at its core, just a caching reverse proxy, so there’s no real “official” type of self-hosted version; you’d just use Nginx. That is exactly what I did when I started to test our photo service locally. However, as I got deeper into implementing the client side, I realized that while Nginx was a good caching mock, it lacked an easy way for me to test whether a signed URL was expired. I’ve generally set my various expiration times (e.g., bearer token expiration) to abnormally low values in local development - just a few minutes - to test this functionality more frequently and stress-test code paths that might only run every few hours or days, otherwise.

It turns out Nginx’s conventional build does not provide a runtime that can compare a query parameter’s value against the current time. This is possible however with Openresty, a superset of Nginx with all sorts of Lua-powered scripting enabled by default. I didn’t really find anything good/current on implementing this, hence this blog post.

It’s worth mentioning that nothing below actually validates the signature, as CloudFront would do. Since an Expires query parameter is an integral part of the signed URL, however, I figure checking this is more than good enough to achieve my aim of catching expired signatures. I don’t need to validate the operation of my signing library overall.

The key is an Nginx configuration that uses a Lua block to do a simple comparison against the expiration time.

resolver 127.0.0.11;

proxy_cache_path /tmp/nginxcache levels=1:2 keys_zone=STATIC:10m inactive=24h max_size=1g;

server {
    listen 80;
    access_log on;
    location / {
        access_by_lua_block {
            local args, err = ngx.req.get_uri_args()
            if (tonumber(args.Expires) < os.time()) then
                ngx.exit(ngx.HTTP_FORBIDDEN)
            end
        }
        proxy_set_header Host $host;
        proxy_set_header x-psk ${PSK};
        proxy_cache STATIC;
        proxy_cache_valid 200 1d;
        add_header X-Cache-Date $upstream_http_date;
        set $upstream ${UPSTREAM};
        proxy_pass http://$upstream:8080;
    }
}

You’ll notice this configuration is templated. This is in order to dynamically set the hostname of the upstream (e.g., a service name on the same docker-compose project’s network) and a value for a pre-shared key header, which our Glide microservice uses to ensure requests are allowed only from the reverse proxy.

The official Nginx image supports templated configuration out of the box, but while Openresty includes envsubst, it doesn’t run a script (like library/nginx does) to automagically transliterate the variables.

Plus, turns out this little helper script is more-or-less necessary, since in the example above, set $upstream ${UPSTREAM} would try and replace both variables, even though the first one needs to get passed through as-is, as valid Nginx config text. So the script collects the defined environment variables before running envsubst.

One little caveat: I had to ever-so-slightly tweak the Nginx script though, since it redirects output to handle 3 which is undefined. Just remove the redirection and allow the message to print to STDOUT and you’ll be fine. Include a copy of this script in your Docker entrypoint before starting Openresty.

Happy mocking!

Updated: