Skip to content

Commit

Permalink
Merge pull request #2560 from kothsidh/kothsidh-feature-waf-cloudfron…
Browse files Browse the repository at this point in the history
…t-websocket-apikey-serverless

New Serverless Land Pattern waf-cloudfront-websocketapi-serverless
  • Loading branch information
julianwood authored Jan 27, 2025
2 parents 5c58adc + a819704 commit 830d8d0
Show file tree
Hide file tree
Showing 14 changed files with 669 additions and 0 deletions.
114 changes: 114 additions & 0 deletions waf-cloudfront-websocket-apigw-cdk-python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Protecting WebSocket API with CloudFront and WAF Integration

This pattern implements a secure WebSocket API using AWS CDK, integrating CloudFront for distribution and WAF for protection through AWS CDK with Python. It makes use of API keys to ensure that the Websocket endpoint can only be accessed via the CloudFront distribution by passing the API key as custom header from CloudFront.

The WebSocket API provides real-time communication capabilities, while CloudFront ensures low-latency content delivery. The Web Application Firewall (WAF) adds an extra layer of security by protecting against common web exploits and controlling access based on configurable rules.

![Alt text](images/architecturediagram.png?raw=true "Architecture Diagram for WebSocket API with CloudFront and WAF Integration")


Learn more about this pattern at [Serverless Land Patterns](https://serverlessland.com/patterns/waf-cloudfront-websocket-apigw-cdk-python).

Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example.

### Prerequisites

- Python 3.9 or later
- AWS CDK CLI
- AWS CLI configured with appropriate credentials

***Please note that AWS WAF is available globally for CloudFront distributions. So you must use the Region us-east-1 region while deploying the stack (N. Virginia)***

### Installation

1. Clone the repository and change directory to pattern directory:
```
git clone https://github.com/aws-samples/serverless-patterns
cd serverless-patterns/waf-cloudfront-websocket-apigw-cdk-python
```

2. Create and activate a virtual environment:
```
python -m venv .venv
source .venv/bin/activate
```

3. Install dependencies:
```
python -m pip install -r requirements.txt
```


4. Deploy the stack:
```
cdk bootstrap
cdk deploy
```


### Configuration

The main configuration for the WebSocket API and related services is defined in `my_websocket_api/my_websocket_api_stack.py`. Key configurations include:

- WebSocket API settings
- Lambda function for handling WebSocket events
- CloudFront distribution settings
- WAF Web ACL rules



### Troubleshooting

1. Connection Issues:
- Ensure you're using the correct CloudFront URL with the "wss://" protocol.
- Verify that the API key is correctly set in the CloudFront distribution's custom headers.

2. Lambda Function Errors:
- Check CloudWatch Logs for the Lambda function to see detailed error messages.
- Ensure the Lambda function has the necessary permissions to execute and access required resources.

3. WAF Blocking Requests:
- Review the WAF rules in the AWS Console to ensure they're not unintentionally blocking legitimate traffic.
- Check the WAF logs in CloudWatch for details on blocked requests.



## Data Flow

The WebSocket API handles data flow as follows:

1. Client initiates a WebSocket connection to the CloudFront distribution URL.
2. WAF validates the request against the configured rules
3. CloudFront forwards the request to the API Gateway WebSocket API with the "x-api-key" as custom header.
4. Websocket API validates the API key and routes the request based on the route selection expression.

![Alt text](images/RequestFlow.png?raw=true "Request Flow for WebSocket API with CloudFront and WAF Integration")

## Testing

Copy the "DistributionURL" value from the Cloudformation Stack's output section. Use it to connect to your Webosocket API. When you connect to your API, API Gateway invokes the $connect route.
```
wscat -c <DistributionURL>
```

Alternatively, you may also use Postman's Websocket Client to test the connection:

![Alt text](images/PostmanScreenshot.png?raw=true "Postman Screenshot for Websocket Connection via CloudFront URL")

After the connection is successful, you may verify the AWS Lambda execution logs to validate the messages that were sent.


## Cleanup

1. Delete the stack
```bash
cdk destroy STACK-NAME
```
1. Confirm the stack has been deleted
```bash
cdk list
```
----
Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.

SPDX-License-Identifier: MIT-0
29 changes: 29 additions & 0 deletions waf-cloudfront-websocket-apigw-cdk-python/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env python3
import os

import aws_cdk as cdk

from my_websocket_api.my_websocket_api_stack import MyWebsocketApiStack


app = cdk.App()
MyWebsocketApiStack(app, "MyWebsocketApiStack",
# If you don't specify 'env', this stack will be environment-agnostic.
# Account/Region-dependent features and context lookups will not work,
# but a single synthesized template can be deployed anywhere.

# Uncomment the next line to specialize this stack for the AWS Account
# and Region that are implied by the current CLI configuration.

#env=cdk.Environment(account=os.getenv('CDK_DEFAULT_ACCOUNT'), region=os.getenv('CDK_DEFAULT_REGION')),

# Uncomment the next line if you know exactly what Account and Region you
# want to deploy the stack to. */

#env=cdk.Environment(account='123456789012', region='us-east-1'),

# For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html
)

app.synth()

78 changes: 78 additions & 0 deletions waf-cloudfront-websocket-apigw-cdk-python/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"app": "python3 app.py",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"requirements*.txt",
"source.bat",
"**/__init__.py",
"**/__pycache__"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
"@aws-cdk/aws-kms:aliasNameRef": true,
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
"@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
"@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
"@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
"@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
"@aws-cdk/aws-eks:nodegroupNameAttribute": true,
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
"@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
"@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
"@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
"@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
"@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true,
"@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
"@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
"@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
"@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
"@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true,
"@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true
}
}
56 changes: 56 additions & 0 deletions waf-cloudfront-websocket-apigw-cdk-python/example-pattern.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"title": "Protecting WebSocket API with CloudFront and WAF Integration",
"description": "Create a WebSocket API integration with CloudFront and WAF using API Keys",
"language": "Python",
"level": "200",
"framework": "CDK",
"introBox": {
"headline": "How it works",
"text": [
"This pattern implements a secure WebSocket API using AWS CDK, integrating CloudFront for distribution and WAF for protection through AWS CDK with Python. It makes use of API keys to ensure that the Websocket endpoint can only be accessed via the CloudFront distribution by passing the API key as custom header from CloudFront.The WebSocket API provides real-time communication capabilities, while CloudFront ensures low-latency content delivery. The Web Application Firewall (WAF) adds an extra layer of security by protecting against common web exploits and controlling access based on configurable rules."
]
},
"gitHub": {
"template": {
"repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/waf-cloudfront-websocket-apigw-cdk-python",
"templateURL": "serverless-patterns/waf-cloudfront-websocket-apigw-cdk-python",
"projectFolder": "waf-cloudfront-websocket-apigw-cdk-python",
"templateFile": "my_websocket_api/my_websocket_api_stack.py"
}
},
"resources": {
"bullets": [
{
"text": "Protecting your API using Amazon API Gateway and AWS WAF",
"link": "https://aws.amazon.com/blogs/compute/protecting-your-api-using-amazon-api-gateway-and-aws-waf-part-2/"
},
{
"text": "Route Configuration for a WebSocket API",
"link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/websocket-api-develop-routes.html#apigateway-websocket-api-routes"
}
]
},
"deploy": {
"text": [
"cdk deploy"
]
},
"testing": {
"text": [
"See the GitHub repo for detailed testing instructions."
]
},
"cleanup": {
"text": [
"cdk destroy"
]
},
"authors": [
{
"name": "Sidharth Kothari",
"image": "https://www.linkedin.com/in/sidharthkothari/",
"bio": "Cloud Engineer @AWS",
"linkedin": "sidharthkothari"
}
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions waf-cloudfront-websocket-apigw-cdk-python/lambda/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import json


def handler(event, context):
print(event)
connection_id = event['requestContext']['connectionId']
route_key = event['requestContext']['routeKey']

# Handle different route types
if route_key == "$connect":
print("Connect Route Triggered, Connection ID:", connection_id)
return handle_connect(connection_id)
elif route_key == "$disconnect":
print("Disconnect Route Triggered, Connection ID:", connection_id)
return handle_disconnect(connection_id)
else:
# Handle other route types or invalid routes
print("Default Route Triggered, Connection ID:", connection_id)
return handle_default(event, connection_id)

def handle_connect(connection_id):
return {
'statusCode': 200,
'body': f'Connected with ID: {connection_id}'
}

def handle_disconnect(connection_id):
return {
'statusCode': 200,
'body': f'Disconnected ID: {connection_id}'
}

def handle_default(event, connection_id):
return {
'statusCode': 200,
'body': json.dumps({
'message': 'Message received',
'connectionId': connection_id,
'event': event
})
}
Empty file.
Loading

0 comments on commit 830d8d0

Please sign in to comment.