DDoS Resilience - the unbelievable importance of HTTP caching

6 minute read
Content level: Advanced
0

Written primarily with CloudFront in mind, this article describes the importance of HTTP caching in preventing malicious requests from reaching your origin as well as some of the CloudFront features and Cache-Control response header values that can help you optimize cache-hit ratio

Please read this article in conjunction with DDoS Resilience - Using AWS WAF to protect against DDoS Botnets

HTTP Caching with CloudFront

Amazon CloudFront's caching capabilities are a powerful tool for protecting your origin servers capacity. By implementing strategic caching policies, even with short time to live (TTLs), you can significantly reduce the load on your origin infrastructure. When content is served from CloudFront's cache hierarchy edge popregional edge caches (RECs) (with or without CloudFronts Origin Shield feature), requests may be cache populated from downstream peers in network, preventing volume from reaching your origin. For content that isn't yet cached or has expired, CloudFront's native request collapsing further protects origin's by consolidating multiple simultaneous requests into a single origin request. This combination of edge caching and request collapsing means that even during high-traffic events or volumetric request floods, only a fraction of requests need to reach your origin, making CDN an essential strategy for origin capacity preservation.

There's a common misconception among content owners that their content is not CDN-cacheable, when in fact it can be cached, or be made to be cacheable. Even when dealing with 'real' dynamic content delivered through asynchronous requests, the main page structure itself can and should be cached. Maximizing the cache hit ratio is crucial for performance and DDoS resilience - it can be the determining factor between whether an HTTP request flood attack impacts your service or not.

Page cacheability should be established as a non-functional requirement during the initial planning stages of any website deployment.

Did you know? Statistically, the ‘/’ URI is the most commonly targetted URI by bad actors in HTTP request floods.

The following solutions may be considered for some common perceived “blockers” to caching static content:

  • For web pages requiring immediate updates when new content is published, content freshness can be managed through multiple strategies, leveraging HTTP standard cache controls as follows:
    • Use CloudFront cache invalidations which can be automatically triggered through content management system integrations during publish events.
    • Implement a split TTL approach:
      • set 0 TTL for viewers via the ‘Cache-Control’ response header ‘max-age=0’ directive. This means that clients will always check for content freshness on page re-load AND
      • set longer TTLs at the CDN level using the ‘s-maxage’ Cache-Control directive or non-zero Minimum TTL in the relevant distribution caching policy (the latter is preferred to enable request collapsing).
    • Use microcaching with Minimum TTL values as low as 1 or 2s for non-personalized but highly dynamic content such as election results, live streaming manifests
    • Set Etag/Date-Modified response headers to enable conditional requests which optimizes page load times and minimizes Data Transfer Out charges, by allowing HTTP 304 (Not Modified) responses with 0 response body, when content has not yet changed.
  • For static assets like images, scripts, and other page resources, implement unique file versioning with far-future client TTLs (1 year or more). Instead of updating files in place, publish new versions with unique URIs or query strings, and update their references in HTML. This strategy provides optimal delivery efficiency and maximum offload across browser caches, CDN, and origin services while ensuring immediate content updates through reference changes rather than cache invalidations.
  • For sites with mixed content, create multiple cache behaviors with appropriate cache policies to maximize DDoS resilience. While static content should be aggressively cached, truly dynamic endpoints that cannot be cached should be identified early and protected with additional controls such as scoped-down AWS WAF rate-based rules with lower limits than any ‘catch-all’ rate-based rules.
  • Resources with Cache-Control response headers like ‘no-cache, no-store’ can be cached on CDN by using a non-zero ‘minimumTTL’ in the relevant CloudFront cache behavior, without compromizing security
  • If you are currently serving cookies in responses to page requests, instead serve cookies from an asynchronous request
  • In case of a situation where the origin performance has been adversely impacted (for whatever reason), consider adding the ‘stale-while-revalidate’ Cache-Control directive, so that users no longer need to wait for responses from the origin. It’s ideal for content that refreshes frequently or unpredictably, or where content requires significant time to regenerate, and where having the latest version of the content is not essential.
  • The ‘stale-if-error’ directive enhances the user experience and improves availability by serving stale content when origins return an error.

Request collapsing

Request collapsing, which is when only a single request with a given cache key is sent to the origin from the same regional edge cache (REC), while other concurrent requests are paused until the outstanding response is received at the REC, reduces the number of requests sent to the origin from a given REC during a typical DDoS attack, to 1. This is important when a requested URI path is not in cache, or has expired.

The following configurations mean that request collapsing will not occur:

A request flood attack in conjunction with non-collapsed but otherwise cacheable requests, can be sufficient to overwhelm some origins, especially those that are unable to dynamically auto-scale.

Origin Shield

Enabling features like CloudFront Origin Shield can further help reduce the load on your origin by enabling a centralized caching layer instead of using distributed regional edge caches. This can be useful when you have a low-capacity origin without the option to dynamically scale.

Error pages and error caching

Cloudfront caches some 4xx and 5xx status code returned by your origin for a default duration of 10s. While it might seem initially seem illogical to cache server errors, doing so can prevent a snowball effect of viewer retries from an error response further degrading an origins heath. Another option is to use the ‘stale-if-error’ Cache-Control directive for content that is cached.

In addition, CloudFront allows you to create custom error pages. If you choose to configure these, we recommend that you store them in an Amazon S3 bucket so that CloudFront is able to access them even when your other origin(s) are returning errors.

EC2 or S3-based caching

There are some edge use-cases such as requiring regional TLS-termination, where customers do not want to use a global CDN like CloudFront - in this case it’s still worth setting up a caching tier using S3, or dynamically-scaled EC2 instances running a cache such as Varnish to protect the content source. Most major Content Management Systems (CMS) will have a plug-in to replicate static content to S3, and there's an AWS Blog Hosting HTTPS Static Websites with ALB, S3, and PrivateLink that gives a reference architecture on accessing S3 from an ALB without using an EC2 tier.