AWS Compute Blog
Protecting your API using Amazon API Gateway and AWS WAF — Part 2
This post courtesy of Heitor Lessa, AWS Specialist Solutions Architect – Serverless
In Part 1 of this blog, we described how to protect your API provided by Amazon API Gateway using AWS WAF. In this blog, we show how to use API keys between an Amazon CloudFront distribution and API Gateway to secure access to your API in API Gateway in addition to your preferred authorization (AuthZ) mechanism already set up in API Gateway. For more information about AuthZ mechanisms in API Gateway, see Secure API Access with Amazon Cognito Federated Identities, Amazon Cognito User Pools, and Amazon API Gateway.
We also extend the AWS CloudFormation stack previously used to automate the creation of the following necessary resources of this solution:
- API Gateway usage plans – Manages API keys dedicated to CloudFront as well as throttling and metering usage if necessary
- AWS Lambda function – Updates the AWS CloudFormation stack parameter Timestamp and triggers API keys rotation
- Amazon CloudWatch Events scheduled job – Triggers the Lambda function in a given schedule
The following are alternative solutions to using an API key, depending on your security requirements:
Using a randomly generated HTTP secret header in CloudFront and verifying by API Gateway request validation
Signing incoming requests with Lambda@Edge and verifying with API Gateway Lambda authorizers
Requirements
To follow along, you need full permissions to create, update, and delete API Gateway, CloudFront, Lambda, and CloudWatch Events through AWS CloudFormation.
Extending the existing AWS CloudFormation stack
First, click here to download the full template. Then follow these steps to update the existing AWS CloudFormation stack:
- Go to the AWS Management Console and open the AWS CloudFormation console.
- Select the stack that you created in Part 1, right-click it, and select Update Stack.
- For option 2, choose Choose file and select the template that you downloaded.
- Fill in the required parameters as shown in the following image.
Here’s more information about these parameters:
- API Gateway to send traffic to – We use the same API Gateway URL as in Part 1 except without the URL scheme (https://): cxm45444t9a.execute-api.us-east-2.amazonaws.com/prod
- Rotating API Keys – We define Daily and use 2018-04-03 as the timestamp value to append to the API key name
Continue with the AWS CloudFormation console to complete the operation. It might take a couple of minutes to update the stack as CloudFront takes its time to propagate changes across all point of presences.
Enabling API Keys in the example Pet Store API
While the stack completes in the background, let’s enable the use of API Keys in the API that CloudFront will send traffic to.
- Go to the AWS Management Console and open the API Gateway console.
- Select the API that you created in Part 1 and choose Resources.
- Under /pets, choose GET and then choose Method Request.
- For API Key Required, choose the dropdown menu and choose true.
- To save this change, select the highlighted check mark as shown in the following image.
Next, we need to deploy these changes so that requests sent to /pets fail if an API key isn’t present.
- Choose Actions and select Deploy API.
- Choose the Deployment stage dropdown menu and select the stage you created in Part 1.
- Add a deployment description such as “Requires API Keys under /pets” and choose Deploy.
When the deployment succeeds, you’re redirected to the API Gateway Stage page. There you can use the Invoke URL to test if the following request fails due to not having an API key.
This failure is expected and proves that our deployed changes are working. Next, let’s try to access the same API but this time through our CloudFront distribution.
- From the AWS Management Console, open the AWS Cloudformation console.
- Select the stack that you created in Part 1 and choose Outputs at the bottom left.
- On the CFDistribution line, copy the URL. Before you paste in a new browser tab or window, append ‘/pets’ to it.
As opposed to our first attempt without an API key, we receive a JSON response from the PetStore API. This is because CloudFront is injecting an API key before it forwards the request to the PetStore API. The following image demonstrates both of these tests:
- Successful request when accessing the API through CloudFront
- Unsuccessful request when accessing the API directly through its Invoke URL
This works as a secret between CloudFront and API Gateway, which could be any agreed random secret that can be rotated like an API key. However, it’s important to know that the API key is a feature to track or meter API consumers’ usage. It’s not a secure authorization mechanism and therefore should be used only in conjunction with an API Gateway authorizer.
Rotating API keys
API keys are automatically rotated based on the schedule (e.g., daily or monthly) that you chose when updating the AWS CloudFormation stack. This requires no maintenance or intervention on your part. In this section, we explain how this process works under the hood and what you can do if you want to manually trigger an API key rotation.
The AWS CloudFormation template that we downloaded and used to update our stack does the following in addition to Part 1.
Introduce a Timestamp parameter that is appended to the API key name
Parameters:
Timestamp:
Type: String
Description: Fill in this format <Year>-<Month>-<Day>
Default: 2018-04-02
Create an API Gateway key, API Gateway usage plan, associate the new key with the API gateway given as a parameter, and configure the CloudFront distribution to send a custom header when forwarding traffic to API Gateway
CFDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Logging:
IncludeCookies: 'false'
Bucket: !Sub ${S3BucketAccessLogs}.s3.amazonaws.com
Prefix: cloudfront-logs
Enabled: 'true'
Comment: API Gateway Regional Endpoint Blog post
Origins:
-
Id: APIGWRegional
DomainName: !Select [0, !Split ['/', !Ref ApiURL]]
CustomOriginConfig:
HTTPPort: 443
OriginProtocolPolicy: https-only
OriginCustomHeaders:
-
HeaderName: x-api-key
HeaderValue: !Ref ApiKey
...
ApiUsagePlan:
Type: AWS::ApiGateway::UsagePlan
Properties:
Description: CloudFront usage only
UsagePlanName: CloudFront_only
ApiStages:
-
ApiId: !Select [0, !Split ['.', !Ref ApiURL]]
Stage: !Select [1, !Split ['/', !Ref ApiURL]]
ApiKey:
Type: "AWS::ApiGateway::ApiKey"
Properties:
Name: !Sub "CloudFront-${Timestamp}"
Description: !Sub "CloudFormation API Key ${Timestamp}"
Enabled: true
ApiKeyUsagePlan:
Type: "AWS::ApiGateway::UsagePlanKey"
Properties:
KeyId: !Ref ApiKey
KeyType: API_KEY
UsagePlanId: !Ref ApiUsagePlan
As shown in the ApiKey resource, we append the given Timestamp to Name as well as use it in the API Gateway usage plan key resource. This means that whenever the Timestamp parameter changes, AWS CloudFormation triggers a resource replacement and updates every resource that depends on that API key. In this case, that includes the AWS CloudFront configuration and API Gateway usage plan.
But what does the rotation schedule that you chose at the beginning of this blog mean in this example?
Create a scheduled activity to trigger a Lambda function on a given schedule
Parameters:
...
ApiKeyRotationSchedule:
Description: Schedule to rotate API Keys e.g. Daily, Monthly, Bimonthly basis
Type: String
Default: Daily
AllowedValues:
- Daily
- Fortnightly
- Monthly
- Bimonthly
- Quarterly
ConstraintDescription: Must be any of the available options
Mappings:
ScheduleMap:
CloudwatchEvents:
Daily: "rate(1 day)"
Fortnightly: "rate(14 days)"
Monthly: "rate(30 days)"
Bimonthly: "rate(60 days)"
Quarterly: "rate(90 days)"
Resources:
...
RotateApiKeysScheduledJob:
Type: "AWS::Events::Rule"
Properties:
Description: "ScheduledRule"
ScheduleExpression: !FindInMap [ScheduleMap, CloudwatchEvents, !Ref ApiKeyRotationSchedule]
State: "ENABLED"
Targets:
-
Arn: !GetAtt RotateApiKeysFunction.Arn
Id: "RotateApiKeys"
The resource RotateApiKeysScheduledJob shows that the schedule that you selected through a dropdown menu when updating the AWS CloudFormation stack is actually converted to a CloudWatch Events rule. This in turn triggers a Lambda function that is defined in the same template.
RotateApiKeysFunction:
Type: "AWS::Lambda::Function"
Properties:
Handler: "index.lambda_handler"
Role: !GetAtt RotateApiKeysFunctionRole.Arn
Runtime: python3.6
Environment:
Variables:
StackName: !Ref "AWS::StackName"
Code:
ZipFile: !Sub |
import datetime
import os
import boto3
from botocore.exceptions import ClientError
session = boto3.Session()
cfn = session.client('cloudformation')
timestamp = datetime.date.today()
params = {
'StackName': os.getenv('StackName'),
'UsePreviousTemplate': True,
'Capabilities': ["CAPABILITY_IAM"],
'Parameters': [
{
'ParameterKey': 'ApiURL',
'UsePreviousValue': True
},
{
'ParameterKey': 'ApiKeyRotationSchedule',
'UsePreviousValue': True
},
{
'ParameterKey': 'Timestamp',
'ParameterValue': str(timestamp)
},
],
}
def lambda_handler(event, context):
""" Updates CloudFormation Stack with a new timestamp and returns CloudFormation response"""
try:
response = cfn.update_stack(**params)
except ClientError as err:
if "No updates are to be performed" in err.response['Error']['Message']:
return {"message": err.response['Error']['Message']}
else:
raise Exception("An error happened while updating the stack: {}".format(err))
return response
All this Lambda function does is trigger an AWS CloudFormation stack update via API (exactly what you did through the console but programmatically) and updates the Timestamp parameter. As a result, it rotates the API key and the CloudFront distribution configuration.
This gives you enough flexibility to change the API key rotation schedule at any time without maintaining or writing any code. You can also manually update the stack and rotate the keys by updating the AWS CloudFormation stack’s Timestamp parameter.
Next Steps
We hope you found the information in this blog helpful. You can use it to understand how to create a mechanism to allow traffic only from CloudFront to API Gateway and avoid bypassing the AWS WAF rules that Part 1 set up.
Keep the following important notes in mind about this solution:
- It assumes that you already have a strong AuthZ mechanism, managed by API Gateway, to control access to your API.
- The API Gateway usage plan and other resources created in this solution work only for APIs created in the same account (the ApiUrl parameter).
- If you already use API keys for tracking API usage, consider using either of the following solutions as a replacement:
- Use a random HTTP header value in CloudFront origin configuration and use an API Gateway request model validation to verify it instead of API keys alone.
- Combine Lambda@Edge and an API Gateway custom authorizer to sign and verify incoming requests using a shared secret known only to the two. This is a more advanced technique.