A quick-and-easy CloudFront mock for locally testing signed URLs
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!