1

I have an application deployed on AWS Elastic Beanstalk, I added some simple licensing to stop abuse of the api, the user has to pass a licensekey as a field

i.e

search.myapi.com/?license=4ca53b04&query=fred

If this is not valid then the request is rejected.

However until the monthly updates the above query will always return the same data, therefore I now point search.myapi.com to an AWS CloudFront distribution, then only if query is not cached does it go to actual server as

direct.myapi.com/?license=4ca53b04&query=fred

However the problem is that if two users make the same query they wont be deemed the same by Cloudfront because the license parameter is different. So the Cloudfront caching is only working at a per user level which is of no use.

What I want to do is have CloudFront ignore the license parameters for caching but not the other parameters. I dont mind too much if that means user could access CloudFront with invalid license as long as they cant make successful query to server (since CloudFront calls are cheap but server calls are expensive, both in terms of cpu and monetary cost)

Perhaps what I need is something in front of CloudFront that does the license check and then strips out the license parameter but I don't know what that would be ?

Paul Taylor
  • 13,411
  • 42
  • 184
  • 351

1 Answers1

1

Two possible come to mind.

The first solution feels like a hack, but would prevent unlicensed users from successfully fetching uncached query responses. If the response is cached, it would leak out, but at no cost in terms of origin server resources.

If the content is not sensitive, and you're only trying to avoid petty theft/annoyance, this might be viable.

For query parameters, CloudFront allows you to forward all, cache on whitelist.

So, whitelist query (and any other necessary fields) but not license.

Results for a given query:

  • valid license, cache miss: request goes to origin, origin returns response, response stored in cache
  • valid license, cache hit: response served from cache
  • invalid license, cache hit: response served from cache
  • invalid license, cache miss: response goes to origin, origin returns error, error stored in cache.

Oops. The last condition is problematic, because authorized users will receive the cached error if the make the same query.

But we can fix this, as long as the origin returns an HTTP error for an invalid request, such as 403 Forbidden.

As I explained in Amazon CloudFront Latency, CloudFront caches responses with HTTP errors using different timers (not min/default/max-ttl), with a default of t minutes. This value can be set to 0 (or other values) for each of several individual HTTP status codes, like 403. So, for the error code your origin returns, set the Error Caching Minimum TTL to 0 seconds.

At this point, the problematic condition of caching error responses and playing them back to authorized clients has been solved.

The second option seems like a better idea, overall, but would require more sophistication and probably cost slightly more.

CloudFront has a feature that connects it with AWS Lambda, called Lambda@Edge. This allows you to analyze and manipulate requests and responses using simple Javascript scripts that are run at specific trigger points in the CloudFront signal flow.

  • Viewer Request runs for each request, before the cache is checked. It can allow the request to continue into CloudFront, or it can stop processing and generate a reaponse directly back to the viewer. Generated responses here are not stored in the cache.
  • Origin Request runs after the cache is checked, only for cache misses, before the request goes to the origin. If this trigger generates a response, the response is stored in the cache and the origin is not contacted.
  • Origin Response runs after the origin response arrives, only for cache misses, and before the response goes onto the cache. If this trigger modifies the response, the modified response stored in the cache.
  • Viewer Response runs immediately before the response is returned to the viewer, for both cache misses and cache hits. If this trigger modifies the response, the modified response is not cached.

From this, you can see how this might be useful.

A Viewer Request trigger could check each request for a valid license key, and reject those without. For this, it would need access to a way to validate the license keys.

If your client base is very small or rarely changes, the list of keys could be embedded in the trigger code itself.

Otherwise, it needs to validate the key, which could be done by sending a request to the origin server from within the trigger code (the runtime environment allows your code to make outbound requests and receive responses via the Internet) or by doing a lookup in a hosted database such as DynamoDB.

Lambda@Edge triggers run in Lambda containers, and depending on traffic load, observations suggest that it is very likely that subsequent requests reaching the same edge location will be handled by the same container. Each container only handles one request at a time, but the container becomes available for the next request as soon as control is returned to CloudFront. As a consequence of this, you can cache the results in memory in a global data structure inside each container, significantly reducing the number of times you need to ascertain whether a license key is valid. The function either allows CloudFront to continue processing as normal, or actively rejects the invalid key by generating its own response. A single trigger will cost you a little under $1 per million requests that it handles.

This solution prevents missing or unauthorized license keys from actually checking the cache or making query requests to the origin. As before, you would want to customize the query string whitelist in the CloudFront cache behavior settings to eliminate license from the whitelist, and change the error caching minimum TTL to ensure that errors are not cached, even though these errors should never occur.

Michael - sqlbot
  • 169,571
  • 25
  • 353
  • 427
  • Thankyou that seems to make sense (although I always struggle with Javascript), since even for option 2 you recommend using the query string whitelist in the CloudFront cache I will implement option 1 first quickly and then extend to 2. But I have a follow up question that I forgot about that I also want to implement rate limiting so even if have license prevents customer abusing the service, would you recommend extending use of Lamba@Edge for this as well ? – Paul Taylor Jun 14 '18 at 12:39
  • What kind of granularity do you need on your rate limiting? Lambda@Edge is not a perfect solution, because it's distributed and you would not have a genuine global request rate counter... but it could still be a valid solution, depending on your needs. – Michael - sqlbot Jun 14 '18 at 20:44
  • Not sure what you mean ? – Paul Taylor Jun 14 '18 at 21:25
  • Are we talking about 1000 requests per minute, or 5 requests per hour, or ...? Granularity: do you need fine-grained control (a brick wall throttle), or coarse (only blocking requests rates that significantly exceed a defined threshold)? – Michael - sqlbot Jun 14 '18 at 22:47
  • Nearer to 1000 per minute, currently about 100 per minute. My plan was this would be done based on ipaddress rather than license, I was thinking the logic could be done on origin server and then Lambda@Edge could call that but ideally would prefer the server did not get called in first place. – Paul Taylor Jun 15 '18 at 09:45
  • Sorry, I didn't ask the previous question clearly. I was thinking about the request rate *per user* rather than the overall rate, but it seems like you might be referring to overall. If you want to limit individual IP addresses to a value that is 2000 requests per 5 minutes *or higher* (the smallest maximum request count is 2000), then [AWS WAF](https://docs.aws.amazon.com/waf/latest/developerguide/how-aws-waf-works.html) can do that, and it works with CloudFront. But I suspect L@E can also get us mostly there, and we're already invoking a trigger, so let's see about how to implement that. – Michael - sqlbot Jun 15 '18 at 10:12
  • I mean per user, and the rate would be set at about 120 requests per minutes/ 2 requests per second. Funnily enough looks like WAF is almost designed for just this but its min request count is too high – Paul Taylor Jun 15 '18 at 11:15
  • Hi never implemented this but looking at again, slightly differently. At this point I dont need any request to fail I just need to be able to pass the licensekey to the query so that it is stored in localaccesslog.txt on server so I can analyse it but not passed to Cloudfront so request from different licenses would use same cached result, so would forward all, cache on whitelist work for that, in https://stackoverflow.com/questions/72197465/how-to-configure-cloudfront-custom-cache-policy-to-consider-all-query-parameters was looking at Include all query strings except option, would that work – Paul Taylor May 11 '22 at 08:18