diff --git a/waf-cloudfront-websocket-apigw-cdk-python/README.md b/waf-cloudfront-websocket-apigw-cdk-python/README.md new file mode 100644 index 000000000..bab7a2e90 --- /dev/null +++ b/waf-cloudfront-websocket-apigw-cdk-python/README.md @@ -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 +``` + +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 \ No newline at end of file diff --git a/waf-cloudfront-websocket-apigw-cdk-python/app.py b/waf-cloudfront-websocket-apigw-cdk-python/app.py new file mode 100644 index 000000000..7daff6899 --- /dev/null +++ b/waf-cloudfront-websocket-apigw-cdk-python/app.py @@ -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() + diff --git a/waf-cloudfront-websocket-apigw-cdk-python/cdk.json b/waf-cloudfront-websocket-apigw-cdk-python/cdk.json new file mode 100644 index 000000000..a23836750 --- /dev/null +++ b/waf-cloudfront-websocket-apigw-cdk-python/cdk.json @@ -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 + } +} diff --git a/waf-cloudfront-websocket-apigw-cdk-python/example-pattern.json b/waf-cloudfront-websocket-apigw-cdk-python/example-pattern.json new file mode 100644 index 000000000..2482ae1fe --- /dev/null +++ b/waf-cloudfront-websocket-apigw-cdk-python/example-pattern.json @@ -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" + } + ] +} diff --git a/waf-cloudfront-websocket-apigw-cdk-python/images/PostmanScreenshot.png b/waf-cloudfront-websocket-apigw-cdk-python/images/PostmanScreenshot.png new file mode 100644 index 000000000..c17cbfd36 Binary files /dev/null and b/waf-cloudfront-websocket-apigw-cdk-python/images/PostmanScreenshot.png differ diff --git a/waf-cloudfront-websocket-apigw-cdk-python/images/RequestFlow.png b/waf-cloudfront-websocket-apigw-cdk-python/images/RequestFlow.png new file mode 100644 index 000000000..a976f6c41 Binary files /dev/null and b/waf-cloudfront-websocket-apigw-cdk-python/images/RequestFlow.png differ diff --git a/waf-cloudfront-websocket-apigw-cdk-python/images/architecturediagram.png b/waf-cloudfront-websocket-apigw-cdk-python/images/architecturediagram.png new file mode 100644 index 000000000..daedbe83a Binary files /dev/null and b/waf-cloudfront-websocket-apigw-cdk-python/images/architecturediagram.png differ diff --git a/waf-cloudfront-websocket-apigw-cdk-python/lambda/index.py b/waf-cloudfront-websocket-apigw-cdk-python/lambda/index.py new file mode 100644 index 000000000..7798893f5 --- /dev/null +++ b/waf-cloudfront-websocket-apigw-cdk-python/lambda/index.py @@ -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 + }) + } \ No newline at end of file diff --git a/waf-cloudfront-websocket-apigw-cdk-python/my_websocket_api/__init__.py b/waf-cloudfront-websocket-apigw-cdk-python/my_websocket_api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/waf-cloudfront-websocket-apigw-cdk-python/my_websocket_api/my_websocket_api_stack.py b/waf-cloudfront-websocket-apigw-cdk-python/my_websocket_api/my_websocket_api_stack.py new file mode 100644 index 000000000..143facec2 --- /dev/null +++ b/waf-cloudfront-websocket-apigw-cdk-python/my_websocket_api/my_websocket_api_stack.py @@ -0,0 +1,241 @@ +from aws_cdk import ( + Stack, + aws_apigatewayv2 as apigatewayv2, + aws_apigateway as apigateway, + aws_cloudfront as cloudfront, + aws_cloudfront_origins as origins, + aws_lambda as _lambda, + custom_resources, + aws_iam, + aws_wafv2 as wafv2, + CfnOutput +) +from constructs import Construct + +class MyWebsocketApiStack(Stack): + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + stage_value = "prod" + + + # Create WebSocket API + websocket_api = apigatewayv2.CfnApi( + self, "WebSocketAPI", + name="my-websocket-api", + protocol_type="WEBSOCKET", + route_selection_expression="$request.body.action", + api_key_selection_expression="$request.header.x-api-key", + disable_schema_validation=False + ) + + + #Create a lambda execution role + lambda_role = aws_iam.Role( + self, "LambdaRole", + assumed_by=aws_iam.ServicePrincipal("lambda.amazonaws.com"), + managed_policies=[ + aws_iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AWSLambdaBasicExecutionRole") + ] + ) + + + # Create Lambda function for WebSocket handler + handler = _lambda.Function( + self, "WebSocketHandler", + runtime=_lambda.Runtime.PYTHON_3_9, + handler="index.handler", + code=_lambda.Code.from_asset("lambda"), + role=lambda_role, + environment={ + "WEBSOCKET_API_ID": websocket_api.ref, + "WEBSOCKET_API_ENDPOINT": websocket_api.attr_api_endpoint + } + ) + + + # add permission for apigateway websocket invokes + handler.add_permission( + "WebSocketInvoke", + principal=aws_iam.ServicePrincipal("apigateway.amazonaws.com"), + action="lambda:InvokeFunction" + ) + + + + # Create default route + default_route = apigatewayv2.CfnRoute( + self, "DefaultRoute", + api_id=websocket_api.ref, + route_key="$default", + target="integrations/" + self.create_lambda_integration(websocket_api, handler, "$default").ref) + + # Create connect route + connect_route = apigatewayv2.CfnRoute( + self, "ConnectRoute", + api_id=websocket_api.ref, + route_key="$connect", + api_key_required=True, + target="integrations/" + self.create_lambda_integration(websocket_api, handler, "$connect").ref) + + # Create disconnect route + disconnect_route = apigatewayv2.CfnRoute( + self, "DisconnectRoute", + api_id=websocket_api.ref, + route_key="$disconnect", + target="integrations/" + self.create_lambda_integration(websocket_api, handler, "$disconnect").ref) + + + #create a deployment on api stage + deployment = apigatewayv2.CfnDeployment( + self, "WebSocketDeployment", + api_id=websocket_api.ref, + ) + deployment.node.add_dependency(default_route) + deployment.node.add_dependency(connect_route) + deployment.node.add_dependency(disconnect_route) + + # Create WebSocket Stage + stage = apigatewayv2.CfnStage( + self, "ProdStage", + api_id=websocket_api.ref, + stage_name=stage_value, + deployment_id=deployment.ref + ) + # add dependency on deployment + stage.node.add_dependency(deployment) + + + + # Create API Key + api_key = apigateway.CfnApiKey( + self, "WebSocketApiKey", + name="my-websocket-api-key", + enabled=True + ) + + # fetch the apikey value and store it in a variable using custom resource + api_key_details: custom_resources.AwsSdkCall = custom_resources.AwsSdkCall( + service="APIGateway", + action="getApiKey", + parameters={ + "apiKey": api_key.ref, + "includeValue": True, + }, + physical_resource_id=custom_resources.PhysicalResourceId.of( + f"APIKey:{api_key.ref}" + ), + ) + + api_key_cr = custom_resources.AwsCustomResource( + self, + "api-key-cr", + policy=custom_resources.AwsCustomResourcePolicy.from_statements( + [ + aws_iam.PolicyStatement( + effect=aws_iam.Effect.ALLOW, + resources=[ + f"arn:aws:apigateway:{self.region}::/apikeys/{api_key.ref}" + ], + actions=["apigateway:GET"], + ), + ] + ), + on_create=api_key_details, + on_update=api_key_details, + ) + api_key_cr.node.add_dependency(api_key) + + self.apikey_value = api_key_cr.get_response_field("value") + + # Create Usage Plan + usage_plan = apigateway.CfnUsagePlan( + self, "WebSocketUsagePlan", + api_stages=[apigateway.CfnUsagePlan.ApiStageProperty( + api_id=websocket_api.ref, + stage="prod")], + throttle=apigateway.CfnUsagePlan.ThrottleSettingsProperty( + burst_limit=100, + rate_limit=50 + ) + ) + usage_plan.node.add_dependency(stage) + + + # Associate API Key with Usage Plan + apigateway.CfnUsagePlanKey( + self, "WebSocketUsagePlanKey", + key_id=api_key.ref, + key_type="API_KEY", + usage_plan_id=usage_plan.ref + ) + usage_plan.node.add_dependency(api_key) + + + + # Create WAF Web ACL + waf_acl = wafv2.CfnWebACL( + self, "WebSocketWafAcl", + default_action=wafv2.CfnWebACL.DefaultActionProperty(allow={}), + scope="CLOUDFRONT", + visibility_config=wafv2.CfnWebACL.VisibilityConfigProperty( + cloud_watch_metrics_enabled=True, + metric_name="WebSocketWafAcl", + sampled_requests_enabled=True + ), + rules=[ + wafv2.CfnWebACL.RuleProperty( + name="LimitRequests", + priority=1, + statement=wafv2.CfnWebACL.StatementProperty( + rate_based_statement=wafv2.CfnWebACL.RateBasedStatementProperty( + limit=2000, + aggregate_key_type="IP" + ) + ), + action=wafv2.CfnWebACL.RuleActionProperty( + block={} + ), + visibility_config=wafv2.CfnWebACL.VisibilityConfigProperty( + cloud_watch_metrics_enabled=True, + metric_name="LimitRequestsRule", + sampled_requests_enabled=True + ) + ) + ] + ) + + # Create CloudFront distribution + distribution = cloudfront.Distribution( + self, "Distribution", + default_behavior=cloudfront.BehaviorOptions( + origin=origins.HttpOrigin( + f"{websocket_api.ref}.execute-api.{self.region}.amazonaws.com", + origin_path=f"/{stage_value}", + custom_headers={ + "x-api-key": self.apikey_value + } + ), + + allowed_methods=cloudfront.AllowedMethods.ALLOW_ALL, + viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + cache_policy=cloudfront.CachePolicy.CACHING_DISABLED, + origin_request_policy=cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER + ), + web_acl_id=waf_acl.attr_arn + ) + distribution.node.add_dependency(waf_acl) + distribution.node.add_dependency(websocket_api) + distribution.node.add_dependency(api_key_cr) + + # Output the CloudFront distribution URL with wss:// + CfnOutput(self, "DistributionURL", value=f"wss://{distribution.domain_name}") + + + def create_lambda_integration(self, websocket_api: apigatewayv2.CfnApi, handler: _lambda.Function, route_key: str = "") -> apigatewayv2.CfnIntegration: + return apigatewayv2.CfnIntegration( + self, f"Integration{handler.node.id}{route_key}", + api_id=websocket_api.ref, + integration_type="AWS_PROXY", + integration_uri=f"arn:aws:apigateway:{Stack.of(self).region}:lambda:path/2015-03-31/functions/{handler.function_arn}/invocations" + ) + diff --git a/waf-cloudfront-websocket-apigw-cdk-python/requirements-dev.txt b/waf-cloudfront-websocket-apigw-cdk-python/requirements-dev.txt new file mode 100644 index 000000000..927094516 --- /dev/null +++ b/waf-cloudfront-websocket-apigw-cdk-python/requirements-dev.txt @@ -0,0 +1 @@ +pytest==6.2.5 diff --git a/waf-cloudfront-websocket-apigw-cdk-python/requirements.txt b/waf-cloudfront-websocket-apigw-cdk-python/requirements.txt new file mode 100644 index 000000000..4cc165344 --- /dev/null +++ b/waf-cloudfront-websocket-apigw-cdk-python/requirements.txt @@ -0,0 +1,2 @@ +aws-cdk-lib==2.173.2 +constructs>=10.0.0,<11.0.0 diff --git a/waf-cloudfront-websocket-apigw-cdk-python/source.bat b/waf-cloudfront-websocket-apigw-cdk-python/source.bat new file mode 100644 index 000000000..9e1a83442 --- /dev/null +++ b/waf-cloudfront-websocket-apigw-cdk-python/source.bat @@ -0,0 +1,13 @@ +@echo off + +rem The sole purpose of this script is to make the command +rem +rem source .venv/bin/activate +rem +rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. +rem On Windows, this command just runs this batch file (the argument is ignored). +rem +rem Now we don't need to document a Windows command for activating a virtualenv. + +echo Executing .venv\Scripts\activate.bat for you +.venv\Scripts\activate.bat diff --git a/waf-cloudfront-websocket-apigw-cdk-python/waf-cloudfront-websocket-apigw-cdk-python.json b/waf-cloudfront-websocket-apigw-cdk-python/waf-cloudfront-websocket-apigw-cdk-python.json new file mode 100644 index 000000000..e70932c66 --- /dev/null +++ b/waf-cloudfront-websocket-apigw-cdk-python/waf-cloudfront-websocket-apigw-cdk-python.json @@ -0,0 +1,94 @@ +{ + "title": "Amazon API Gateway WebSocket API with Amazon CloudFront & AWS WAF", + "description": "Protecting an Amazon API Gateway WebSocket API with Amazon CloudFront, AWS WAF Integration and 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" + } + ], + "patternArch": { + "icon1": { + "x": 15, + "y": 50, + "service": "waf", + "label": "AWS WAF" + }, + "icon2": { + "x": 40, + "y": 50, + "service": "cloudfront", + "label": "Amazon CloudFront" + }, + "icon3": { + "x": 65, + "y": 50, + "service": "api-gateway", + "label": "Amazon API Gateway" + }, + "icon4": { + "x": 90, + "y": 50, + "service": "lambda", + "label": "AWS Lambda" + }, + "line1": { + "from": "icon1", + "to": "icon2" + }, + "line2": { + "from": "icon2", + "to": "icon3" + }, + "line3": { + "from": "icon3", + "to": "icon4" + } + } +}