Recently, I had a requirement to expose a REST API from API Gateway exclusively through CloudFront. Keeping API Gateway behind Cloudfront provides additional layer of security because Cloudfront comes with automatic protections of AWS Shield Standard, at no additional charge.
There are several ways to achieve this, for example, using a signed request using Lambda@Edge in Cloudfront, but I went for a simpler solution, which is using a custom header in the request to API Gateway from the Cloudfront and using Lambda authorizer at the API gateway to validate it. This blog post explains how to implement this solution.
How it works
In the Cloudfront distribution, we create an origin for API gateway endpoint.
Here, we define a custom header on this cloudfront origin.
When a request comes to Cloudfront distribution, as per the behaviour we define, it will call the API gateway endpoint.
This request will contain the custom header.
In the API Gateway end, there is an Lambda authorizer which validates this incoming header.
There is a SSM secure parameter which holds the value of the header.
When it is required to validate the incoming header, Lambda authorizer will fetch this value from the parameter store.
Communication between the Cloudfront distribution and the API gateway is secure and there is no way someone who doesn't have access to Cloudfront settings will get to know the header value. However, keeping a static value for a validation seems not correct. So, it is better to rotate the value of the header for added security.
Rotating the header value
There is a EventBridge scheduler which will invoke a Lambda function in a given interval.
-
This Lambda function will perform below actions:
- Generate a random value for the header.
- Update the SSM secure parameter.
- Update the Cloudfront origin's custom header value.
Once this Lambda execution is successful, in both sides (Cloudfront and API Gateway) the new header value will be used.
Try this yourself
Here is a Github repository of the project I created to try out this solution. https://github.com/pubudusj/secure-api-with-cloudfront
You can deploy this to your AWS account using CDK.
Please note: First, you need to create a secure SSM parameter with any value. Then create an
.env
file copying the.env.example
file in the project root directory and set the name/path of the parameter in the .env file.
In the CDK code, you will notice that the initial header value is hardcoded.
origin=origins.RestApiOrigin(
rest_api,
origin_path="/prod",
custom_headers={custom_header_key: "test"},
),
Also, this obviously does not match with the value you have in the SSM parameter.
This is fine, because this stack includes a Cloudformation custom resource, which will be executed on create. This custom resource will start the initial token rotation as soon as the stack is created which generates a new header value and syncs both SSM secure parameter and Cloudfront distribution.
Once the stack is deployed, you can access the API using API Gateway endpoint and also with Cloudfront endpoint.
For example:
- Cloudfront endpoint:
https://[CloudfrontPrefix].cloudfront.net/prod/hello
- API Gateway endpoint:
https://[APIGWPrefix].execute-api.[region].amazonaws.com/prod/hello
You will notice that Cloudfront endpoint works fine while there is a 401 Unauthorized
error from the API Gateway endpoint.
Tips/Lesson Learned
Here I have used an SSM Parameter store with a custom built rotation mechanism. However, you can use Secret manager instead with its inbuilt rotation features. Make sure you are aware of the differences. Yan has done this comparison in detail in his blog post https://theburningmonk.com/2023/03/the-old-faithful-why-ssm-parameter-store-still-reigns-over-secrets-manager/
Keep in mind that when setting a new custom header value on the Cloudfront origin, it will perform a re-deployment. Cloudfront re-deployment takes time. It can be within a couple of minutes to 10-15 minutes too. But for a change like this, it should be faster.
Because of this reason, during a Cloudfront re-deployment, your SSM parameter and incoming header value at API Gateway can be different. One solution I used to address this is to cache the Lambda authorization result. Here I used a ttl of 5 minutes, but you can adjust as required.
authorizer = apigateway.RequestAuthorizer(
self,
"LambdaHeaderAuthorizer",
handler=custom_authorizer,
identity_sources=[apigateway.IdentitySource.header(custom_header_key)],
results_cache_ttl=Duration.minutes(5),
)
Useful Links
Cloudfront custom headers: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html
API Gateway Lambda authorizer: https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html
Please let me know your thoughts on this implementation.
You can find more AWS and Serverless contents at my personal blog: https://pubudu.dev
And don't forget to follow me on LinkedIn too:
https://www.linkedin.com/in/pubudusj
Top comments (0)