Protecting APIs with custom headers in CloudFront
Tags: aws, security, api, cloud
Published: 2022-04-29 06:38:06

The protection of our APIs against DDOS attacks has multiple layers. One of them is restricting access to the endpoints to trusted sources. We can use CloudFront to perform this task.

## 1. The problem

Alice is a cloud engineer and works for Example Corp. They built one of their microservices a few years ago, and although Alice recently wanted to redesign the architecture, deploying new features was always a priority.

The service was behind a legacy <a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/introduction.html" class="link" target="_blank" rel="noopener noreferrer">Classic Load Balancer</a>, and it had worked well until the service received a DDOS attack one day.

The service was down for hours, and the company lost tens of thousands of dollars.

## 2. Chosen solution

Suddenly it became urgent to redesign the service.

Alice and her coworkers decided to put the service behind an <a href="https://aws.amazon.com/api-gateway/" class="link" target="_blank" rel="noopener noreferrer">Amazon API Gateway</a> and protect the API with <a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html" class="link" target="_blank" rel="noopener noreferrer">CloudFront</a>.

### 2.1. Regional endpoint with self-managed distribution

When we create a new <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html" class="link" target="_blank" rel="noopener noreferrer">REST API</a> with API Gateway, we can choose if the endpoint is edge-optimized or regionally deployed.

Edge-optimized endpoints come with an AWS-managed CloudFront distribution, which we can't access, so we can't make any changes to its settings.

But we can manually create and deploy a self-managed CloudFront distribution and put it before the **regional** endpoint. In this case we need to select the API URL (which has the `https://{restapi_id}.execute-api.{region}.amazonaws.com/{stage_name}/` format) as a **custom origin** for the distribution.

### 2.2. Restricting access with origin headers

We haven't finished it yet because the API is public, and anyone can invoke it directly.

We can set up a **custom origin header** in CloudFront, which will protect our API. When we send a request to the endpoint via CloudFront (either by invoking the distribution's URL or setting up a custom domain), CloudFront will add the custom header to the request made to API Gateway (origin request). This header won't be in the client request to CloudFront (viewer request) because the client is not aware of the existence of the custom header.

The custom header is called `x-api-key`, and we add a random string as a value. The longer the string is the better it is for our purpose (although there might be some unexpected limitations on the length, see <a href="https://www.how-hard-can-it.be/cloudfront-origin-custom-headers-403-error/" class="link" target="_blank" rel="noopener noreferrer">this article</a>, but 32 characters work well from my experience).

We then set up this string as an **API key** in API Gateway. It means that API Gateway will deny any requests that don't have the specified API key. This way, direct invocations to the endpoint will not receive a successful response because the caller won't know the API key. But when we invoke the endpoint via CloudFront, the request will succeed because the distribution will add the necessary key to the request.

## 3. Protection at the edge

CloudFront provides effective protection against DDOS attacks at Layer 3/4 (network and transport layers) and Layer 7 (application level).

It integrates with <a href="https://aws.amazon.com/shield/" class="link" target="_blank" rel="noopener noreferrer">Shield Standard</a> and <a href="https://aws.amazon.com/waf/" class="link" target="_blank" rel="noopener noreferrer">WAF</a>, and it won't let malicious traffic reach the origin because it captures it at the adge.

## 4. Main steps

I'm not writing about how to create an API or set up a CloudFront distribution (links to the relevant documentation in the References section). Instead, let's discuss the core steps that make the architecture work.

### 4.1. Forwarding all headers to the origin

We have to set up the behavior to forward all headers to the origin, in this case, API Gateway. We can do it by selecting the `CachingDisabled` cache policy.

![Caching disabled in CloudFront](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lxw83ri2hgc4j0d5xfgx.png)

This setting will prevent CloudFront from storing the headers in the cache key.

### 4.2. Adding the custom header

We also have to add the `x-api-key` header with the value of the API key in the origin settings:

![Setting x-api-key header in CloudFront](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ifru24fqsfkwin8kk2jz.png)

CloudFront will add this header to all requests made to API Gateway.

### 4.3 Creating an API Key in API Gateway

We must let API Gateway know of the custom header CloudFront will send. API Gateway will convert the value of the `x-api-key` header to an API key value. Here we choose `HEADER` as the API Key source.

![Set API key in API Gateway](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p9wbyc0j31c9ry0r7p3w.png)

After creating the API key, we can add it to the endpoints we want to protect.

### 4.4. Setting up a usage plan

The API key alone won't work because it should belong to a usage plan. We have to set up a **usage plan** in API Gateway, attach it to the API stage, and add the newly created API key.

If we don't create a usage plan, we'll receive a 403 error and will see the following log message:

```bash
API Key  not authorized because method 'GET /...' requires API Key and API Key is not associated with a Usage Plan for API Stage STAGE NAME: API Stage is not in a usage plan
```

### 4.5. Ready to go!

This part of the architecture is complete, we can make requests to the API via CloudFront, and we should receive the response payload.

If we investigate the API Gateway logs in <a href="https://docs.aws.amazon.com/cloudwatch/" class="link" target="_blank" rel="noopener noreferrer">Cloudwatch</a>, we'll see the `User-Agent=Amazon CloudFront` header.

But if we try to invoke the API directly, we will receive a `403 Forbidden` error.

## 5. Considerations

### 5.1. Other solutions are possible

It's not the only solution Alice can chosen. She can set up an <a href="https://aws.amazon.com/elasticloadbalancing/application-load-balancer/" class="link" target="_blank" rel="noopener noreferrer">Application Load Balancer</a> if she wants to keep the original architecture. CloudFront can protect ALBs in a similar way.

In this case, Alice has to create a rule in the ALB, which watches the custom header CloudFront sents. If the custom header is in the request, ALB will forward it to the target. If not, it will deny the request.

Alice can also choose different solutions with CloudFront and API Gateway, which I might explore in a separate post.

### 5.2 Securing the API key

Malicious actors are also aware that the `x-api-key` header can exist, so they might try to invoke the API directly. Many APIs have an easily guessable custom domain like `api.example.com`. In this case, Alice should be very thoughtful when protecting the endpoint from unwanted access.

It's important to find a <a href="https://www.hivesystems.io/blog/are-your-passwords-in-the-green" class="link" target="_blank" rel="noopener noreferrer">strong and secure</a> API key and apply **rate limits** in API Gateway.

Alice can also create a rule in WAF to block the IP addresses from which the number of requests exceeds the allowed limit.

## 6. Summary

Defense against DDOS attacks has multiple components. One of them is to protect the API endpoints and hide them behind a CloudFront distribution.

If we set up custom origin headers in CloudFront, we will ensure that the API will only accept requests originating from the distribution. Any direct requests that don't have the API key will result in a `403 Forbidden` error.

## 7. References and further reading

<a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-create-api.html" class="link" target="_blank" rel="noopener noreferrer">Creating a REST API in Amazon API Gateway</a> - It does exactly what the title of the article says

<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-creating-console.html" class="link" target="_blank" rel="noopener noreferrer">Creating a distribution</a> - How to create a CloudFront distribution

<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html#add-origin-custom-headers-configure" class="link" target="_blank" rel="noopener noreferrer">Custom origin headers</a> - How to add custom headers to the CloudFront distribution

<a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-setup-api-key-with-console.html" class="link" target="_blank" rel="noopener noreferrer">Set up API keys using the API Gateway console</a> - No surprise here either

<a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html" class="link" target="_blank" rel="noopener noreferrer">API Gateway API usage plans</a> - Creating a usage plan, adding an API key to it and adding it to a stage