Last active
June 28, 2024 10:00
-
-
Save anaarezo/c2bc2f28cbfb7284b03af523321780bc to your computer and use it in GitHub Desktop.
WAF CDK examples with WAF Stack.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // https://spoofing.medium.com/deploying-a-cloudfront-waf-with-typescript-and-aws-cdk-e35df6d7d00c | |
| import * as cdk from "aws-cdk-lib"; | |
| import * as wafv2 from "aws-cdk-lib/aws-wafv2"; | |
| import { aws_opensearchservice as opensearchservice } from "aws-cdk-lib"; | |
| import { aws_kinesisfirehose as kinesisfirehose } from "aws-cdk-lib"; | |
| import { aws_s3 } from "aws-cdk-lib"; | |
| import * as ec2 from "aws-cdk-lib/aws-ec2"; | |
| import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; | |
| import * as logs from "aws-cdk-lib/aws-logs"; | |
| import { RetentionDays } from "aws-cdk-lib/aws-logs"; | |
| import * as iam from "aws-cdk-lib/aws-iam"; | |
| import { ApplicationStack, ApplicationStackProps } from "./application"; | |
| export function createCloudfrontWAF( | |
| stack: ApplicationStack, | |
| props: ApplicationStackProps | |
| ) { | |
| /////////////////////////////////////////////////////////////////////////////////////////// | |
| //Creating IP Sets to Block/Allow IPs from WAF | |
| /////////////////////////////////////////////////////////////////////////////////////////// | |
| const demoIPSet = new wafv2.CfnIPSet(stack, "DemoIPSet", { | |
| addresses: [ | |
| "10.30.0.0/16", //VPC CIDR | |
| "16.208.45.100/32", //Elastic IP | |
| ], | |
| ipAddressVersion: "IPV4", | |
| scope: "CLOUDFRONT", | |
| }); | |
| /////////////////////////////////////////////////////////////////////////////////////////// | |
| //Creating Regex Patterns for WAF | |
| /////////////////////////////////////////////////////////////////////////////////////////// | |
| // prettier-ignore | |
| const regexPatternSet = new wafv2.CfnRegexPatternSet( | |
| stack, | |
| "DemoRegexPatternSet", | |
| { | |
| regularExpressionList: ["^\\/api\\/v1\\/demo"], //The sequence "\\" inserts a "\" in a string | |
| scope: "CLOUDFRONT", | |
| } | |
| ); | |
| /////////////////////////////////////////////////////////////////////////////////////////// | |
| //Creating Rules for WAF | |
| /////////////////////////////////////////////////////////////////////////////////////////// | |
| interface WafRule { | |
| Rule: wafv2.CfnWebACL.RuleProperty; | |
| } | |
| const awsManagedRules: WafRule[] = [ | |
| { | |
| Rule: { | |
| name: "AllowInternalTraffic", | |
| priority: 0, | |
| statement: { | |
| ipSetReferenceStatement: { | |
| arn: demoIPSet.attrArn, | |
| }, | |
| }, | |
| action: { | |
| allow: {}, | |
| }, | |
| visibilityConfig: { | |
| sampledRequestsEnabled: true, | |
| cloudWatchMetricsEnabled: true, | |
| metricName: "AllowInternalTraffic", | |
| }, | |
| }, | |
| }, | |
| { | |
| Rule: { | |
| name: "IPRateLimitingRule", | |
| priority: 1, | |
| statement: { | |
| rateBasedStatement: { | |
| limit: 600, | |
| aggregateKeyType: "IP", | |
| }, | |
| }, | |
| action: { | |
| block: {}, | |
| }, | |
| visibilityConfig: { | |
| sampledRequestsEnabled: true, | |
| cloudWatchMetricsEnabled: true, | |
| metricName: "IPRateLimitingRule", | |
| }, | |
| }, | |
| }, | |
| { | |
| Rule: { | |
| name: "AWS-AWSManagedRulesCommonRuleSet", | |
| priority: 2, | |
| statement: { | |
| managedRuleGroupStatement: { | |
| vendorName: "AWS", | |
| name: "AWSManagedRulesCommonRuleSet", | |
| excludedRules: [ | |
| { | |
| name: "CrossSiteScripting_BODY", | |
| }, | |
| { | |
| name: "EC2MetaDataSSRF_BODY", | |
| }, | |
| { | |
| name: "GenericLFI_BODY", | |
| }, | |
| { | |
| name: "GenericRFI_BODY", | |
| }, | |
| { | |
| name: "SizeRestrictions_BODY", | |
| }, | |
| ], | |
| }, | |
| }, | |
| overrideAction: { | |
| none: {}, | |
| }, | |
| visibilityConfig: { | |
| sampledRequestsEnabled: true, | |
| cloudWatchMetricsEnabled: true, | |
| metricName: "AWS-AWSManagedRulesCommonRuleSet", | |
| }, | |
| }, | |
| }, | |
| { | |
| Rule: { | |
| name: "AWS-AWSManagedRulesBotControlRuleSet", | |
| priority: 3, | |
| statement: { | |
| managedRuleGroupStatement: { | |
| vendorName: "AWS", | |
| name: "AWSManagedRulesBotControlRuleSet", | |
| excludedRules: [ | |
| { | |
| name: "CategoryAdvertising", | |
| }, | |
| { | |
| name: "CategoryContentFetcher", | |
| }, | |
| { | |
| name: "CategoryHttpLibrary", | |
| }, | |
| { | |
| name: "CategoryLinkChecker", | |
| }, | |
| { | |
| name: "CategoryMiscellaneous", | |
| }, | |
| { | |
| name: "CategoryMonitoring", | |
| }, | |
| { | |
| name: "CategorySeo", | |
| }, | |
| { | |
| name: "CategorySocialMedia", | |
| }, | |
| { | |
| name: "SignalAutomatedBrowser", | |
| }, | |
| { | |
| name: "SignalKnownBotDataCenter", | |
| }, | |
| { | |
| name: "SignalNonBrowserUserAgent", | |
| }, | |
| ], | |
| }, | |
| }, | |
| overrideAction: { | |
| none: {}, | |
| }, | |
| visibilityConfig: { | |
| sampledRequestsEnabled: true, | |
| cloudWatchMetricsEnabled: true, | |
| metricName: "AWS-AWSManagedRulesBotControlRuleSet", | |
| }, | |
| }, | |
| }, | |
| { | |
| Rule: { | |
| name: "AWS-AWSManagedRulesWordPressRuleSet", | |
| priority: 4, | |
| statement: { | |
| managedRuleGroupStatement: { | |
| vendorName: "AWS", | |
| name: "AWSManagedRulesWordPressRuleSet", | |
| }, | |
| }, | |
| overrideAction: { | |
| none: {}, | |
| }, | |
| visibilityConfig: { | |
| sampledRequestsEnabled: true, | |
| cloudWatchMetricsEnabled: true, | |
| metricName: "AWS-AWSManagedRulesWordPressRuleSet", | |
| }, | |
| }, | |
| }, | |
| { | |
| Rule: { | |
| name: "AWS-AWSManagedRulesKnownBadInputsRuleSet", | |
| priority: 5, | |
| statement: { | |
| managedRuleGroupStatement: { | |
| vendorName: "AWS", | |
| name: "AWSManagedRulesKnownBadInputsRuleSet", | |
| }, | |
| }, | |
| overrideAction: { | |
| none: {}, | |
| }, | |
| visibilityConfig: { | |
| sampledRequestsEnabled: true, | |
| cloudWatchMetricsEnabled: true, | |
| metricName: "AWS-AWSManagedRulesKnownBadInputsRuleSet", | |
| }, | |
| }, | |
| }, | |
| { | |
| Rule: { | |
| name: "AWS-AWSManagedRulesUnixRuleSet", | |
| priority: 6, | |
| statement: { | |
| managedRuleGroupStatement: { | |
| vendorName: "AWS", | |
| name: "AWSManagedRulesUnixRuleSet", | |
| excludedRules: [ | |
| { | |
| name: "UNIXShellCommandsVariables_BODY", | |
| }, | |
| ], | |
| }, | |
| }, | |
| overrideAction: { | |
| none: {}, | |
| }, | |
| visibilityConfig: { | |
| sampledRequestsEnabled: true, | |
| cloudWatchMetricsEnabled: true, | |
| metricName: "AWS-AWSManagedRulesUnixRuleSet", | |
| }, | |
| }, | |
| }, | |
| { | |
| Rule: { | |
| name: "AWS-AWSManagedRulesSQLiRuleSet", | |
| priority: 7, | |
| statement: { | |
| managedRuleGroupStatement: { | |
| vendorName: "AWS", | |
| name: "AWSManagedRulesSQLiRuleSet", | |
| excludedRules: [ | |
| { | |
| name: "SQLi_BODY", | |
| }, | |
| ], | |
| }, | |
| }, | |
| overrideAction: { | |
| none: {}, | |
| }, | |
| visibilityConfig: { | |
| sampledRequestsEnabled: true, | |
| cloudWatchMetricsEnabled: true, | |
| metricName: "AWS-AWSManagedRulesSQLiRuleSet", | |
| }, | |
| }, | |
| }, | |
| ]; | |
| /////////////////////////////////////////////////////////////////////////////////////////// | |
| //Creating WebACL for WAF | |
| /////////////////////////////////////////////////////////////////////////////////////////// | |
| const demoWebACL = new wafv2.CfnWebACL(stack, "WebACL", { | |
| defaultAction: { allow: {} }, | |
| scope: "CLOUDFRONT", | |
| visibilityConfig: { | |
| cloudWatchMetricsEnabled: true, | |
| metricName: "WAFMetric", | |
| sampledRequestsEnabled: true, | |
| }, | |
| rules: awsManagedRules.map((wafRule) => wafRule.Rule), | |
| }); | |
| /////////////////////////////////////////////////////////////////////////////////////////// | |
| //Creating OpenSearch for WAF | |
| /////////////////////////////////////////////////////////////////////////////////////////// | |
| const serviceLinkedRole = new cdk.CfnResource(stack, "RoleOpenSearch", { | |
| type: "AWS::IAM::ServiceLinkedRole", | |
| properties: { | |
| AWSServiceName: "es.amazonaws.com", | |
| }, | |
| }); | |
| const openSG = new ec2.SecurityGroup(stack, "SecurityGroupOpenSearch", { | |
| vpc: props.vpc, | |
| allowAllOutbound: true, | |
| }); | |
| openSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443)); | |
| const openSearchSecret = new secretsmanager.Secret( | |
| stack, | |
| "OpenSearchSecret", | |
| { | |
| generateSecretString: { | |
| secretStringTemplate: JSON.stringify({ | |
| username: "master_user", | |
| }), | |
| generateStringKey: "password", | |
| }, | |
| } | |
| ); | |
| const esDomainName = "waf-logs-es-iac"; | |
| const elasticDomain = new opensearchservice.CfnDomain(stack, "OpenSearch", { | |
| accessPolicies: { | |
| Version: "2012-10-17", | |
| Statement: [ | |
| { | |
| Effect: "Allow", | |
| Principal: { | |
| AWS: "*", | |
| }, | |
| Action: "es:*", | |
| Resource: `arn:aws:es:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:domain/${esDomainName}/*`, | |
| }, | |
| ], | |
| }, | |
| domainName: esDomainName, | |
| engineVersion: "OpenSearch_1.3", | |
| advancedSecurityOptions: { | |
| enabled: true, | |
| internalUserDatabaseEnabled: true, | |
| masterUserOptions: { | |
| masterUserName: openSearchSecret | |
| .secretValueFromJson("username") | |
| .unsafeUnwrap(), | |
| masterUserPassword: openSearchSecret | |
| .secretValueFromJson("password") | |
| .unsafeUnwrap(), | |
| }, | |
| }, | |
| domainEndpointOptions: { | |
| enforceHttps: true, | |
| }, | |
| clusterConfig: { | |
| dedicatedMasterCount: 3, | |
| dedicatedMasterEnabled: true, | |
| dedicatedMasterType: "t3.small.search", | |
| instanceCount: 2, | |
| instanceType: "t3.small.search", | |
| zoneAwarenessConfig: { | |
| availabilityZoneCount: 2, | |
| }, | |
| zoneAwarenessEnabled: true, | |
| }, | |
| encryptionAtRestOptions: { | |
| enabled: true, | |
| }, | |
| nodeToNodeEncryptionOptions: { | |
| enabled: true, | |
| }, | |
| ebsOptions: { | |
| ebsEnabled: true, | |
| volumeSize: 10, | |
| volumeType: "gp3", | |
| }, | |
| vpcOptions: { | |
| securityGroupIds: [openSG.securityGroupId], | |
| subnetIds: [ | |
| props.vpc.selectSubnets({ subnetGroupName: "Private" }).subnetIds[0], | |
| props.vpc.selectSubnets({ subnetGroupName: "Private" }).subnetIds[1], | |
| ], | |
| }, | |
| }); | |
| elasticDomain.node.addDependency(serviceLinkedRole); | |
| // /////////////////////////////////////////////////////////////////////////////////////////// | |
| // //Creating Data Firehose Delivery Stream | |
| // /////////////////////////////////////////////////////////////////////////////////////////// | |
| const demoWAFBucket = new aws_s3.Bucket(stack, "DemoAllLogs", { | |
| removalPolicy: cdk.RemovalPolicy.DESTROY, | |
| encryption: aws_s3.BucketEncryption.S3_MANAGED, | |
| autoDeleteObjects: true, | |
| }); | |
| const logGroup = new logs.LogGroup(stack, "KinesisLogGroup", { | |
| retention: RetentionDays.ONE_MONTH, | |
| removalPolicy: cdk.RemovalPolicy.DESTROY, | |
| }); | |
| const logStream = new logs.LogStream(stack, "KinesisLogStream", { | |
| logGroup: logGroup, | |
| removalPolicy: cdk.RemovalPolicy.DESTROY, | |
| }); | |
| const s3logGroup = new logs.LogGroup(stack, "S3WAFLogGroup", { | |
| retention: RetentionDays.ONE_MONTH, | |
| removalPolicy: cdk.RemovalPolicy.DESTROY, | |
| }); | |
| const s3logStream = new logs.LogStream(stack, "S3WAFLogStream", { | |
| logGroup: logGroup, | |
| removalPolicy: cdk.RemovalPolicy.DESTROY, | |
| }); | |
| const firehoseRole = new iam.Role(stack, "WAFFirehoseRole", { | |
| assumedBy: new iam.ServicePrincipal("firehose.amazonaws.com"), | |
| }); | |
| const firehouseManagedPolicy = new iam.ManagedPolicy( | |
| stack, | |
| "FirehouseManagedPolicy", | |
| { | |
| statements: [ | |
| new iam.PolicyStatement({ | |
| effect: iam.Effect.ALLOW, | |
| actions: ["es:*"], | |
| resources: [ | |
| `arn:aws:es:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:domain/${esDomainName}/*`, | |
| `arn:aws:es:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:domain/${esDomainName}`, | |
| ], | |
| }), | |
| new iam.PolicyStatement({ | |
| effect: iam.Effect.ALLOW, | |
| actions: [ | |
| "ec2:DescribeVpcs", | |
| "ec2:DescribeVpcAttribute", | |
| "ec2:DescribeSubnets", | |
| "ec2:DescribeSecurityGroups", | |
| "ec2:DescribeNetworkInterfaces", | |
| "ec2:CreateNetworkInterface", | |
| "ec2:CreateNetworkInterfacePermission", | |
| "ec2:DeleteNetworkInterface", | |
| ], | |
| resources: ["*"], | |
| }), | |
| new iam.PolicyStatement({ | |
| effect: iam.Effect.ALLOW, | |
| actions: ["logs:PutLogEvents"], | |
| resources: [logGroup.logGroupArn, s3logGroup.logGroupArn], | |
| }), | |
| new iam.PolicyStatement({ | |
| effect: iam.Effect.ALLOW, | |
| actions: ["lambda:InvokeFunction", "lambda:GetFunctionConfiguration"], | |
| resources: [ | |
| "arn:aws:lambda:us-east-1:884515407868:function:%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%", | |
| ], | |
| }), | |
| new iam.PolicyStatement({ | |
| effect: iam.Effect.ALLOW, | |
| actions: ["kms:GenerateDataKey", "kms:Decrypt"], | |
| resources: [ | |
| "arn:aws:kms:us-east-1:884515407868:key/%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%", | |
| ], | |
| conditions: { | |
| StringEquals: { | |
| "kms:ViaService": "s3.us-east-1.amazonaws.com", | |
| }, | |
| StringLike: { | |
| "kms:EncryptionContext:aws:s3:arn": [ | |
| "arn:aws:s3:::%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%/*", | |
| "arn:aws:s3:::%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%", | |
| ], | |
| }, | |
| }, | |
| }), | |
| new iam.PolicyStatement({ | |
| effect: iam.Effect.ALLOW, | |
| actions: [ | |
| "kinesis:DescribeStream", | |
| "kinesis:GetShardIterator", | |
| "kinesis:GetRecords", | |
| "kinesis:ListShards", | |
| ], | |
| resources: ["*"], | |
| }), | |
| new iam.PolicyStatement({ | |
| effect: iam.Effect.ALLOW, | |
| actions: ["kms:Decrypt"], | |
| resources: [ | |
| "arn:aws:kms:us-east-1:884515407868:key/%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%", | |
| ], | |
| conditions: { | |
| StringEquals: { | |
| "kms:ViaService": "kinesis.us-east-1.amazonaws.com", | |
| }, | |
| StringLike: { | |
| "kms:EncryptionContext:aws:kinesis:arn": | |
| "arn:aws:kinesis:us-east-1:884515407868:stream/%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%", | |
| }, | |
| }, | |
| }), | |
| ], | |
| } | |
| ); | |
| firehoseRole.addManagedPolicy(firehouseManagedPolicy); | |
| demoWAFBucket.grantReadWrite(firehoseRole); | |
| const wafDeliveryStream = new kinesisfirehose.CfnDeliveryStream( | |
| stack, | |
| "WafIaCDeliveryStream", | |
| { | |
| amazonopensearchserviceDestinationConfiguration: { | |
| domainArn: elasticDomain.attrArn, | |
| indexName: "aws-waf-iac-logs", | |
| indexRotationPeriod: "OneWeek", | |
| retryOptions: { | |
| durationInSeconds: 300, | |
| }, | |
| roleArn: firehoseRole.roleArn, | |
| s3BackupMode: "AllDocuments", | |
| s3Configuration: { | |
| bucketArn: demoWAFBucket.bucketArn, | |
| roleArn: firehoseRole.roleArn, | |
| prefix: "waf-logs-iac", | |
| bufferingHints: { | |
| intervalInSeconds: 60, | |
| sizeInMBs: 1, | |
| }, | |
| cloudWatchLoggingOptions: { | |
| enabled: true, | |
| logGroupName: s3logGroup.logGroupName, | |
| logStreamName: s3logStream.logStreamName, | |
| }, | |
| compressionFormat: "UNCOMPRESSED", | |
| }, | |
| vpcConfiguration: { | |
| roleArn: firehoseRole.roleArn, | |
| securityGroupIds: [openSG.securityGroupId], | |
| subnetIds: props.vpc.selectSubnets({ | |
| subnetGroupName: "Private", | |
| }).subnetIds, | |
| }, | |
| bufferingHints: { | |
| intervalInSeconds: 60, | |
| sizeInMBs: 1, | |
| }, | |
| cloudWatchLoggingOptions: { | |
| enabled: true, | |
| logGroupName: logGroup.logGroupName, | |
| logStreamName: logStream.logStreamName, | |
| }, | |
| }, | |
| deliveryStreamType: "DirectPut", | |
| deliveryStreamName: "aws-waf-logs-iac", | |
| } | |
| ); | |
| ///////////////////////////////////////////////////////////////////////////////////////// | |
| //Creating WAF ACL Logging | |
| ///////////////////////////////////////////////////////////////////////////////////////// | |
| const cfnLoggingConfiguration = new wafv2.CfnLoggingConfiguration( | |
| stack, | |
| "AclLoggingConfiguration", | |
| { | |
| logDestinationConfigs: [wafDeliveryStream.attrArn], | |
| resourceArn: demoWebACL.attrArn, | |
| } | |
| ); | |
| return demoWebACL; | |
| } | |
| view rawcreate-cloudfrontWAF.ts hosted with ❤ by GitHub |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment