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