├── output.tf ├── code ├── dev.zip ├── ipAutomation.zip ├── dev.py ├── data-ingestion-lambda.py └── ipAutomation.py ├── images ├── waf-offering-design.jpg ├── waf-offering-logging-option1.jpg ├── waf-offering-logging-option2.png └── waf-offering-logging-option3.jpg ├── modules └── vpc │ ├── 1-data.tf │ ├── 5-s3-endpoint.tf │ ├── output.tf │ ├── 3-flow-logs.tf │ ├── 4-ssm.tf │ ├── 0-variables.tf │ └── 2-vpc.tf ├── documentation └── AWS WAF deployment with AWS Firewall manager and Terraform.docx ├── CODE_OF_CONDUCT.md ├── user-data └── user-data.sh ├── html └── index.html ├── LICENSE ├── 3-aws-waf-rate-based.tf ├── 3-aws-waf-geo.tf ├── dashboard-crossaccount-kinesis.yaml ├── 6-optional-preprod-s3_origin.tf_ ├── 4-fwm-waf-logging-option3.tf ├── vars └── PROD.tfvars ├── 0-locals-data.tf ├── 0-providers.tf ├── dashboard-crossaccount-kinesis-role.yaml ├── 5-optional-dev-cfront.tf_ ├── CONTRIBUTING.md ├── 3-aws-waf-ip.tf ├── 3-aws-waf-rate-based.yaml ├── 1-fwm-regional-webacl.tf ├── 1-fwm-global-webacl.tf ├── 6-optional-preprod-cfront.tf_ ├── 6-optional-variables_cfront.tf ├── 5-optional-dev_apigw.tf_ ├── 2-aws-waf-automation-ip.tf ├── 3-aws-waf-regex.tf ├── 4-fwm-waf-logging-option2_global.tf ├── 4-fwm-waf-logging-option2_regional.tf ├── 4-fwm-waf-logging-option1_global.tf ├── 4-fwm-waf-logging-option1_regional.tf ├── 0-variables_firewall.tf ├── 3-aws-waf-sqli-xss.tf ├── README.md └── dashboard.yaml /output.tf: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/dev.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-waf-firewall-manager-terraform/HEAD/code/dev.zip -------------------------------------------------------------------------------- /code/ipAutomation.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-waf-firewall-manager-terraform/HEAD/code/ipAutomation.zip -------------------------------------------------------------------------------- /images/waf-offering-design.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-waf-firewall-manager-terraform/HEAD/images/waf-offering-design.jpg -------------------------------------------------------------------------------- /modules/vpc/1-data.tf: -------------------------------------------------------------------------------- 1 | 2 | data "aws_caller_identity" "current" {} 3 | data "aws_availability_zones" "available" {} 4 | data "aws_region" "current" {} -------------------------------------------------------------------------------- /images/waf-offering-logging-option1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-waf-firewall-manager-terraform/HEAD/images/waf-offering-logging-option1.jpg -------------------------------------------------------------------------------- /images/waf-offering-logging-option2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-waf-firewall-manager-terraform/HEAD/images/waf-offering-logging-option2.png -------------------------------------------------------------------------------- /images/waf-offering-logging-option3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-waf-firewall-manager-terraform/HEAD/images/waf-offering-logging-option3.jpg -------------------------------------------------------------------------------- /documentation/AWS WAF deployment with AWS Firewall manager and Terraform.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-waf-firewall-manager-terraform/HEAD/documentation/AWS WAF deployment with AWS Firewall manager and Terraform.docx -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /user-data/user-data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | sudo yum install -y https://s3.${Region}.amazonaws.com/amazon-ssm-${Region}/latest/linux_amd64/amazon-ssm-agent.rpm 5 | sudo systemctl status amazon-ssm-agent 6 | 7 | sudo systemctl enable amazon-ssm-agent 8 | sudo systemctl start amazon-ssm-agent 9 | sudo systemctl status amazon-ssm-agent -------------------------------------------------------------------------------- /code/dev.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import json 4 | import pprint 5 | 6 | #--- main lambda handler 7 | def handler(event, context): 8 | 9 | return { 10 | "statusCode": 200, 11 | "body": "waf poc lambda function", 12 | "headers": { 13 | "Content-Type": "application/json" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello World 4 | 5 | 6 | 34 | 35 | 36 |

Hello World

37 |

We are live with a private bucket (default example)!!!

38 | 39 | 40 | -------------------------------------------------------------------------------- /modules/vpc/5-s3-endpoint.tf: -------------------------------------------------------------------------------- 1 | #FOR S3 2 | data "aws_vpc_endpoint_service" "s3" { 3 | count = var.enable_ssm || var.enable_s3_endpoint ? 1 : 0 4 | service = "s3" 5 | service_type = "Gateway" 6 | 7 | } 8 | 9 | resource "aws_vpc_endpoint" "s3" { 10 | count = var.enable_ssm || var.enable_s3_endpoint ? 1 : 0 11 | vpc_id = aws_vpc.vpc.id 12 | service_name = data.aws_vpc_endpoint_service.s3[0].service_name 13 | tags = merge( 14 | var.tags, 15 | { 16 | "Name" = "${var.vpc_name}-S3-ENDPOINT" 17 | }, 18 | ) 19 | } 20 | 21 | #this must be created outside of the module 22 | #resource "aws_vpc_endpoint_route_table_association" "public_s3" { 23 | # count = var.enable_ssm ? 1 : 0 24 | # vpc_endpoint_id = aws_vpc_endpoint.s3[0].id 25 | # route_table_id = aws_route_table.subnet_{subnet-type}_route_table[1].id 26 | #} 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /3-aws-waf-rate-based.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | 10 | #-----use this resource to create your own Rate based rules ---------------------------- 11 | #rate-based-statement rules are not supported by terraform at the moment in rule groups, issue opened https://github.com/hashicorp/terraform-provider-aws/issues/21631 12 | 13 | resource "aws_cloudformation_stack" "aws_waf_rulegroup_ratebased" { 14 | provider = aws.global 15 | name = "ratebBasedRuleGroup" 16 | 17 | template_body = file("3-aws-waf-rate-based.yaml") 18 | capabilities = ["CAPABILITY_IAM", ] 19 | tags = merge( 20 | local.common_tags, 21 | { 22 | "Purpose" = "Cloudformation stack for aws_waf_rulegroup_ratebased" 23 | }, 24 | ) 25 | } 26 | 27 | -------------------------------------------------------------------------------- /3-aws-waf-geo.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | 10 | #-----use this resource to create your geo location rule ---------------------------- 11 | # Not part of AWS FM WAF policy by default 12 | resource "aws_wafv2_rule_group" "GeoRuleGroup" { 13 | count = var.create_waf_geo_rule_group ? 1 : 0 14 | provider = aws.regional 15 | name = "${var.regional_policy_name}-GeoRuleGroup" 16 | scope = "REGIONAL" 17 | capacity = 60 18 | 19 | rule { 20 | name = "GeoLocation-Based-BlockList" 21 | priority = 1 22 | 23 | action { 24 | block {} 25 | } 26 | 27 | statement { 28 | geo_match_statement { 29 | country_codes = var.waf_rule_geo_country_list 30 | } 31 | } 32 | 33 | visibility_config { 34 | cloudwatch_metrics_enabled = true 35 | metric_name = "GeoLocation-BlockList-metric" 36 | sampled_requests_enabled = true 37 | } 38 | } 39 | 40 | visibility_config { 41 | cloudwatch_metrics_enabled = true 42 | metric_name = "WAFCUSTGeoRuleGroupMetric" 43 | sampled_requests_enabled = true 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /dashboard-crossaccount-kinesis.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | 10 | AWSTemplateFormatVersion: "2010-09-09" 11 | Description: Kinesis deployment for AWS WAF Dashboard build 12 | 13 | Parameters: 14 | DestinationS3bucketARN: 15 | Type: String 16 | Description: ARN of the destinatio S3 bucket 17 | 18 | KinesisDeliveryRoleARN: 19 | Type: String 20 | Description: ARN of iam role for kinesis 21 | 22 | 23 | Resources: 24 | KinesisFirehoseDeliveryStream: 25 | Type: AWS::KinesisFirehose::DeliveryStream 26 | Properties: 27 | DeliveryStreamName: !Sub aws-waf-logs-${AWS::StackName}-delivery 28 | DeliveryStreamType: DirectPut 29 | ExtendedS3DestinationConfiguration: 30 | BucketARN: !Ref DestinationS3bucketARN 31 | RoleARN: !Ref KinesisDeliveryRoleARN 32 | BufferingHints: 33 | IntervalInSeconds: 60 34 | SizeInMBs: 5 35 | Prefix: waf-logs 36 | #EncryptionConfiguration: 37 | #CloudWatchLoggingOptions: 38 | # Enabled: 39 | # LogGroupName: 40 | # LogStreamName: 41 | 42 | 43 | Outputs: 44 | 45 | KinesisFirehoseDeliveryStreamArn: 46 | Description: kenesis Firehose 47 | Value: !GetAtt KinesisFirehoseDeliveryStream.Arn -------------------------------------------------------------------------------- /6-optional-preprod-s3_origin.tf_: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | 10 | data "aws_iam_policy_document" "s3_policy" { 11 | statement { 12 | actions = ["s3:GetObject"] 13 | resources = ["${aws_s3_bucket.preprod_web.arn}/*"] 14 | 15 | principals { 16 | type = "AWS" 17 | identifiers = [aws_cloudfront_origin_access_identity.preprod_origin_access_identity.iam_arn] 18 | } 19 | } 20 | } 21 | 22 | locals { 23 | bucket_name = "waf-poc-11111" 24 | } 25 | 26 | resource "aws_s3_bucket_policy" "preprod_web" { 27 | provider = aws.preprod 28 | bucket = aws_s3_bucket.preprod_web.id 29 | policy = data.aws_iam_policy_document.s3_policy.json 30 | } 31 | 32 | resource "aws_s3_bucket" "preprod_web" { 33 | provider = aws.preprod 34 | bucket = local.bucket_name 35 | acl = "private" 36 | 37 | tags = merge( 38 | { 39 | "Name" = format("%s", "Bucket for CloudFront ${var.environment}") 40 | }, 41 | { 42 | "Environment" = format("%s", var.environment) 43 | }, 44 | local.common_tags, 45 | ) 46 | 47 | versioning { 48 | enabled = true 49 | } 50 | 51 | lifecycle_rule { 52 | id = "expire-old-versions" 53 | enabled = true 54 | 55 | noncurrent_version_expiration { 56 | days = 7 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /4-fwm-waf-logging-option3.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | 10 | resource "aws_cloudformation_stack" "dashboards-option3-regional" { 11 | count = var.logging_option == "option3" ? 1 : 0 12 | provider = aws.regional 13 | name = "security-waf-dashboards-regional" 14 | 15 | parameters = { 16 | UserEmail = var.ESUserEmail, 17 | DataNodeEBSVolumeSize = var.DataNodeEBSVolumeSize, 18 | ESConfigBucket = var.kinesis_destination_s3_name 19 | } 20 | 21 | template_body = file("dashboard.yaml") 22 | capabilities = ["CAPABILITY_IAM", ] 23 | tags = merge( 24 | local.common_tags, 25 | { 26 | "Purpose" = "Cloudformation stack for waf dashboards" 27 | }, 28 | ) 29 | } 30 | 31 | resource "aws_cloudformation_stack" "dashboards-option3-global" { 32 | count = var.logging_option == "option3" ? 1 : 0 33 | provider = aws.global 34 | name = "security-waf-dashboards-global" 35 | 36 | parameters = { 37 | UserEmail = var.ESUserEmail, 38 | DataNodeEBSVolumeSize = var.DataNodeEBSVolumeSize, 39 | ESConfigBucket = var.kinesis_destination_s3_name 40 | } 41 | 42 | template_body = file("dashboard.yaml") 43 | capabilities = ["CAPABILITY_IAM", ] 44 | tags = merge( 45 | local.common_tags, 46 | { 47 | "Purpose" = "Cloudformation stack for waf dashboards" 48 | }, 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /vars/PROD.tfvars: -------------------------------------------------------------------------------- 1 | #------------Deployment 2 | master_region = "eu-west-1" 3 | application_region = "eu-west-1" 4 | create_global_fms_waf_policy = true 5 | create_regional_fms_waf_policy = true 6 | create_waf_geo_rule_group = true 7 | create_waf_regex_rule_group = false 8 | create_waf_sqli_rule_group = false 9 | create_waf_xss_rule_group = false 10 | create_waf_ip_rule_group = false 11 | logging_option = "option1" 12 | 13 | #------------Firewall manager - policies 14 | global_policy_name = "PoCCFrontPolicy_global" 15 | global_policy_exclude_resource_tags = false 16 | global_policy_remediation_enabled = true 17 | global_policy_orgunit_list = ["ou-xxxxx", "ou-xxxx"] #please customize 18 | global_policy_resource_tags = { 19 | "AWS_WAF" = "Enabled", 20 | "Environment" = "poc", 21 | "Type" = "edge" 22 | } 23 | global_policy_overrideCustomerWebACLAssociation = true 24 | global_policy_default_action = "ALLOW" 25 | regional_policy_name = "ApplicationPoCPolicy" 26 | regional_policy_exclude_resource_tags = false 27 | regional_policy_remediation_enabled = false 28 | regional_policy_orgunit_list = ["ou-xxxxx", "ou-xxxx"] #please customize 29 | regional_policy_resource_tags = { 30 | "AWS_WAF" = "Enabled", 31 | "Environment" = "poc", 32 | "Type" = "edge" 33 | } 34 | regional_policy_overrideCustomerWebACLAssociation = true 35 | regional_policy_default_action = "ALLOW" 36 | 37 | #------------Firewall manager - logging 38 | kinesis_firehose_name = "aws-waf-logs-poc" 39 | kinesis_destination_s3_name = "test-waf-logs-xxxx" 40 | kinesis_prefix = "waf-logs" 41 | s3_delivery_buffer_interval = 60 42 | s3_delivery_buffer_size = 5 43 | DataNodeEBSVolumeSize = 500 44 | ESConfigBucket = "waf-dash-xxxxxx" 45 | ESUserEmail = "adf@amazon.com" 46 | ES_SSH_tunnel_allowed_CIDR = ["192.168.0.21/32"] #please customize -------------------------------------------------------------------------------- /0-locals-data.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | 10 | locals { 11 | common_tags = { 12 | Environment = var.environment 13 | Project = var.Project 14 | Owner = var.Owner 15 | managedBy = "terraform" 16 | } 17 | #https://docs.aws.amazon.com/redshift/latest/mgmt/db-auditing.html 18 | redshift_logging_region_account_id_bucket_policy = { 19 | "us-east-1" : "193672423079", 20 | "us-east-2" : "391106570357", 21 | "us-west-1" : "262260360010", 22 | "us-west-2" : "902366379725", 23 | "af-south-1" : "365689465814", 24 | "ap-east-1" : "313564881002", 25 | "ap-south-1" : "865932855811", 26 | "ap-northeast-3" : "090321488786", 27 | "ap-northeast-2" : "760740231472", 28 | "ap-southeast-1" : "361669875840", 29 | "ap-southeast-2" : "762762565011", 30 | "ap-northeast-1" : "404641285394", 31 | "ca-central-1" : "907379612154", 32 | "eu-central-1" : "053454850223", 33 | "eu-west-1" : "210876761215", 34 | "eu-west-2" : "307160386991", 35 | "eu-south-1" : "945612479654", 36 | "eu-west-3" : "915173422425", 37 | "eu-north-1" : "729911121831", 38 | "me-south-1" : "013126148197", 39 | "sa-east-1" : "075028567923" 40 | } 41 | cfront_tags = { 42 | AWS_WAF = "Enabled", 43 | Environment = "poc", 44 | Type = "edge" 45 | } 46 | 47 | app_tags = { 48 | AWS_WAF = "Enabled", 49 | Environment = "poc", 50 | Type = "application" 51 | } 52 | 53 | } 54 | 55 | data "aws_region" "region" {} 56 | data "aws_caller_identity" "current" {} 57 | data "aws_caller_identity" "security" { 58 | provider = aws.regional 59 | } 60 | data "aws_availability_zones" "available" {} 61 | 62 | data "aws_caller_identity" "us-east-1" { 63 | provider = aws.global 64 | } 65 | 66 | data "aws_region" "us-east-1" { 67 | provider = aws.global 68 | } 69 | 70 | data "aws_region" "security" { 71 | provider = aws.regional 72 | } 73 | -------------------------------------------------------------------------------- /0-providers.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | 10 | #master accounr 11 | provider "aws" { 12 | region = var.master_region 13 | } 14 | 15 | #network firewall admin account - Global scope 16 | provider "aws" { 17 | region = "us-east-1" 18 | alias = "global" 19 | assume_role { 20 | role_arn = "arn:aws:iam::{network firewall admin account}:role/{role}" #customize 21 | external_id = "my_external_id" 22 | } 23 | } 24 | 25 | 26 | #network firewall admin account - Regional scope 27 | provider "aws" { 28 | region = var.application_region 29 | alias = "regional" 30 | assume_role { 31 | role_arn = "arn:aws:iam::{network firewall admin account}:role/{role}" #customize 32 | external_id = "my_external_id" 33 | } 34 | } 35 | 36 | #logging account 37 | provider "aws" { 38 | region = var.application_region 39 | alias = "logging" 40 | assume_role { 41 | role_arn = "arn:aws:iam::{logging account}:role/{role}" #customize 42 | external_id = "my_external_id" 43 | } 44 | } 45 | 46 | #logging account 47 | provider "aws" { 48 | region = "us-east-1" 49 | alias = "logging-global" 50 | assume_role { 51 | role_arn = "arn:aws:iam::215097317823:role/{role}" #customize 52 | external_id = "my_external_id" 53 | } 54 | } 55 | 56 | #For testing purposes... 57 | provider "aws" { 58 | region = var.application_region 59 | alias = "dev" 60 | assume_role { 61 | role_arn = "arn:aws:iam::299537{logging account}281981:role/{role}" 62 | external_id = "my_external_id" 63 | } 64 | } 65 | 66 | #For testing purposes... 67 | provider "aws" { 68 | region = var.application_region 69 | alias = "preprod" 70 | assume_role { 71 | role_arn = "arn:aws:iam::{pre prod account}:role/{role}" 72 | external_id = "my_external_id" 73 | } 74 | } 75 | 76 | terraform { 77 | required_providers { 78 | aws = { 79 | source = "hashicorp/aws" 80 | version = "3.63.0" 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /modules/vpc/output.tf: -------------------------------------------------------------------------------- 1 | output "vpc_id" { 2 | value = aws_vpc.vpc.id 3 | } 4 | 5 | output "subnet_priv_ids" { 6 | value = aws_subnet.subnet_priv.*.id 7 | } 8 | 9 | output "subnet_public_ids" { 10 | value = aws_subnet.subnet_public.*.id 11 | } 12 | 13 | output "subnet_priv_tgw_ids" { 14 | value = aws_subnet.subnet_priv_tgw.*.id 15 | } 16 | 17 | output "subnet_fw_ids" { 18 | value = aws_subnet.subnet_fw.*.id 19 | } 20 | 21 | output "subnet_web_ids" { 22 | value = aws_subnet.subnet_web_tier.*.id 23 | } 24 | 25 | output "subnet_presentation_ids" { 26 | value = aws_subnet.subnet_pres_tier.*.id 27 | } 28 | 29 | output "subnet_database_ids" { 30 | value = aws_subnet.subnet_database_tier.*.id 31 | } 32 | 33 | output "subnet_outposts_ids" { 34 | value = aws_subnet.subnet_outposts.*.id 35 | } 36 | 37 | 38 | output "subnet_public_route_table_ids" { 39 | value = aws_route_table.subnet_public_route_table.*.id 40 | } 41 | 42 | output "subnet_priv_route_table_ids" { 43 | value = aws_route_table.subnet_priv_route_table.*.id 44 | } 45 | 46 | output "subnet_priv_tgw_route_table_ids" { 47 | value = aws_route_table.subnet_priv_tgw_route_table.*.id 48 | } 49 | 50 | output "subnet_fw_route_table_ids" { 51 | value = aws_route_table.subnet_fw_route_table.*.id 52 | } 53 | 54 | output "subnet_web_tier_route_table_ids" { 55 | value = aws_route_table.subnet_web_tier_route_table.*.id 56 | } 57 | 58 | output "subnet_pres_tier_route_table_ids" { 59 | value = aws_route_table.subnet_pres_tier_route_table.*.id 60 | } 61 | 62 | output "subnet_database_tier_route_table_ids" { 63 | value = aws_route_table.subnet_database_tier_route_table.*.id 64 | } 65 | 66 | output "subnet_outposts_route_table_ids" { 67 | value = aws_route_table.subnet_outposts_route_table.*.id 68 | } 69 | 70 | output "iam_role_ec2_ssm_id"{ 71 | value = var.enable_ssm && var.create_iam_role_ssm ? aws_iam_role.ssm_ec2_iam_role[0].id : "" 72 | depends_on = [ 73 | aws_iam_role.ssm_ec2_iam_role, 74 | ] 75 | } 76 | 77 | output "iam_instance_profile_ec2_ssm_id"{ 78 | value = var.enable_ssm && var.create_iam_role_ssm ? aws_iam_instance_profile.ec2_ssm[0].id : "" 79 | depends_on = [ 80 | aws_iam_role.ssm_ec2_iam_role, 81 | ] 82 | } 83 | 84 | output "ssm_security_group_id"{ 85 | value = var.enable_ssm ? aws_security_group.ssm_endpoint_sg[0].id : "" 86 | } 87 | 88 | output "igw_id"{ 89 | value = var.enable_internet_gateway && length(var.public_subnets_cidr_list) > 0 ? aws_internet_gateway.igw[0].id : "" 90 | } 91 | 92 | output "s3_vpc_endpoint"{ 93 | value = var.enable_ssm || var.enable_s3_endpoint ? aws_vpc_endpoint.s3[0].id : "" 94 | } 95 | -------------------------------------------------------------------------------- /dashboard-crossaccount-kinesis-role.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | AWSTemplateFormatVersion: "2010-09-09" 10 | Description: Kinesis deployment Role for AWS WAF Dashboard build 11 | 12 | Parameters: 13 | S3Bucket: 14 | Type: String 15 | Description: Name of the destinatio S3 bucket 16 | 17 | 18 | Resources: 19 | 20 | DeliveryStreamRole: 21 | Type: AWS::IAM::Role 22 | Properties: 23 | RoleName: kinesis-delivery-role 24 | AssumeRolePolicyDocument: 25 | Version: 2012-10-17 26 | Statement: 27 | - Effect: Allow 28 | Action: sts:AssumeRole 29 | Principal: 30 | Service: firehose.amazonaws.com 31 | Policies: 32 | - PolicyName: DeliveryStreamRolePolicy 33 | PolicyDocument: 34 | Version: 2012-10-17 35 | Statement: 36 | - Effect: Allow 37 | Action: 38 | - s3:AbortMultipartUpload 39 | - s3:GetBucketLocation 40 | - s3:GetObject 41 | - s3:ListBucket 42 | - s3:ListBucketMultipartUploads 43 | - s3:PutObject 44 | - s3:PutObjectAcl 45 | Resource: 46 | - !Sub arn:aws:s3:::${S3Bucket} 47 | - !Sub arn:aws:s3:::${S3Bucket}/* 48 | - Effect: Allow 49 | Action: 50 | - logs:PutLogEvents 51 | Resource: "*" 52 | - Effect: Allow 53 | Action: 54 | - kinesis:DescribeStream 55 | - kinesis:GetShardIterator 56 | - kinesis:GetRecords 57 | - kinesis:ListShards 58 | Resource: !Sub arn:aws:kinesis:${AWS::Region}:${AWS::AccountId}:stream/%FIREHOSE_STREAM_NAME% 59 | #- Effect: Allow 60 | # Action: 61 | # - kms:Decrypt 62 | # - kms:GenerateDataKey 63 | # Resource: !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}-id:key/key-id 64 | # Condition: 65 | # StringEquals: 66 | # "kms:ViaService": !Ref s3.region.amazonaws.com 67 | # StringLike: 68 | # "kms:EncryptionContext:aws:s3:arn": "arn:aws:s3:::bucket-name/prefix*" 69 | 70 | Outputs: 71 | 72 | KinesisFirehoseDeliveryRoleArn: 73 | Description: kenesis Firehose 74 | Value: !GetAtt DeliveryStreamRole.Arn -------------------------------------------------------------------------------- /5-optional-dev-cfront.tf_: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | 10 | resource "aws_cloudfront_origin_access_identity" "dev_origin_access_identity" { 11 | provider = aws.dev 12 | comment = "waf poc" 13 | } 14 | 15 | resource "aws_cloudfront_distribution" "dev" { 16 | provider = aws.dev 17 | #default_root_object = var.default_root_object 18 | enabled = true 19 | is_ipv6_enabled = var.is_ipv6_enabled 20 | price_class = "PriceClass_All" 21 | retain_on_delete = var.retain_on_delete 22 | wait_for_deployment = var.wait_for_deployment 23 | #web_acl_id = var.web_acl_id 24 | 25 | /* dynamic "logging_config" { 26 | for_each = length(keys(var.logging_config)) == 0 ? [] : [var.logging_config] 27 | 28 | content { 29 | bucket = logging_config.value["bucket"] 30 | prefix = lookup(logging_config.value, "prefix", null) 31 | include_cookies = lookup(logging_config.value, "include_cookies", null) 32 | } 33 | } */ 34 | 35 | origin { 36 | domain_name = "${aws_api_gateway_rest_api.apigw.id}.execute-api.eu-west-1.amazonaws.com" 37 | origin_id = "apigw" 38 | origin_path = "/dev" 39 | 40 | custom_origin_config { 41 | http_port = 80 42 | https_port = 443 43 | origin_protocol_policy = "https-only" 44 | origin_ssl_protocols = ["TLSv1", "TLSv1.1"] 45 | } 46 | 47 | /* dynamic "custom_header" { 48 | for_each = lookup(origin.value, "custom_header", []) 49 | content { 50 | name = custom_header.value.name 51 | value = custom_header.value.value 52 | } 53 | } */ 54 | 55 | } 56 | 57 | 58 | default_cache_behavior { 59 | target_origin_id = "apigw" 60 | viewer_protocol_policy = "https-only" 61 | 62 | allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] 63 | cached_methods = ["GET", "HEAD", "OPTIONS"] 64 | compress = true 65 | #field_level_encryption_id = lookup(i.value, "field_level_encryption_id", null) 66 | min_ttl = 0 67 | default_ttl = 3600 68 | max_ttl = 86400 69 | 70 | forwarded_values { 71 | query_string = true 72 | headers = ["Accept", "Referer", "Authorization", "Content-Type"] 73 | cookies { 74 | forward = "all" 75 | } 76 | } 77 | 78 | } 79 | 80 | 81 | viewer_certificate { 82 | cloudfront_default_certificate = true 83 | } 84 | 85 | restrictions { 86 | geo_restriction { 87 | restriction_type = "none" 88 | } 89 | } 90 | 91 | tags = merge( 92 | { 93 | "Name" = format("%s", "CloudFront ${var.environment}") 94 | }, 95 | { 96 | "Environment" = format("%s", var.environment) 97 | }, 98 | local.cfront_tags, 99 | local.common_tags, 100 | ) 101 | 102 | lifecycle { 103 | ignore_changes = [ 104 | web_acl_id, 105 | ] 106 | } 107 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /3-aws-waf-ip.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | 10 | #-----use this resource to create your own IP Set and AWS WAF IP Rule ---------------------------- 11 | # Not part of AWS FM WAF policy by default 12 | resource "aws_wafv2_ip_set" "AllowListsSet" { 13 | provider = aws.regional 14 | for_each = var.allow_lists_set 15 | name = "${var.regional_policy_name}-Allowt_${var.allow_lists_set[each.key]["ip_address_version"]}" 16 | scope = "REGIONAL" 17 | ip_address_version = var.allow_lists_set[each.key]["ip_address_version"] 18 | addresses = var.allow_lists_set[each.key]["addresses"] 19 | 20 | tags = local.common_tags 21 | } 22 | 23 | resource "aws_wafv2_ip_set" "BlockListsSet" { 24 | provider = aws.regional 25 | for_each = var.block_lists_set 26 | name = "${var.regional_policy_name}-Block_${var.block_lists_set[each.key]["ip_address_version"]}" 27 | scope = "REGIONAL" 28 | ip_address_version = var.block_lists_set[each.key]["ip_address_version"] 29 | addresses = var.block_lists_set[each.key]["addresses"] 30 | 31 | tags = local.common_tags 32 | } 33 | 34 | # Not part of AWS FM WAF policy by default 35 | resource "aws_wafv2_rule_group" "IPRuleGroup" { 36 | count = var.create_waf_ip_rule_group ? 1 : 0 37 | provider = aws.regional 38 | name = "${var.regional_policy_name}-IP" 39 | scope = "REGIONAL" 40 | capacity = 60 41 | description = "IP Rule Group" 42 | 43 | visibility_config { 44 | cloudwatch_metrics_enabled = true 45 | metric_name = "WAFCUSTIPRuleGroupMetric" 46 | sampled_requests_enabled = true 47 | } 48 | 49 | rule { 50 | name = "IPAllowListsRule" 51 | priority = 3 52 | 53 | action { 54 | allow {} 55 | } 56 | 57 | statement { 58 | 59 | or_statement { 60 | dynamic "statement" { 61 | for_each = aws_wafv2_ip_set.AllowListsSet 62 | content { 63 | ip_set_reference_statement { 64 | arn = aws_wafv2_ip_set.AllowListsSet[statement.key].arn 65 | } 66 | } 67 | } 68 | } 69 | } 70 | visibility_config { 71 | cloudwatch_metrics_enabled = true 72 | metric_name = "IPAllowListsRule-metric" 73 | sampled_requests_enabled = true 74 | } 75 | } 76 | 77 | rule { 78 | name = "IPBlockListsRule" 79 | priority = 1 80 | 81 | action { 82 | block {} 83 | } 84 | 85 | statement { 86 | 87 | or_statement { 88 | dynamic "statement" { 89 | for_each = aws_wafv2_ip_set.BlockListsSet 90 | content { 91 | ip_set_reference_statement { 92 | arn = aws_wafv2_ip_set.BlockListsSet[statement.key].arn 93 | } 94 | } 95 | } 96 | } 97 | } 98 | visibility_config { 99 | cloudwatch_metrics_enabled = true 100 | metric_name = "IPBlockListsRule-metric" 101 | sampled_requests_enabled = true 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /modules/vpc/3-flow-logs.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | # Only create flow log if user selected to create a VPC as well 3 | enable_flow_log = var.enable_flow_log 4 | 5 | create_flow_log_cloudwatch_iam_role = local.enable_flow_log && var.flow_log_destination_type != "s3" && var.create_flow_log_cloudwatch_iam_role 6 | create_flow_log_cloudwatch_log_group = local.enable_flow_log && var.flow_log_destination_type != "s3" && var.create_flow_log_cloudwatch_log_group 7 | 8 | flow_log_destination_arn = local.create_flow_log_cloudwatch_log_group ? aws_cloudwatch_log_group.flow_log[0].arn : var.flow_log_destination_arn 9 | flow_log_iam_role_arn = var.flow_log_destination_type != "s3" && local.create_flow_log_cloudwatch_iam_role ? aws_iam_role.vpc_flow_log_iam_role[0].arn : var.flow_log_cloudwatch_iam_role_arn 10 | } 11 | 12 | ################### 13 | # Flow Log 14 | ################### 15 | resource "aws_flow_log" "this" { 16 | count = local.enable_flow_log ? 1 : 0 17 | 18 | log_destination_type = var.flow_log_destination_type 19 | log_destination = local.flow_log_destination_arn 20 | log_format = var.flow_log_log_format 21 | iam_role_arn = local.flow_log_iam_role_arn 22 | traffic_type = var.flow_log_traffic_type 23 | vpc_id = aws_vpc.vpc.id 24 | 25 | } 26 | 27 | ##################### 28 | # Flow Log CloudWatch 29 | ##################### 30 | resource "aws_cloudwatch_log_group" "flow_log" { 31 | count = local.create_flow_log_cloudwatch_log_group ? 1 : 0 32 | name = "${var.vpc_name}-vpc-flow-logs" 33 | retention_in_days = var.flow_log_cloudwatch_log_group_retention_in_days 34 | 35 | tags = merge( 36 | var.tags, 37 | map( 38 | "Purpose", "VPC Flow Log CloudWatch Log Group for ${var.vpc_name}" 39 | ) 40 | ) 41 | } 42 | 43 | ######################### 44 | # Flow Log CloudWatch IAM 45 | ######################### 46 | resource "aws_iam_role" "vpc_flow_log_iam_role" { 47 | count = local.create_flow_log_cloudwatch_iam_role ? 1 : 0 48 | name = "${var.vpc_name}-vpcflowlogs-cw-role" 49 | 50 | assume_role_policy = jsonencode( 51 | { 52 | "Version": "2012-10-17", 53 | "Statement": [ 54 | { 55 | "Sid": "FlowLogsAssumeRole", 56 | "Effect": "Allow", 57 | "Principal": { 58 | "Service": "vpc-flow-logs.amazonaws.com" 59 | }, 60 | "Action": "sts:AssumeRole" 61 | } 62 | ] 63 | } 64 | ) 65 | 66 | tags = merge( 67 | var.tags, 68 | map( 69 | "Purpose", "IAM role to allow VPC Flow Logs to publish to CloudWatch Logs" 70 | ) 71 | ) 72 | } 73 | 74 | 75 | 76 | resource "aws_iam_role_policy" "vpc_flow_log_iam_policy" { 77 | count = local.create_flow_log_cloudwatch_iam_role ? 1 : 0 78 | name = "${var.vpc_name}-vpcflowlogs-cloudwatch-log-policy" 79 | role = aws_iam_role.vpc_flow_log_iam_role[0].id 80 | 81 | policy = jsonencode( 82 | { 83 | "Version": "2012-10-17", 84 | "Statement": [ 85 | { 86 | "Sid": "CreateAndPutLogs", 87 | "Effect": "Allow", 88 | "Action": [ 89 | "logs:CreateLogGroup", 90 | "logs:CreateLogStream", 91 | "logs:PutLogEvents", 92 | "logs:DescribeLogGroups", 93 | "logs:DescribeLogStreams" 94 | ], 95 | "Resource": "*" 96 | } 97 | ] 98 | } 99 | ) 100 | } -------------------------------------------------------------------------------- /3-aws-waf-rate-based.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | 10 | AWSTemplateFormatVersion: "2010-09-09" 11 | Description: template to create AWS WAF v2 Rule group with rate based rule 12 | 13 | Parameters: 14 | HTTPGetFloodRateParam: 15 | Description: "A Rate-Based Rule looking for a maximum rate of a single IP address can perform HTTP Get requests in 5 minutes" 16 | Type: Number 17 | Default: 10000 18 | MinValue: 100 19 | MaxValue: 20000000 20 | 21 | HTTPPostLoginParam: 22 | Description: "Enter the URI for a Login page to rate-limit IP addresses from login attemps. Leave this blank for universal HTTP Post rate-limit" 23 | Type: String 24 | Default: login 25 | 26 | Resources: 27 | SampleRuleGroup: 28 | Type: 'AWS::WAFv2::RuleGroup' 29 | Properties: 30 | Name: HTTP-FloodP-Protection 31 | Scope: CLOUDFRONT 32 | Description: HTTPFloodPProtection 33 | VisibilityConfig: 34 | SampledRequestsEnabled: true 35 | CloudWatchMetricsEnabled: true 36 | MetricName: SampleRateBasedRuleGroupMetrics 37 | Capacity: 100 38 | Rules: 39 | - Name: HTTPFloodPProtection 40 | Priority: 0 41 | Action: 42 | Block: {} 43 | VisibilityConfig: 44 | SampledRequestsEnabled: true 45 | CloudWatchMetricsEnabled: true 46 | MetricName: RateBasedGet 47 | Statement: 48 | RateBasedStatement: 49 | AggregateKeyType: "IP" 50 | Limit: !Ref HTTPGetFloodRateParam 51 | ScopeDownStatement: 52 | ByteMatchStatement: 53 | FieldToMatch: 54 | Method: {} 55 | PositionalConstraint: EXACTLY 56 | SearchString: get 57 | TextTransformations: 58 | - Priority: 0 59 | Type: LOWERCASE 60 | - Name: HTTPPostFloodPProtection 61 | Priority: 1 62 | Action: 63 | Block: {} 64 | VisibilityConfig: 65 | SampledRequestsEnabled: true 66 | CloudWatchMetricsEnabled: true 67 | MetricName: RateBasedPost 68 | Statement: 69 | RateBasedStatement: 70 | AggregateKeyType: "IP" 71 | Limit: !Ref HTTPGetFloodRateParam 72 | ScopeDownStatement: 73 | AndStatement: 74 | Statements: 75 | - ByteMatchStatement: 76 | FieldToMatch: 77 | UriPath: {} 78 | PositionalConstraint: CONTAINS 79 | SearchString: !Ref HTTPPostLoginParam 80 | TextTransformations: 81 | - Priority: 0 82 | Type: LOWERCASE 83 | - Priority: 1 84 | Type: URL_DECODE 85 | - ByteMatchStatement: 86 | SearchString: post 87 | FieldToMatch: 88 | Method: {} 89 | TextTransformations: 90 | - Priority: 0 91 | Type: LOWERCASE 92 | PositionalConstraint: EXACTLY 93 | Outputs: 94 | RuleGorupARN: 95 | Description: RuleGroup Arn 96 | Value: !GetAtt SampleRuleGroup.Arn 97 | -------------------------------------------------------------------------------- /1-fwm-regional-webacl.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | 10 | #Application layer policy 11 | #check https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html 12 | #For web app: AWS managed SQL injection, Linux, PHP + custom rules like regex 13 | #------------------------Please customize with YOUR rules-------------------- 14 | resource "aws_fms_policy" "NonProdApplicationPolicy_regional" { 15 | count = var.create_regional_fms_waf_policy ? 1 : 0 16 | provider = aws.regional 17 | name = var.regional_policy_name 18 | exclude_resource_tags = var.regional_policy_exclude_resource_tags 19 | remediation_enabled = var.regional_policy_remediation_enabled 20 | resource_type_list = ["AWS::ElasticLoadBalancingV2::LoadBalancer", "AWS::ApiGateway::Stage"] 21 | include_map { 22 | orgunit = var.regional_policy_orgunit_list 23 | } 24 | 25 | resource_tags = length(var.regional_policy_resource_tags) == 0 ? null : var.regional_policy_resource_tags 26 | security_service_policy_data { 27 | type = "WAFV2" 28 | 29 | managed_service_data = jsonencode({ 30 | type = "WAFV2", 31 | preProcessRuleGroups = [ 32 | { 33 | managedRuleGroupIdentifier = { 34 | vendorName = "AWS", 35 | managedRuleGroupName = "AWSManagedRulesSQLiRuleSet" 36 | }, 37 | ruleGroupType = "ManagedRuleGroup", 38 | ruleGroupArn = null, 39 | overrideAction = { 40 | type = "NONE" 41 | } }, 42 | { 43 | managedRuleGroupIdentifier = { 44 | vendorName = "AWS", 45 | managedRuleGroupName = "AWSManagedRulesLinuxRuleSet" 46 | }, 47 | ruleGroupType = "ManagedRuleGroup", 48 | ruleGroupArn = null, 49 | overrideAction = { 50 | type = "NONE" 51 | } }, 52 | { 53 | managedRuleGroupIdentifier = { 54 | vendorName = "AWS", 55 | managedRuleGroupName = "AWSManagedRulesUnixRuleSet" 56 | }, 57 | ruleGroupType = "ManagedRuleGroup", 58 | ruleGroupArn = null, 59 | overrideAction = { 60 | type = "NONE" 61 | } }, 62 | { 63 | managedRuleGroupIdentifier = { 64 | vendorName = "AWS", 65 | managedRuleGroupName = "AWSManagedRulesPHPRuleSet" 66 | }, 67 | ruleGroupType = "ManagedRuleGroup", 68 | ruleGroupArn = null, 69 | overrideAction = { 70 | type = "NONE" 71 | } }, 72 | { 73 | managedRuleGroupIdentifier = { 74 | vendorName = "AWS", 75 | managedRuleGroupName = "AWSManagedRulesWordPressRuleSet" 76 | }, 77 | ruleGroupType = "ManagedRuleGroup", 78 | ruleGroupArn = null, 79 | overrideAction = { 80 | type = "NONE" 81 | } }, 82 | { 83 | ruleGroupType = "RuleGroup", 84 | ruleGroupArn = aws_wafv2_rule_group.allowCloudfrontIP.arn, 85 | overrideAction = { 86 | type = "NONE" 87 | } }], 88 | postProcessRuleGroups = [], 89 | overrideCustomerWebACLAssociation = var.regional_policy_overrideCustomerWebACLAssociation, 90 | defaultAction = { 91 | type = var.regional_policy_default_action 92 | }, 93 | ruleGroups = [], 94 | loggingConfiguration = { 95 | logDestinationConfigs = var.logging_option == "option1" ? [aws_kinesis_firehose_delivery_stream.WAFKinesisFirehose_regional[0].arn] : var.logging_option == "option2" ? aws_cloudformation_stack.dashboards_private_kinesis[0].outputs["KinesisFirehoseDeliveryStreamArn"] : var.logging_option == "option3" ? aws_cloudformation_stack.dashboards-option3-regional[0].outputs["KinesisFirehoseDeliveryStreamArn"] : null 96 | } 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /1-fwm-global-webacl.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | 10 | #Edge Network WAF 11 | #check https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html 12 | # AWS managed rules Core Rule Set, geographical location (custom), AWS Managed IP reputation, AWS managed anonymous IP*, knwon bad inputs set, ip block/allow set (custom) 13 | #------------------------Please customize with YOUR rules-------------------- 14 | resource "aws_fms_policy" "NonProdCFrontPolicy_global" { 15 | count = var.create_global_fms_waf_policy ? 1 : 0 16 | provider = aws.global 17 | name = var.global_policy_name 18 | exclude_resource_tags = var.global_policy_exclude_resource_tags 19 | remediation_enabled = var.global_policy_remediation_enabled 20 | resource_type = "AWS::CloudFront::Distribution" 21 | include_map { 22 | orgunit = var.global_policy_orgunit_list 23 | } 24 | 25 | resource_tags = length(var.global_policy_resource_tags) == 0 ? null : var.global_policy_resource_tags 26 | 27 | security_service_policy_data { 28 | type = "WAFV2" 29 | 30 | managed_service_data = jsonencode({ 31 | type = "WAFV2", 32 | preProcessRuleGroups = [ 33 | { 34 | managedRuleGroupIdentifier = { 35 | vendorName = "AWS", 36 | managedRuleGroupName = "AWSManagedRulesAmazonIpReputationList" 37 | }, 38 | ruleGroupType = "ManagedRuleGroup", 39 | ruleGroupArn = null, 40 | overrideAction = { 41 | type = "NONE" 42 | } }, 43 | { 44 | managedRuleGroupIdentifier = { 45 | vendorName = "AWS", 46 | managedRuleGroupName = "AWSManagedRulesCommonRuleSet" 47 | }, 48 | ruleGroupType = "ManagedRuleGroup", 49 | ruleGroupArn = null, 50 | overrideAction = { 51 | type = "NONE" 52 | } }, 53 | { 54 | managedRuleGroupIdentifier = { 55 | vendorName = "AWS", 56 | managedRuleGroupName = "AWSManagedRulesKnownBadInputsRuleSet" 57 | }, 58 | ruleGroupType = "ManagedRuleGroup", 59 | ruleGroupArn = null, 60 | overrideAction = { 61 | type = "NONE" 62 | } }, 63 | { 64 | managedRuleGroupIdentifier = { 65 | vendorName = "AWS", 66 | managedRuleGroupName = "AWSManagedRulesAdminProtectionRuleSet" 67 | }, 68 | ruleGroupType = "ManagedRuleGroup", 69 | ruleGroupArn = null, 70 | overrideAction = { 71 | type = "NONE" 72 | } }, 73 | { 74 | managedRuleGroupIdentifier = { 75 | vendorName = "AWS", 76 | managedRuleGroupName = "AWSManagedRulesBotControlRuleSet" 77 | }, 78 | ruleGroupType = "ManagedRuleGroup", 79 | ruleGroupArn = null, 80 | overrideAction = { 81 | type = "COUNT" 82 | } }, 83 | { 84 | ruleGroupType = "RuleGroup", 85 | ruleGroupArn = aws_cloudformation_stack.aws_waf_rulegroup_ratebased.outputs["RuleGorupARN"], 86 | overrideAction = { 87 | type = "NONE" 88 | } }], 89 | postProcessRuleGroups = [], 90 | overrideCustomerWebACLAssociation = var.global_policy_overrideCustomerWebACLAssociation, 91 | defaultAction = { 92 | type = var.global_policy_default_action 93 | }, 94 | ruleGroups = [], 95 | loggingConfiguration = { 96 | logDestinationConfigs = var.logging_option == "option1" ? [aws_kinesis_firehose_delivery_stream.WAFKinesisFirehose_global[0].arn] : var.logging_option == "option2" ? aws_cloudformation_stack.dashboards_private_kinesis_global[0].outputs["KinesisFirehoseDeliveryStreamArn"] : var.logging_option == "option3" ? aws_cloudformation_stack.dashboards-option3-global[0].outputs["KinesisFirehoseDeliveryStreamArn"] : null 97 | } 98 | }) 99 | } 100 | } 101 | 102 | -------------------------------------------------------------------------------- /6-optional-preprod-cfront.tf_: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | 10 | resource "aws_cloudfront_origin_access_identity" "preprod_origin_access_identity" { 11 | provider = aws.preprod 12 | comment = "waf poc" 13 | } 14 | 15 | #virginia for Cfront protected resources 16 | resource "aws_cloudfront_distribution" "preprod" { 17 | provider = aws.preprod 18 | default_root_object = var.default_root_object 19 | enabled = true 20 | is_ipv6_enabled = var.is_ipv6_enabled 21 | price_class = var.price_class 22 | retain_on_delete = var.retain_on_delete 23 | wait_for_deployment = var.wait_for_deployment 24 | #web_acl_id = var.web_acl_id 25 | 26 | /* dynamic "logging_config" { 27 | for_each = length(keys(var.logging_config)) == 0 ? [] : [var.logging_config] 28 | 29 | content { 30 | bucket = logging_config.value["bucket"] 31 | prefix = lookup(logging_config.value, "prefix", null) 32 | include_cookies = lookup(logging_config.value, "include_cookies", null) 33 | } 34 | } */ 35 | 36 | origin { 37 | domain_name = aws_s3_bucket.preprod_web.bucket_regional_domain_name 38 | origin_id = "primaryS3" 39 | #origin_path = lookup(origin.value, "origin_path", "") 40 | 41 | s3_origin_config { 42 | origin_access_identity = aws_cloudfront_origin_access_identity.preprod_origin_access_identity.cloudfront_access_identity_path 43 | } 44 | 45 | /* dynamic "custom_origin_config" { 46 | for_each = length(lookup(origin.value, "custom_origin_config", "")) == 0 ? [] : [lookup(origin.value, "custom_origin_config", "")] 47 | content { 48 | http_port = custom_origin_config.value.http_port 49 | https_port = custom_origin_config.value.https_port 50 | origin_protocol_policy = custom_origin_config.value.origin_protocol_policy 51 | origin_ssl_protocols = custom_origin_config.value.origin_ssl_protocols 52 | origin_keepalive_timeout = lookup(custom_origin_config.value, "origin_keepalive_timeout", null) 53 | origin_read_timeout = lookup(custom_origin_config.value, "origin_read_timeout", null) 54 | } 55 | } */ 56 | 57 | /* dynamic "custom_header" { 58 | for_each = lookup(origin.value, "custom_header", []) 59 | content { 60 | name = custom_header.value.name 61 | value = custom_header.value.value 62 | } 63 | } */ 64 | 65 | } 66 | 67 | 68 | default_cache_behavior { 69 | target_origin_id = "primaryS3" 70 | viewer_protocol_policy = "allow-all" 71 | 72 | allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] 73 | cached_methods = ["GET", "HEAD"] 74 | #compress = true 75 | #field_level_encryption_id = lookup(i.value, "field_level_encryption_id", null) 76 | min_ttl = 0 77 | default_ttl = 3600 78 | max_ttl = 86400 79 | 80 | forwarded_values { 81 | query_string = false 82 | 83 | cookies { 84 | forward = "none" 85 | } 86 | } 87 | 88 | } 89 | 90 | ordered_cache_behavior { 91 | path_pattern = "/content/immutable/*" 92 | allowed_methods = ["GET", "HEAD", "OPTIONS"] 93 | cached_methods = ["GET", "HEAD", "OPTIONS"] 94 | target_origin_id = "primaryS3" 95 | 96 | forwarded_values { 97 | query_string = false 98 | headers = ["Origin"] 99 | 100 | cookies { 101 | forward = "none" 102 | } 103 | } 104 | 105 | min_ttl = 0 106 | default_ttl = 86400 107 | max_ttl = 31536000 108 | compress = true 109 | viewer_protocol_policy = "redirect-to-https" 110 | } 111 | 112 | viewer_certificate { 113 | cloudfront_default_certificate = true 114 | } 115 | 116 | restrictions { 117 | geo_restriction { 118 | restriction_type = "none" 119 | } 120 | } 121 | 122 | tags = merge( 123 | { 124 | "Name" = format("%s", "CloudFront ${var.environment}") 125 | }, 126 | { 127 | "Environment" = format("%s", var.environment) 128 | }, 129 | local.cfront_tags, 130 | local.common_tags, 131 | ) 132 | 133 | lifecycle { 134 | ignore_changes = [ 135 | web_acl_id, 136 | ] 137 | } 138 | } -------------------------------------------------------------------------------- /code/data-ingestion-lambda.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import pyasn 4 | from user_agents import parse 5 | from counter_robots import is_machine, is_robot 6 | import boto3 7 | import os 8 | import re 9 | import requests 10 | from requests_aws4auth import AWS4Auth 11 | 12 | print('Loading AWS WAF Logs Enrichment function') 13 | asndb = pyasn.pyasn('/opt/ipasn.dat') 14 | ua = {} 15 | s3 = boto3.client('s3') 16 | 17 | 18 | def getUAInfo(user_agent_string): 19 | if user_agent_string in ua: 20 | return ua[user_agent_string] 21 | else: 22 | info = {} 23 | user_agent = parse(user_agent_string) 24 | browser = {} 25 | browser['family'] = user_agent.browser.family 26 | browser['version'] = user_agent.browser.version_string 27 | info['browser'] = browser 28 | os = {} 29 | os['family'] = user_agent.os.family 30 | os['version'] = user_agent.os.version_string 31 | info['os'] = os 32 | device = {} 33 | device['family'] = user_agent.device.family 34 | device['brand'] = user_agent.device.brand 35 | device['model'] = user_agent.device.model 36 | info['device'] = device 37 | ua[user_agent_string] = info 38 | return info 39 | 40 | def enrich(payload): 41 | #output = [] 42 | #payload = base64.b64decode(record['data']) 43 | log = json.loads(payload) 44 | # Flattens the header fields 45 | flatheaders = {} 46 | for item in log['httpRequest']['headers']: 47 | flatheaders[item['name'].lower()] = item['value'] 48 | del(log['httpRequest']['headers']) 49 | log['httpRequest']['headers'] = flatheaders 50 | # Parse and add webacl information 51 | webacl_info = log['webaclId'].split(":") 52 | log['webaclRegion'] = webacl_info[3] 53 | log['webaclAccount'] = webacl_info[4] 54 | webacl_name = webacl_info[5].split("/") 55 | log['webaclName'] = webacl_name[2] 56 | 57 | # Adds ASN number 58 | ip = "" 59 | if 'x-forwarded-for' in log['httpRequest']['headers']: 60 | ip = log['httpRequest']['headers']['x-forwarded-for'] 61 | # XFF header may come with more than one IP address, we'll use the first appearance in the list to report the ASN 62 | if len(ip) > 15: 63 | print(f'[WARNING] XFF with multiple IP addresses: {ip}; considering last') 64 | ip = ip.split(', ')[-1] 65 | else: 66 | ip = log['httpRequest']['clientIp'] 67 | try: 68 | log['httpRequest']['asn'] = asndb.lookup(ip)[0] 69 | except Exception as e: 70 | log['httpRequest']['asn'] = 'unknown' 71 | print(f'[ERROR] Got error {e}, while processing ASN for IP {ip}') 72 | print(e) 73 | if 'user-agent' in log['httpRequest']['headers']: 74 | user_agent_string = log['httpRequest']['headers']['user-agent'] 75 | # Adds user-agent information 76 | ua_info = getUAInfo(user_agent_string) 77 | log['httpRequest']['browser'] = ua_info['browser'] 78 | log['httpRequest']['os'] = ua_info['os'] 79 | log['httpRequest']['device'] = ua_info['device'] 80 | # Adds robot information 81 | device_type = '' 82 | if is_machine(user_agent_string): 83 | device_type = 'machine' 84 | elif is_robot(user_agent_string): 85 | device_type = 'robot' 86 | else: 87 | device_type = 'other' 88 | log['httpRequest']['deviceType'] = device_type 89 | #payload_output = json.dumps(log) 90 | #output_record = { 91 | # 'recordId': record['recordId'], 92 | # 'result': 'Ok', 93 | # 'data': base64.b64encode(payload.encode('utf-8') + b'\n').decode('utf-8') 94 | #} 95 | #output.append(output_record) 96 | print("Successfully processed record") 97 | return log 98 | 99 | def lambda_handler(event, context): 100 | print(event) 101 | index = os.environ['index'] + "-" + (event['Records'][0]['eventTime'].split("T"))[0] 102 | region = event['Records'][0]['awsRegion'] 103 | print(region) 104 | service = 'es' 105 | credentials = boto3.Session().get_credentials() 106 | awsauth = AWS4Auth(credentials.access_key, credentials.secret_key, region, service, session_token=credentials.token) 107 | headers = { "Content-Type": "application/json" } 108 | endpoint_name=os.environ['DOMAINNAME'] 109 | type = '_doc' 110 | url = "https://" + endpoint_name + '/' + index + '/' + type 111 | print(url) 112 | 113 | 114 | for record in event['Records']: 115 | bucket = record['s3']['bucket']['name'] 116 | key = record['s3']['object']['key'] 117 | try: 118 | obj = s3.get_object(Bucket=bucket, Key=key) 119 | body = obj['Body'].read() 120 | lines = body.splitlines() 121 | print(body) 122 | 123 | for line in lines: 124 | 125 | line = line.decode("utf-8") 126 | print(line) 127 | enrich_line = enrich(line) 128 | print(enrich_line) 129 | r = requests.post(url, auth=awsauth, json=enrich_line, headers=headers) 130 | print(r) 131 | 132 | 133 | 134 | except Exception as e: 135 | print(e) 136 | print('Error getting object {} from bucket {}. Make sure they exist and your bucket is in the same region as this function.'.format(key, bucket)) 137 | raise e 138 | -------------------------------------------------------------------------------- /6-optional-variables_cfront.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | 10 | variable "region" { 11 | description = "region" 12 | type = string 13 | default = "eu-west-1" 14 | } 15 | 16 | variable "environment" { 17 | description = "Environment name used for tagging" 18 | type = string 19 | default = "poc" 20 | } 21 | 22 | variable "Project" { 23 | description = "project name used for tagging" 24 | type = string 25 | default = "waf-poc" 26 | } 27 | 28 | variable "Owner" { 29 | description = "owner email address used for tagging" 30 | type = string 31 | default = "adf@amazon.com" 32 | } 33 | 34 | variable "create_distribution" { 35 | description = "Controls if CloudFront distribution should be created" 36 | type = bool 37 | default = true 38 | } 39 | 40 | variable "create_origin_access_identity" { 41 | description = "Controls if CloudFront origin access identity should be created" 42 | type = bool 43 | default = false 44 | } 45 | 46 | variable "origin_access_identities" { 47 | description = "Map of CloudFront origin access identities (value as a comment)" 48 | type = map(string) 49 | default = {} 50 | } 51 | 52 | variable "aliases" { 53 | description = "Extra CNAMEs (alternate domain names), if any, for this distribution." 54 | type = list(string) 55 | default = null 56 | } 57 | 58 | variable "comment" { 59 | description = "Any comments you want to include about the distribution." 60 | type = string 61 | default = null 62 | } 63 | 64 | variable "default_root_object" { 65 | description = "The object that you want CloudFront to return (for example, index.html) when an end user requests the root URL." 66 | type = string 67 | default = "index.html" 68 | } 69 | 70 | variable "enabled" { 71 | description = "Whether the distribution is enabled to accept end user requests for content." 72 | type = bool 73 | default = true 74 | } 75 | 76 | variable "http_version" { 77 | description = "The maximum HTTP version to support on the distribution. Allowed values are http1.1 and http2. The default is http2." 78 | type = string 79 | default = "http2" 80 | } 81 | 82 | variable "is_ipv6_enabled" { 83 | description = "Whether the IPv6 is enabled for the distribution." 84 | type = bool 85 | default = null 86 | } 87 | 88 | variable "price_class" { 89 | description = "The price class for this distribution. One of PriceClass_All, PriceClass_200, PriceClass_100" 90 | type = string 91 | default = null 92 | } 93 | 94 | variable "retain_on_delete" { 95 | description = "Disables the distribution instead of deleting it when destroying the resource through Terraform. If this is set, the distribution needs to be deleted manually afterwards." 96 | type = bool 97 | default = false 98 | } 99 | 100 | variable "wait_for_deployment" { 101 | description = "If enabled, the resource will wait for the distribution status to change from InProgress to Deployed. Setting this tofalse will skip the process." 102 | type = bool 103 | default = true 104 | } 105 | 106 | variable "web_acl_id" { 107 | description = "If you're using AWS WAF to filter CloudFront requests, the Id of the AWS WAF web ACL that is associated with the distribution. The WAF Web ACL must exist in the WAF Global (CloudFront) region and the credentials configuring this argument must have waf:GetWebACL permissions assigned. If using WAFv2, provide the ARN of the web ACL." 108 | type = string 109 | default = null 110 | } 111 | 112 | variable "tags" { 113 | description = "A map of tags to assign to the resource." 114 | type = map(string) 115 | default = null 116 | } 117 | 118 | variable "origin" { 119 | description = "One or more origins for this distribution (multiples allowed)." 120 | type = any 121 | default = null 122 | } 123 | 124 | variable "origin_group" { 125 | description = "One or more origin_group for this distribution (multiples allowed)." 126 | type = any 127 | default = {} 128 | } 129 | 130 | variable "viewer_certificate" { 131 | description = "The SSL configuration for this distribution" 132 | type = any 133 | default = { 134 | cloudfront_default_certificate = true 135 | minimum_protocol_version = "TLSv1" 136 | } 137 | } 138 | 139 | variable "geo_restriction" { 140 | description = "The restriction configuration for this distribution (geo_restrictions)" 141 | type = any 142 | default = {} 143 | } 144 | 145 | variable "logging_config" { 146 | description = "The logging configuration that controls how logs are written to your distribution (maximum one)." 147 | type = any 148 | default = {} 149 | } 150 | 151 | variable "custom_error_response" { 152 | description = "One or more custom error response elements" 153 | type = any 154 | default = {} 155 | } 156 | 157 | variable "default_cache_behavior" { 158 | description = "The default cache behavior for this distribution" 159 | type = any 160 | default = null 161 | } 162 | 163 | variable "ordered_cache_behavior" { 164 | description = "An ordered list of cache behaviors resource for this distribution. List from top to bottom in order of precedence. The topmost cache behavior will have precedence 0." 165 | type = any 166 | default = [] 167 | } -------------------------------------------------------------------------------- /5-optional-dev_apigw.tf_: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | resource "aws_api_gateway_rest_api" "apigw" { 10 | provider = aws.dev 11 | name = "waf-poc" 12 | description = "API for waf poc" 13 | endpoint_configuration { 14 | types = ["REGIONAL"] 15 | } 16 | 17 | tags = merge( 18 | local.app_tags, 19 | local.common_tags, 20 | ) 21 | 22 | policy = < 0: 33 | logging.getLogger().setLevel(logging.ERROR) 34 | else: 35 | logging.basicConfig(level=logging.ERROR) 36 | 37 | # Set the environment variable DEBUG to 'true' if you want verbose debug details in CloudWatch Logs. 38 | if INFO_LOGGING == 'true': 39 | logging.getLogger().setLevel(logging.INFO) 40 | 41 | 42 | # If you want different services, set the SERVICES environment variable 43 | # It defaults to ROUTE53_HEALTHCHECKS and CLOUDFRONT. Using 'jq' and 'curl' get the list of possible 44 | # services like this: 45 | # curl -s 'https://ip-ranges.amazonaws.com/ip-ranges.json' | jq -r '.prefixes[] | .service' ip-ranges.json | sort -u 46 | 47 | message = json.loads(event['Records'][0]['Sns']['Message']) 48 | 49 | # Load the ip ranges from the url 50 | ip_ranges = json.loads(get_ip_groups_json(message['url'], message['md5'])) 51 | 52 | # Extract the service ranges 53 | ranges = get_ranges_for_service(ip_ranges,SERVICES,EC2_REGIONS) 54 | 55 | # Update the AWS WAF IP sets 56 | update_waf_ipset(IPV4_SET_NAME,IPV4_SET_ID,ranges['ipv4']) 57 | update_waf_ipset(IPV6_SET_NAME,IPV6_SET_ID,ranges['ipv6']) 58 | 59 | return ranges 60 | 61 | def get_ip_groups_json(url, expected_hash): 62 | 63 | logging.debug("Updating from " + url) 64 | 65 | response = request.urlopen(url) 66 | ip_json = response.read() 67 | 68 | m = hashlib.md5() 69 | m.update(ip_json) 70 | hash = m.hexdigest() 71 | 72 | # If the hash provided is 'test-hash', returns the JSON without checking the hash 73 | if expected_hash == 'test-hash': 74 | print('Running in test mode') 75 | return ip_json 76 | 77 | if hash != expected_hash: 78 | raise Exception('MD5 Mismatch: got ' + hash + ' expected ' + expected_hash) 79 | 80 | return ip_json 81 | 82 | def get_ranges_for_service(ranges, services,ec2_regions): 83 | """Gets IPv4 and IPv6 prefixes from the matching services""" 84 | service_ranges = {'ipv6':[],'ipv4':[]} 85 | ec2_regions = strip_list(ec2_regions) 86 | services = strip_list(services) 87 | 88 | # Loop over the IPv4 prefixes and appends the matching services 89 | print(f'Searching for {services} IPv4 prefixes') 90 | for prefix in ranges['prefixes']: 91 | 92 | if prefix['service'] in services and \ 93 | ( 94 | (prefix['service'] != 'EC2') \ 95 | or \ 96 | (prefix['service']=='EC2' and ec2_regions != ['all'] and prefix['region'] in ec2_regions) \ 97 | or \ 98 | (prefix['service']=='EC2' and ec2_regions == ['all']) 99 | ): 100 | 101 | logging.info((f"Found {prefix['service']} region: {prefix['region']} range: {prefix['ip_prefix']}")) 102 | service_ranges['ipv4'].append(prefix['ip_prefix']) 103 | 104 | # Loop over the IPv6 prefixes and appends the matching services 105 | print(f'Searching for {services} IPv6 prefixes') 106 | for ipv6_prefix in ranges['ipv6_prefixes']: 107 | 108 | if ipv6_prefix['service'] in services and \ 109 | ( 110 | (ipv6_prefix['service'] != 'EC2') \ 111 | or \ 112 | (ipv6_prefix['service']=='EC2' and ec2_regions != ['all'] and ipv6_prefix['region'] in ec2_regions) \ 113 | or \ 114 | (ipv6_prefix['service']=='EC2' and ec2_regions == ['all']) 115 | ): 116 | 117 | logging.info((f"Found {ipv6_prefix['service']} region: {ipv6_prefix['region']} ipv6 range: {ipv6_prefix['ipv6_prefix']}")) 118 | service_ranges['ipv6'].append(ipv6_prefix['ipv6_prefix']) 119 | 120 | return service_ranges 121 | 122 | def update_waf_ipset(ipset_name,ipset_id,address_list): 123 | """Updates the AWS WAF IP set""" 124 | waf_client = boto3.client('wafv2') 125 | 126 | lock_token = get_ipset_lock_token(waf_client,ipset_name,ipset_id) 127 | 128 | logging.info(f'Got LockToken for AWS WAF IP Set "{ipset_name}": {lock_token}') 129 | 130 | waf_client.update_ip_set( 131 | Name=ipset_name, 132 | Scope='REGIONAL', 133 | Id=ipset_id, 134 | Addresses=address_list, 135 | LockToken=lock_token 136 | ) 137 | 138 | print(f'Updated IPSet "{ipset_name}" with {len(address_list)} CIDRs') 139 | 140 | def get_ipset_lock_token(client,ipset_name,ipset_id): 141 | """Returns the AWS WAF IP set lock token""" 142 | ip_set = client.get_ip_set( 143 | Name=ipset_name, 144 | Scope='REGIONAL', 145 | Id=ipset_id) 146 | 147 | return ip_set['LockToken'] 148 | 149 | def strip_list(list): 150 | """Strips individual elements of the strings""" 151 | return [item.strip() for item in list] -------------------------------------------------------------------------------- /2-aws-waf-automation-ip.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | 10 | 11 | #Automation from https://aws.amazon.com/blogs/security/automatically-update-aws-waf-ip-sets-with-aws-ip-ranges/ but translated to terraform 12 | resource "aws_wafv2_ip_set" "ipv4_automation" { 13 | provider = aws.regional 14 | name = "${var.regional_policy_name}-cloudfront-ipv4" 15 | scope = "REGIONAL" 16 | ip_address_version = "IPV4" 17 | addresses = [] 18 | 19 | tags = local.common_tags 20 | } 21 | 22 | resource "aws_wafv2_ip_set" "ipv6_automation" { 23 | provider = aws.regional 24 | name = "${var.regional_policy_name}-cloudfront-ipv6" 25 | scope = "REGIONAL" 26 | ip_address_version = "IPV6" 27 | addresses = [] 28 | 29 | tags = local.common_tags 30 | } 31 | 32 | resource "aws_wafv2_rule_group" "allowCloudfrontIP" { 33 | provider = aws.regional 34 | name = "${var.regional_policy_name}-allowCloudfrontIP" 35 | scope = "REGIONAL" 36 | capacity = 60 37 | description = "IP Rule Group for Cloudfront IP addresses" 38 | 39 | visibility_config { 40 | cloudwatch_metrics_enabled = true 41 | metric_name = "WAFCFrontIPRuleGroupMetric" 42 | sampled_requests_enabled = true 43 | } 44 | 45 | rule { 46 | name = "CFrontIPAllowRule" 47 | priority = 0 48 | 49 | action { 50 | allow {} 51 | } 52 | 53 | statement { 54 | 55 | or_statement { 56 | statement { 57 | ip_set_reference_statement { 58 | arn = aws_wafv2_ip_set.ipv4_automation.arn 59 | } 60 | } 61 | 62 | statement { 63 | ip_set_reference_statement { 64 | arn = aws_wafv2_ip_set.ipv6_automation.arn 65 | } 66 | } 67 | } 68 | } 69 | visibility_config { 70 | cloudwatch_metrics_enabled = true 71 | metric_name = "CFrontIPAllowRule-metric" 72 | sampled_requests_enabled = true 73 | } 74 | } 75 | 76 | } 77 | 78 | 79 | data "archive_file" "ipAutomation" { 80 | type = "zip" 81 | 82 | output_path = "${path.module}/code/ipAutomation.zip" 83 | source { 84 | content = file("${path.module}/code/ipAutomation.py") 85 | filename = "ipAutomation.py" 86 | } 87 | } 88 | 89 | 90 | resource "aws_lambda_function" "ipAutomation" { 91 | provider = aws.regional 92 | filename = "${path.module}/code/ipAutomation.zip" 93 | description = " This Lambda function, invoked by an incoming SNS message, updates the IPv4 and IPv6 sets with the addresses from the specified services" 94 | function_name = "AWS-WAF-Update-IP-Sets" 95 | role = aws_iam_role.ipAutomation-iam-role.arn 96 | handler = "ipAutomation.lambda_handler" 97 | runtime = "python3.8" 98 | source_code_hash = data.archive_file.ipAutomation.output_base64sha256 99 | timeout = 100 100 | 101 | environment { 102 | variables = { 103 | IPV4_SET_NAME = "${var.regional_policy_name}-cloudfront-ipv4" 104 | IPV4_SET_ID = aws_wafv2_ip_set.ipv4_automation.id 105 | IPV6_SET_NAME = "${var.regional_policy_name}-cloudfront-ipv6" 106 | IPV6_SET_ID = aws_wafv2_ip_set.ipv6_automation.id 107 | SERVICES = "CLOUDFRONT" 108 | EC2_REGIONS = "all" 109 | INFO_LOGGING = "true" 110 | } 111 | } 112 | 113 | tags = local.common_tags 114 | } 115 | 116 | 117 | # ipAutomation 118 | resource "aws_iam_role" "ipAutomation-iam-role" { 119 | provider = aws.regional 120 | name = "AWS-WAF-Update-IP-Sets" 121 | 122 | assume_role_policy = < S3 bucket in the same account + KMS encryption---------------- 11 | #Please add your lambda processors for Kinesis and S3 bucket access log configuration 12 | #--------------------------------GLOBAL LOGGING 13 | 14 | resource "aws_kinesis_firehose_delivery_stream" "WAFKinesisFirehose_global" { 15 | count = var.logging_option == "option1" ? 1 : 0 16 | provider = aws.global 17 | name = "${var.kinesis_firehose_name}-global" 18 | destination = "extended_s3" 19 | 20 | extended_s3_configuration { 21 | role_arn = aws_iam_role.firehose_role_global[0].arn 22 | bucket_arn = aws_s3_bucket.logging_bucket_global[0].arn 23 | prefix = var.kinesis_prefix 24 | buffer_interval = var.s3_delivery_buffer_interval 25 | buffer_size = var.s3_delivery_buffer_size 26 | kms_key_arn = aws_kms_key.waf_logs_global[0].arn 27 | 28 | # processing_configuration { 29 | # enabled = "true" 30 | 31 | # processors { 32 | # type = "Lambda" 33 | 34 | # parameters { 35 | # parameter_name = "LambdaArn" 36 | # parameter_value = "${aws_lambda_function.lambda_processor.arn}:$LATEST" 37 | # } 38 | # } 39 | # } 40 | } 41 | 42 | } 43 | 44 | resource "aws_s3_bucket" "logging_bucket_global" { 45 | count = var.logging_option == "option1" ? 1 : 0 46 | provider = aws.global 47 | bucket = "${var.kinesis_destination_s3_name}-global" 48 | acl = "private" 49 | 50 | server_side_encryption_configuration { 51 | rule { 52 | apply_server_side_encryption_by_default { 53 | kms_master_key_id = aws_kms_key.waf_logs_global[0].arn 54 | sse_algorithm = "aws:kms" 55 | } 56 | } 57 | } 58 | 59 | lifecycle_rule { 60 | id = "manage-old-objects" 61 | enabled = true 62 | 63 | expiration { 64 | days = 90 65 | } 66 | 67 | noncurrent_version_expiration { 68 | days = 90 69 | } 70 | } 71 | } 72 | 73 | resource "aws_s3_bucket_policy" "logging_bucket_global" { 74 | count = var.logging_option == "option1" ? 1 : 0 75 | provider = aws.global 76 | bucket = aws_s3_bucket.logging_bucket_global[0].id 77 | 78 | policy = <0 && var.enable_ssm ? 1 : 0 126 | vpc_id = aws_vpc.vpc.id 127 | service_name = data.aws_vpc_endpoint_service.ssm[0].service_name 128 | vpc_endpoint_type = "Interface" 129 | 130 | security_group_ids = [aws_security_group.ssm_endpoint_sg[0].id] 131 | subnet_ids = [aws_subnet.subnet_priv[0].id] 132 | private_dns_enabled = true 133 | tags = merge( 134 | var.tags, 135 | { 136 | "Name" = "${var.vpc_name}-SSM-ENDPOINT" 137 | }, 138 | ) 139 | } 140 | 141 | #FOR SSM MESSAGES 142 | data "aws_vpc_endpoint_service" "ssmmessages" { 143 | count = var.enable_ssm ? 1 : 0 144 | service = "ssmmessages" 145 | } 146 | 147 | resource "aws_vpc_endpoint" "ssmmessages" { 148 | count = length(var.private_subnets_cidr_list)>0 && var.enable_ssm ? 1 : 0 149 | vpc_id = aws_vpc.vpc.id 150 | service_name = data.aws_vpc_endpoint_service.ssmmessages[0].service_name 151 | vpc_endpoint_type = "Interface" 152 | 153 | security_group_ids = [aws_security_group.ssm_endpoint_sg[0].id] 154 | subnet_ids = [aws_subnet.subnet_priv[0].id] 155 | private_dns_enabled = true 156 | tags = merge( 157 | var.tags, 158 | { 159 | "Name" = "${var.vpc_name}-SSMMESSAGES-ENDPOINT" 160 | }, 161 | ) 162 | } 163 | 164 | #FOR EC2 165 | data "aws_vpc_endpoint_service" "ec2" { 166 | count = var.enable_ssm ? 1 : 0 167 | service = "ec2" 168 | } 169 | 170 | resource "aws_vpc_endpoint" "ec2" { 171 | count = length(var.private_subnets_cidr_list)>0 && var.enable_ssm ? 1 : 0 172 | vpc_id = aws_vpc.vpc.id 173 | service_name = data.aws_vpc_endpoint_service.ec2[0].service_name 174 | vpc_endpoint_type = "Interface" 175 | 176 | security_group_ids = [aws_security_group.ssm_endpoint_sg[0].id] 177 | subnet_ids = [aws_subnet.subnet_priv[0].id] 178 | private_dns_enabled = true 179 | tags = merge( 180 | var.tags, 181 | { 182 | "Name" = "${var.vpc_name}--EC2-ENDPOINT" 183 | }, 184 | ) 185 | } 186 | 187 | #FOR EC2 MESSAGES 188 | data "aws_vpc_endpoint_service" "ec2messages" { 189 | count = var.enable_ssm ? 1 : 0 190 | service = "ec2messages" 191 | } 192 | 193 | resource "aws_vpc_endpoint" "ec2messages" { 194 | count = length(var.private_subnets_cidr_list)>0 && var.enable_ssm ? 1 : 0 195 | vpc_id = aws_vpc.vpc.id 196 | service_name = data.aws_vpc_endpoint_service.ec2messages[0].service_name 197 | vpc_endpoint_type = "Interface" 198 | 199 | security_group_ids = [aws_security_group.ssm_endpoint_sg[0].id] 200 | subnet_ids = [aws_subnet.subnet_priv[0].id] 201 | private_dns_enabled = true 202 | tags = merge( 203 | var.tags, 204 | { 205 | "Name" = "${var.vpc_name}-EC2MESSAGES-ENDPOINT" 206 | }, 207 | ) 208 | } 209 | 210 | #FOR KMS 211 | data "aws_vpc_endpoint_service" "kms" { 212 | count = var.enable_ssm ? 1 : 0 213 | service = "kms" 214 | } 215 | 216 | resource "aws_vpc_endpoint" "kms" { 217 | count = length(var.private_subnets_cidr_list)>0 && var.enable_ssm ? 1 : 0 218 | vpc_id = aws_vpc.vpc.id 219 | service_name = data.aws_vpc_endpoint_service.kms[0].service_name 220 | vpc_endpoint_type = "Interface" 221 | 222 | security_group_ids = [aws_security_group.ssm_endpoint_sg[0].id] 223 | subnet_ids = [aws_subnet.subnet_priv[0].id] 224 | private_dns_enabled = true 225 | tags = merge( 226 | var.tags, 227 | { 228 | "Name" = "${var.vpc_name}-KMS-ENDPOINT" 229 | }, 230 | ) 231 | } -------------------------------------------------------------------------------- /4-fwm-waf-logging-option1_regional.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | 10 | # LOGGING CONFIGURATION OPTION 1 - Kinesis -> S3 bucket in the same account + KMS encryption---------------- 11 | #Please add your lambda processors for Kinesis and S3 bucket access log configuration 12 | resource "aws_kinesis_firehose_delivery_stream" "WAFKinesisFirehose_regional" { 13 | count = var.logging_option == "option1" ? 1 : 0 14 | provider = aws.regional 15 | name = "${var.kinesis_firehose_name}-regional" 16 | destination = "extended_s3" 17 | 18 | extended_s3_configuration { 19 | role_arn = aws_iam_role.firehose_role_regional[0].arn 20 | bucket_arn = aws_s3_bucket.logging_bucket_regional[0].arn 21 | prefix = var.kinesis_prefix 22 | buffer_interval = var.s3_delivery_buffer_interval 23 | buffer_size = var.s3_delivery_buffer_size 24 | kms_key_arn = aws_kms_key.waf_logs_regional[0].arn 25 | 26 | # processing_configuration { #add your lambda function 27 | # enabled = "true" 28 | 29 | # processors { 30 | # type = "Lambda" 31 | 32 | # parameters { 33 | # parameter_name = "LambdaArn" 34 | # parameter_value = "${aws_lambda_function.lambda_processor.arn}:$LATEST" 35 | # } 36 | # } 37 | # } 38 | } 39 | 40 | } 41 | 42 | resource "aws_s3_bucket" "logging_bucket_regional" { 43 | count = var.logging_option == "option1" ? 1 : 0 44 | provider = aws.regional 45 | bucket = "${var.kinesis_destination_s3_name}-regional" 46 | acl = "private" 47 | 48 | server_side_encryption_configuration { 49 | rule { 50 | apply_server_side_encryption_by_default { 51 | kms_master_key_id = aws_kms_key.waf_logs_regional[0].arn 52 | sse_algorithm = "aws:kms" 53 | } 54 | } 55 | } 56 | 57 | /* logging { #add your loggin s3 bucket 58 | target_bucket = local.s3_bucket_logging 59 | target_prefix = "s3-access-logs/${var.kinesis_destination_s3_name}/" 60 | } */ 61 | 62 | lifecycle_rule { 63 | id = "manage-old-objects" 64 | enabled = true 65 | 66 | expiration { 67 | days = 90 68 | } 69 | 70 | noncurrent_version_expiration { 71 | days = 90 72 | } 73 | } 74 | } 75 | 76 | resource "aws_s3_bucket_policy" "logging_bucket_regional" { 77 | count = var.logging_option == "option1" ? 1 : 0 78 | provider = aws.regional 79 | bucket = aws_s3_bucket.logging_bucket_regional[0].id 80 | 81 | policy = < AWS Firewall Manager WAF policy with account and resource scope 9 | 10 | > Centralized logging configuration for AWS WAF Web ACLs 11 | 12 | > Dashboard using Amazon ElasticSearch 13 | 14 | > Automation of AWS WAF IP set with CloudFront IP addresses and AWS WAF IP rule 15 | 16 | > Creation of AWS WAF custom rules (XSS, Regex, IP set, SQLi, Rate based) 17 | 18 | > AWS Managed Bot rules are used as an example 19 | 20 | > AWS WAF Rate based rule deployed with AWS Firewall Manager 21 | 22 | > 3 types of logging configuration: 1) Kinesis -> S3, 2) Kinesis -> (cross account) S3 -> ES (private in VPC) and 3) Kinesis->lambda->ES->Kibana in the same account 23 | 24 | ### Architecture 25 | 26 | ![img](images/waf-offering-design.jpg) 27 | 28 | ### Logging Option 1 29 | 30 | ![img](images/waf-offering-logging-option1.jpg) 31 | 32 | ### Logging Option 2 33 | 34 | ![img](images/waf-offering-logging-option2.png) 35 | 36 | ### Logging Option 3 37 | 38 | ![img](images/waf-offering-logging-option3.jpg) 39 | 40 | ### variables: 41 | The following variables are the most important ones, but please check the file **0-variables_firewall.tf** for a complete visibility. 42 | 43 | **For deployment**: 44 | > master_region and application_region 45 | 46 | > create_global_fms_waf_policy: enable or disable the creation of AWS Firewall Manager WAF policy with GLOBAL scope 47 | 48 | > create_regional_fms_waf_policy: enable or disable the creation of AWS Firewall Manager WAF policy with REGIONAL scope 49 | 50 | > create_waf_geo_rule_group: enable or disable the creation of AWS WAF rule group with Geolocation (it is not part of AWS Firewall Manager Policy by default) 51 | 52 | > create_waf_regex_rule_group: enable or disable the creation of AWS WAF rule group with Regex pattern (it is not part of AWS Firewall Manager Policy by default) 53 | 54 | > create_waf_geo_rule_group: enable or disable the creation of AWS WAF rule group with Geolocation (it is not part of AWS Firewall Manager Policy by default) 55 | 56 | > create_waf_regex_rule_group: enable or disable the creation of AWS WAF rule group with Regex pattern (it is not part of AWS Firewall Manager Policy by default) 57 | 58 | > create_waf_sqli_rule_group: enable or disable the creation of AWS WAF rule group with SQLi rule (it is not part of AWS Firewall Manager Policy by default) 59 | 60 | > create_waf_xss_rule_group: enable or disable the creation of AWS WAF rule group with XSS rule (it is not part of AWS Firewall Manager Policy by default) 61 | 62 | > create_waf_ip_rule_group: enable or disable the creation of AWS WAF rule group with IP set (it is not part of AWS Firewall Manager Policy by default) 63 | 64 | > logging_option: variable that defines the logging resources to create. option1 by default. Available options: option1, option2 and option3. 65 | 66 | **For Global Firewall Manager WAF policy**: 67 | > global_policy_name: name for the AWS Firewall Manager WAF policy for CloudFront distributions 68 | 69 | > global_policy_remediation_enabled (true by default) 70 | 71 | > global_policy_orgunit_list: org unit of the workload accounts to deploy FM security policy for Shield Advanced 72 | 73 | > global_policy_resource_tags: map of resource tags that aws firewall manager will check to associate web acl 74 | 75 | > global_policy_overrideCustomerWebACLAssociation (true by default) 76 | 77 | > global_policy_default_action (ALLOW by default) 78 | 79 | **For regional Firewall Manager WAF policy**: 80 | > regional_policy_name 81 | 82 | > regional_policy_remediation_enabled 83 | 84 | > regional_policy_orgunit_list 85 | 86 | > regional_policy_resource_tags 87 | 88 | > regional_policy_default_action 89 | 90 | **For logging**: 91 | > kinesis_firehose_name: name for kinesis firehose that is the aws waf logging destination (it has to starts with aws-waf-logs-) 92 | 93 | > kinesis_destination_s3_name: name for s3 bucket that is the target of kinesis firehose 94 | 95 | > kinesis_prefix (waf-logs by default) 96 | 97 | > DataNodeEBSVolumeSize: Volume side for ES Data Nodes 98 | 99 | > ESConfigBucket: Name for ES Config Bucket 100 | 101 | > ESUserEmail 102 | 103 | > ES_SSH_tunnel_instance_type, ES_SSH_tunnel_amid_id_regional, ES_SSH_tunnel_amid_id_global and ES_SSH_tunnel_key_name 104 | 105 | > ES_SSH_tunnel_allowed_CIDR: list of CIDR to allow access to SSH tunnel 106 | 107 | ## Pre-requisites 108 | > AWS Organizations 109 | 110 | > Member accounts must have AWS config enabled in the region 111 | 112 | > Set the AWS Firewall Manager Admin account in the master account of the organization 113 | 114 | > Customize the variable values in the file PROD.tfvars 115 | 116 | > Update providers with your account IDs and your role name (0-providers.tf) 117 | 118 | 119 | 120 | ## Deployment steps 121 | 122 | 1. Add your details in providers (role to assume), variables and vars/PROD.tfvars 123 | 124 | 2. Choose which AWS WAF Web Acls you are going to deploy. Use 1-fwm-global and 1-fmw-regional as a template for them. By default, both AWS Firewall manager policies will be deployed, but you can control this using create_global_fms_waf_policy and create_regional_fms_waf_policy vairables. The rule groups used in each policy are not customized with variables because of its format, so you can either deploy the rules that are established at the moment, or make the changes that your solution needs. 125 | 126 | 3. Choose which AWS WAF custom rules you are going to deploy. Use 3-aws-waf files as templates for them. Add those custom rules to rule groups and modify 1-fwm files. You can also control which AWS WAF custom rule group is created using the variables create_waf_*. Those custom rules groups are not part of the AWS Firewall Manager WAF policy by default. Also, the resources in 2-aws-waf-automation-ip.tf and 3-aws-waf-rate-based.tf will be created independently of the variables values. If you do not require those resources, please update the files. 127 | 128 | 4. Choose your logging configuration using the variable logging_option. Possible values: option1, option2 and option3. Options 2 and 3 use CloudFormation stacks for the deployment and configuration of Cognito and ElasticSearch. 129 | 130 | 6. Run first terraform plan and terraform apply. For logging configuration, there is a cycle between KMS key policy and Firehose IAM role. Create first one (IAM role) and then edit the other (KMS policy). 131 | 132 | 7. {optional} If you would like to test these policies and you have other workload accounts, you can deploy CloudFront distribution + S3 bucket and CloudFront distribution + API GW using 5-optional and 6-optional files. Check the providers and the configuration values first. 133 | 134 | 135 | ### Solution 136 | 137 | > Template for AWS Firewall Manager WAF policy to apply regional (for applications) and globally (for CloudFront). User needs to customize the rule groups that apply into the first and last rule group for AWS WAF. 138 | 139 | > Templates for AWS WAF custom rules to create specific rules such as SQL injection, Cross-site scripting, Regex pattern, string matching, IP sets, Geo blocking, rate based rules, etc. 140 | 141 | > Automation to create two IP sets with CloudFront IP addresses and a AWS WAF IP rule using those. Check blog https://aws.amazon.com/blogs/security/automatically-update-aws-waf-ip-sets-with-aws-ip-ranges/ but translated to terraform 142 | 143 | > Logging configuration for 3 different cases: 1) Kinesis + S3 bucket in firewall manager account, 2) Kinesis in firewall manager account & S3 + ElasticSearch + Kibana in logging account, and 3) Kinesis + ElasticSearch + Kibana in firewall manager account. In this script the solution 1 is implemented. 144 | 145 | > Terraform does not support rule groups with rate based rules, workaround using CloudFormation template. 146 | 147 | ### Files 148 | 149 | #### AWS Firewall Manager WAF policy 150 | These are the files with the code to deploy a AWS WAF WebACL using AWS Firewall Manager: 151 | - **1-fwm-global-webacl.tf**: It creates an aws firewall manager policy in the global scope (provider calles global) for the resource type AWS::CloudFront::Distribution. With the input variables you can specify the values for the name, the remediation, Org unit, tags, default action, etc. For the AWS WAF WebACL, the following managed rules have been added: AWS managed rules Core Rule Set, AWS Managed admin protection, AWS Managed IP reputation, known bad inputs set and AWS Managed Bot Control rules (in count mode as an example of how to use it). A rate based rule has been added as a custom rule. For the logging, an Amazon Kinesis Firehose has been created in us-east-1. 152 | - **1-fwm-regional-webacl.tf**: It created an aws firewall manager policy in a region (defined by the provider called regional) for the resource type Load Balancer and Api GW. With the input variables you can specify the values for the name, the remediation, Org unit, tags, default action, etc. For the AWS WAF WebACL, the following managed rules have been added: AWS Managed SQL injection rules, AWS Managed Linux rules, AWS Managed Unit rules, AWS Managed PHP rules and AWS Managed Word Press rules.IP allow set (CloudFront IP addresses) has been added as custom rule. For the logging, an Amazon Kinesis Firehose has been created in the region. 153 | 154 | 155 | #### WAF Automation 156 | File **“2-aws-waf-automation-ip.tf”** is using the automation from this AWS post, but the code has been translated into Terraform. This part created AWS IP set to collect CloudFront IP addresses (IPv4 and IPv6) and allow the traffic in the application from those IP addresses. 157 | 158 | #### AWS WAF custom rules and Rule groups 159 | 160 | Files: 161 | - **3-aws-waf-geo.tf** : It shows how to define a Rule group to apply Geolocation block list. For the example, the country code used is “KP”. 162 | - **3-aws-waf-ip.tf** : It shows how to define an IP Set and rule groups to Allow/Deny those IP sets. 163 | - **3-aws-waf-regex.tf** : It shows how to define a regex pattern set and a rule group that uses it.* With the new release of Rate based rules supported by AWS Firewall Manager, this file may need to be updated. 164 | - **3-aws-waf-rate-based.tf** and **3-aws-waf-rate-based.yaml** : Terraform does not support rule groups with rate based rules, workaround using CloudFormation template. 165 | - **3-aws-waf-sqli-xss.tf** : It shows how to create a rule group that uses SQLi match statements. 166 | 167 | #### Centralized logging 168 | 169 | There are three options to deploy the centralized logging that you can use and customize. 170 | 171 | 1) **Option 1**: 4-fwm-waf-logging-option1*.tf, Amazon Kinesis Firehose with S3 as destination in the same account and with KMS encryption. You can add a lambda to process the logs in Amazon Kinesis Firehose configuration. 172 | 2) **Option 2**: 4-fwm-waf-logging-option2*.tf Kinesis Firehose -> (cross account) S3 -> ES (private in VPC). Check this artifact for more information about this architecture. This architecture uses a CloudFormation stack to deploy Amazon Kinesis Firehose, the IAM role used by Kinesis Firehose and the resources for Amazon Elastic Search. It also creates an ssh tunnel to access the dashboard, following this documentation. 173 | 3) **Option 3**: 4-fwm-waf-logging-option3*.tf, Kinesis->lambda->ES->Kibana in the same account. This architecture uses a CloudFormation stack defined in the file dashboard.yaml. 174 | 175 | 176 | #### Test resources 177 | Files **5-optional-*.tf** create resources that can be protected by AWS WAF with the purpose of testing the AWS WAF WebACL. 178 | 179 | ## Example 180 | Check the documentation in the folder documentation. -------------------------------------------------------------------------------- /modules/vpc/0-variables.tf: -------------------------------------------------------------------------------- 1 | #-----------Identifiers and tags 2 | 3 | variable "vpc_name" { 4 | description = "[Required] Name to be used for the vpc and all the resources as identifier" 5 | type = string 6 | } 7 | 8 | variable "vpc_suffix" { 9 | description = "[optional] suffix to append to the VPC name" 10 | type = string 11 | default = "" 12 | } 13 | 14 | variable "tags" { 15 | description = "[optional] A map of tags to add to all resources" 16 | type = map(string) 17 | default = {} 18 | } 19 | 20 | #-----------VPC CIDR blocks 21 | 22 | variable "vpc_main_cidr" { 23 | description = "[Required] The main CIDR block for the VPC" 24 | type = string 25 | } 26 | 27 | 28 | variable "enable_ipv6" { 29 | description = "[optional] Requests an Amazon-provided IPv6 CIDR block with a /56 prefix length for the VPC. You cannot specify the range of IP addresses, or the size of the CIDR block." 30 | type = bool 31 | default = false 32 | } 33 | 34 | variable "secondary_cidr_blocks" { 35 | description = "[optional] List of secondary CIDR blocks to associate with the VPC to extend the IP Address pool" 36 | type = list(string) 37 | default = [] 38 | } 39 | 40 | 41 | #-----------VPC characteristics 42 | 43 | 44 | variable "enable_dns_hostnames" { 45 | description = "[optional] Should be true to enable DNS hostnames in the VPC" 46 | type = bool 47 | default = false 48 | } 49 | 50 | variable "enable_dns_support" { 51 | description = "[optional] Should be true to enable DNS support in the VPC" 52 | type = bool 53 | default = true 54 | } 55 | 56 | 57 | variable "instance_tenancy" { 58 | description = "[optional] A tenancy option for instances launched into the VPC" 59 | type = string 60 | default = "default" 61 | } 62 | 63 | variable "enable_internet_gateway" { 64 | description = "[optional] Should be true if you want to provision Internet Gateways for your public subnets" 65 | type = bool 66 | default = false 67 | } 68 | 69 | variable "enable_nat_gateway" { 70 | description = "[optional] Should be true if you want to provision NAT Gateways for each of your private networks" 71 | type = bool 72 | default = false 73 | } 74 | 75 | #-----------subnet characteristics 76 | 77 | variable "number_AZ" { 78 | description = "[optional] This field is used if you need to create more than one subnet per AZ. Specify the number of AZ's (default 2). In the variable *_subnets_cidr_list, the order should be [CIDR subnet 1 AZ A, CIDR subnet 2 AZ B, CIDR subnet 3 AZ A...]" 79 | type = number 80 | default = 2 81 | } 82 | 83 | variable "private_subnets_cidr_list" { 84 | description = "[optional] A list of private subnet CIDR blocks inside the VPC (for endpoints, eni, etc.)." 85 | type = list(string) 86 | default = [] 87 | } 88 | 89 | variable "private_subnets_suffix" { 90 | description = "[optional] Suffix to append to private subnets name" 91 | type = string 92 | default = "private" 93 | } 94 | 95 | variable "private_subnets_internet_access_nat_gw" { 96 | description = "[optional] connectivity with nat gw. Boolean. False by default." 97 | type = bool 98 | default = false 99 | } 100 | 101 | variable "tgw_subnets_cidr_list" { 102 | description = "[optional] A list of transit gateway private subnets CIDR blocks inside the VPC (for endpoints, eni, etc.)" 103 | type = list(string) 104 | default = [] 105 | } 106 | 107 | variable "tgw_subnets_suffix" { 108 | description = "[optional] Suffix to append to transit gateway private subnets name" 109 | type = string 110 | default = "tgw-private" 111 | } 112 | 113 | variable "tgw_subnets_internet_access_nat_gw" { 114 | description = "[optional] connectivity with nat gw. Boolean. False by default." 115 | type = bool 116 | default = false 117 | } 118 | 119 | variable "fw_subnets_cidr_list" { 120 | description = "[optional] A list of network firewall subnets CIDR blocks inside the VPC (for endpoints, eni, etc.)" 121 | type = list(string) 122 | default = [] 123 | } 124 | 125 | variable "fw_subnets_suffix" { 126 | description = "[optional] Suffix to append to network firewall subnets name" 127 | type = string 128 | default = "network-fw" 129 | } 130 | 131 | variable "fw_subnets_internet_access_nat_gw" { 132 | description = "[optional] connectivity with nat gw. Boolean. False by default." 133 | type = bool 134 | default = false 135 | } 136 | 137 | 138 | variable "public_subnets_cidr_list" { 139 | description = "[optional] A list of public subnet CIDR blocks inside the VPC" 140 | type = list(string) 141 | default = [] 142 | } 143 | 144 | variable "public_subnets_internet_access_igw" { 145 | description = "[optional] connectivity with igw. Boolean. True by default." 146 | type = bool 147 | default = true 148 | } 149 | 150 | variable "public_subnets_suffix" { 151 | description = "[optional] Suffix to append to public subnets name" 152 | type = string 153 | default = "public" 154 | } 155 | 156 | 157 | variable "map_public_ip_on_launch" { 158 | description = "[optional] pecify true to indicate that instances launched into the subnet should be assigned a public IP address." 159 | type = string 160 | default = false 161 | } 162 | 163 | variable "web_tier_subnets_cidr_list" { 164 | description = "[optional] A list of web tier subnet CIDR blocks inside the VPC" 165 | type = list(string) 166 | default = [] 167 | } 168 | 169 | variable "web_subnets_suffix" { 170 | description = "[optional] Suffix to append to web subnets name" 171 | type = string 172 | default = "web" 173 | } 174 | 175 | variable "web_subnets_internet_access_nat_gw" { 176 | description = "[optional] connectivity with nat gw. Boolean. False by default." 177 | type = bool 178 | default = false 179 | } 180 | 181 | variable "pres_tier_subnets_cidr_list" { 182 | description = "[optional] A list of presentation tier subnet CIDR blocks inside the VPC" 183 | type = list(string) 184 | default = [] 185 | } 186 | 187 | variable "pres_subnets_suffix" { 188 | description = "[optional] Suffix to append to presentation subnets name" 189 | type = string 190 | default = "presentation" 191 | } 192 | 193 | 194 | variable "database_tier_subnets_cidr_list" { 195 | description = "[optional] A list of database tier subnet CIDR blocks inside the VPC" 196 | type = list(string) 197 | default = [] 198 | } 199 | 200 | variable "database_subnets_suffix" { 201 | description = "[optional] Suffix to append to database subnets name" 202 | type = string 203 | default = "database" 204 | } 205 | 206 | variable "outposts_subnets_cidr_list" { 207 | description = "[optional] A list of outposts subnet CIDR blocks inside the VPC" 208 | type = list(string) 209 | default = [] 210 | } 211 | 212 | variable "coip_auto_assign" { 213 | description = "[optional] true if customer owned ip address pool has to be associated with outpost subnets" 214 | type = bool 215 | default = false 216 | } 217 | 218 | variable "outposts_subnets_suffix" { 219 | description = "[optional] Suffix to append to outposts subnets name" 220 | type = string 221 | default = "outposts" 222 | } 223 | 224 | variable "outposts_arn" { 225 | description = "[optional] Arn of the outposts where the subnets will be launched" 226 | type = string 227 | default = "" 228 | } 229 | 230 | #variable "associate_vpc_with_local_gw_route_table" { 231 | # description = "[optional] create an association between outposts local gateway route table and vpc" 232 | # type = bool 233 | # default = false 234 | #} 235 | 236 | variable "outposts_route_to_LGW_destination" { 237 | description = "[optional] IPv4 CIDR block destination to route to LGW in outposts subnet" 238 | type = string 239 | default = "" 240 | } 241 | 242 | variable "outposts_local_gateway_id" { 243 | description = "[optional] Outposts local gateway ID" 244 | type = string 245 | default = "" 246 | } 247 | 248 | 249 | 250 | #---------SSM 251 | 252 | 253 | variable "enable_ssm" { 254 | description = "[optional] Enable SSM for EC2 instances. If true, IAM role and endpoints will be deployed." 255 | type = bool 256 | default = false 257 | } 258 | 259 | variable "enable_s3_endpoint" { 260 | description = "[optional] True to create an S3 VPC gateway endpoint." 261 | type = bool 262 | default = true 263 | } 264 | 265 | variable "create_iam_role_ssm" { 266 | description = "[optional] Enable or disable the creation of an IAM role for EC2 instances to connect using SSM" 267 | type = bool 268 | default = true 269 | } 270 | 271 | #---------DHCP OPTIONS 272 | 273 | variable "enable_dhcp_options" { 274 | description = "[optional] Should be true if you want to specify a DHCP options set with a custom domain name, DNS servers, NTP servers, netbios servers, and/or netbios server type" 275 | type = bool 276 | default = false 277 | } 278 | 279 | variable "dhcp_options_domain_name" { 280 | description = "[optional] Specifies DNS name for DHCP options set (requires enable_dhcp_options set to true)" 281 | type = string 282 | default = "" 283 | } 284 | 285 | variable "dhcp_options_domain_name_servers" { 286 | description = "[optional] Specify a list of DNS server addresses for DHCP options set, default to AWS provided (requires enable_dhcp_options set to true)" 287 | type = list(string) 288 | default = ["AmazonProvidedDNS"] 289 | } 290 | 291 | variable "dhcp_options_ntp_servers" { 292 | description = "[optional] Specify a list of NTP servers for DHCP options set (requires enable_dhcp_options set to true)" 293 | type = list(string) 294 | default = [] 295 | } 296 | 297 | variable "dhcp_options_netbios_name_servers" { 298 | description = "[optional] Specify a list of netbios servers for DHCP options set (requires enable_dhcp_options set to true)" 299 | type = list(string) 300 | default = [] 301 | } 302 | 303 | variable "dhcp_options_netbios_node_type" { 304 | description = "[optional] Specify netbios node_type for DHCP options set (requires enable_dhcp_options set to true)" 305 | type = string 306 | default = "" 307 | } 308 | 309 | #-----------FLOW LOGS 310 | variable "enable_flow_log" { 311 | description = "[optional] Whether or not to enable CW VPC Flow Logs" 312 | type = bool 313 | default = false 314 | } 315 | 316 | variable "create_flow_log_cloudwatch_log_group" { 317 | description = "[optional] Whether to create CloudWatch log group for VPC Flow Logs" 318 | type = bool 319 | default = false 320 | } 321 | 322 | variable "create_flow_log_cloudwatch_iam_role" { 323 | description = "[optional] Whether to create IAM role for VPC Flow Logs" 324 | type = bool 325 | default = false 326 | } 327 | 328 | variable "flow_log_traffic_type" { 329 | description = "[optional] The type of traffic to capture. Valid values: ACCEPT, REJECT, ALL." 330 | type = string 331 | default = "ALL" 332 | } 333 | 334 | variable "flow_log_destination_type" { 335 | description = "[optional] Type of flow log destination. Can be s3 or cloud-watch-logs." 336 | type = string 337 | default = "cloud-watch-logs" 338 | } 339 | 340 | variable "flow_log_log_format" { 341 | description = "[optional] The fields to include in the flow log record, in the order in which they should appear." 342 | type = string 343 | default = null 344 | } 345 | 346 | variable "flow_log_destination_arn" { 347 | description = "[optional] The ARN of the CloudWatch log group or S3 bucket where VPC Flow Logs will be pushed. If this ARN is a S3 bucket the appropriate permissions need to be set on that bucket's policy. When create_flow_log_cloudwatch_log_group is set to false this argument must be provided." 348 | type = string 349 | default = "" 350 | } 351 | 352 | variable "flow_log_cloudwatch_iam_role_arn" { 353 | description = "[optional] The ARN for the IAM role that's used to post flow logs to a CloudWatch Logs log group. When flow_log_destination_arn is set to ARN of Cloudwatch Logs, this argument needs to be provided." 354 | type = string 355 | default = "" 356 | } 357 | 358 | variable "flow_log_cloudwatch_log_group_name_prefix" { 359 | description = "[optional] Specifies the name prefix of CloudWatch Log Group for VPC flow logs." 360 | type = string 361 | default = "/aws/vpc-flow-log/" 362 | } 363 | 364 | variable "flow_log_cloudwatch_log_group_retention_in_days" { 365 | description = "[optional] Specifies the number of days you want to retain log events in the specified log group for VPC flow logs." 366 | type = number 367 | default = null 368 | } 369 | 370 | -------------------------------------------------------------------------------- /modules/vpc/2-vpc.tf: -------------------------------------------------------------------------------- 1 | ################### 2 | # VPC 3 | ################### 4 | 5 | resource "aws_vpc" "vpc" { 6 | cidr_block = var.vpc_main_cidr 7 | enable_dns_support = var.enable_dns_support 8 | enable_dns_hostnames = var.enable_dns_hostnames 9 | assign_generated_ipv6_cidr_block = var.enable_ipv6 10 | 11 | tags = merge( 12 | var.tags, 13 | { 14 | "Name" = "${var.vpc_name}${var.vpc_suffix}" 15 | }, 16 | ) 17 | } 18 | 19 | resource "aws_vpc_ipv4_cidr_block_association" "secondary_cidr" { 20 | count = length(var.secondary_cidr_blocks) 21 | vpc_id = aws_vpc.vpc.id 22 | cidr_block = element(var.secondary_cidr_blocks, count.index) 23 | } 24 | 25 | ################### 26 | # DHCP Options Set 27 | ################### 28 | resource "aws_vpc_dhcp_options" "this" { 29 | count = var.enable_dhcp_options ? 1 : 0 30 | 31 | domain_name = var.dhcp_options_domain_name 32 | domain_name_servers = var.dhcp_options_domain_name_servers 33 | ntp_servers = var.dhcp_options_ntp_servers 34 | netbios_name_servers = var.dhcp_options_netbios_name_servers 35 | netbios_node_type = var.dhcp_options_netbios_node_type 36 | 37 | tags = merge( 38 | var.tags, 39 | { 40 | "Name" = "${var.vpc_name}-DHCP-OPTIONS" 41 | }, 42 | ) 43 | } 44 | 45 | 46 | resource "aws_vpc_dhcp_options_association" "this" { 47 | count = var.enable_dhcp_options ? 1 : 0 48 | 49 | vpc_id = aws_vpc.vpc.id 50 | dhcp_options_id = aws_vpc_dhcp_options.this[0].id 51 | } 52 | 53 | ################ 54 | # Publiс Subnets 55 | ################ 56 | resource "aws_subnet" "subnet_public" { 57 | count = length(var.public_subnets_cidr_list) 58 | vpc_id = aws_vpc.vpc.id 59 | cidr_block = var.public_subnets_cidr_list[count.index] 60 | availability_zone = data.aws_availability_zones.available.names[count.index%var.number_AZ] 61 | map_public_ip_on_launch = var.map_public_ip_on_launch 62 | 63 | tags = merge( 64 | var.tags, 65 | { 66 | "Name" = "${var.vpc_name}-${var.public_subnets_suffix}-subnet-${count.index}" 67 | }, 68 | ) 69 | } 70 | 71 | 72 | ################ 73 | # Private Subnets 74 | ################ 75 | 76 | resource "aws_subnet" "subnet_priv" { 77 | count = length(var.private_subnets_cidr_list) 78 | vpc_id = aws_vpc.vpc.id 79 | cidr_block = var.private_subnets_cidr_list[count.index] 80 | availability_zone = data.aws_availability_zones.available.names[count.index%var.number_AZ] 81 | tags = merge( 82 | var.tags, 83 | { 84 | "Name" = "${var.vpc_name}-${var.private_subnets_suffix}-subnet-${count.index}" 85 | }, 86 | ) 87 | } 88 | 89 | ################ 90 | # Private TGW Endpoint Subnets 91 | ################ 92 | 93 | resource "aws_subnet" "subnet_priv_tgw" { 94 | count = length(var.tgw_subnets_cidr_list) 95 | vpc_id = aws_vpc.vpc.id 96 | cidr_block = var.tgw_subnets_cidr_list[count.index] 97 | availability_zone = data.aws_availability_zones.available.names[count.index%var.number_AZ] 98 | tags = merge( 99 | var.tags, 100 | { 101 | "Name" = "${var.vpc_name}-${var.tgw_subnets_suffix}-subnet-${count.index}" 102 | }, 103 | ) 104 | } 105 | 106 | 107 | ################ 108 | # Network Firewall Endpoint Subnets 109 | ################ 110 | 111 | resource "aws_subnet" "subnet_fw" { 112 | count = length(var.fw_subnets_cidr_list) 113 | vpc_id = aws_vpc.vpc.id 114 | cidr_block = var.fw_subnets_cidr_list[count.index] 115 | availability_zone = data.aws_availability_zones.available.names[count.index%var.number_AZ] 116 | tags = merge( 117 | var.tags, 118 | { 119 | "Name" = "${var.vpc_name}-${var.fw_subnets_suffix}-subnet-${count.index}" 120 | }, 121 | ) 122 | } 123 | 124 | ################ 125 | # Web Tier Subnets 126 | ################ 127 | 128 | resource "aws_subnet" "subnet_web_tier" { 129 | count = length(var.web_tier_subnets_cidr_list) 130 | vpc_id = aws_vpc.vpc.id 131 | cidr_block = var.web_tier_subnets_cidr_list[count.index] 132 | availability_zone = data.aws_availability_zones.available.names[count.index%var.number_AZ] 133 | 134 | tags = merge( 135 | var.tags, 136 | { 137 | "Name" = "${var.vpc_name}-${var.web_subnets_suffix}-subnet-${count.index}" 138 | }, 139 | ) 140 | } 141 | 142 | ################ 143 | # Presentation Tier Subnets 144 | ################ 145 | 146 | resource "aws_subnet" "subnet_pres_tier" { 147 | count = length(var.pres_tier_subnets_cidr_list) 148 | vpc_id = aws_vpc.vpc.id 149 | cidr_block = var.pres_tier_subnets_cidr_list[count.index] 150 | availability_zone = data.aws_availability_zones.available.names[count.index%var.number_AZ] 151 | 152 | tags = merge( 153 | var.tags, 154 | { 155 | "Name" = "${var.vpc_name}-${var.pres_subnets_suffix}-subnet-${count.index}" 156 | }, 157 | ) 158 | } 159 | 160 | ################ 161 | # Database Tier Subnets 162 | ################ 163 | 164 | resource "aws_subnet" "subnet_database_tier" { 165 | count = length(var.database_tier_subnets_cidr_list) 166 | vpc_id = aws_vpc.vpc.id 167 | cidr_block = var.database_tier_subnets_cidr_list[count.index] 168 | availability_zone = data.aws_availability_zones.available.names[count.index%var.number_AZ] 169 | 170 | tags = merge( 171 | var.tags, 172 | { 173 | "Name" = "${var.vpc_name}-${var.database_subnets_suffix}-subnet-${count.index}" 174 | }, 175 | ) 176 | } 177 | 178 | ################ 179 | # Outposts Subnets 180 | ################ 181 | 182 | #Question: do we need to have the option to create one? how to see in the console the one associated with lgw? 183 | data "aws_ec2_coip_pool" "coip" { 184 | count = var.outposts_arn != "" ? 1 : 0 185 | filter { 186 | name = "coip-pool.local-gateway-route-table-id" 187 | values = [data.aws_ec2_local_gateway_route_table.outposts_lgw_route_table[0].local_gateway_route_table_id] 188 | } 189 | } 190 | 191 | data "aws_outposts_outpost" "target_outpost" { 192 | count = var.outposts_arn != "" ? 1 : 0 193 | arn = var.outposts_arn 194 | } 195 | 196 | resource "aws_subnet" "subnet_outposts" { 197 | count = var.outposts_arn != "" ? length(var.outposts_subnets_cidr_list) : 0 198 | vpc_id = aws_vpc.vpc.id 199 | cidr_block = var.outposts_subnets_cidr_list[count.index] 200 | availability_zone_id = data.aws_outposts_outpost.target_outpost[0].availability_zone_id 201 | 202 | map_customer_owned_ip_on_launch = var.coip_auto_assign ? true : null 203 | customer_owned_ipv4_pool = var.coip_auto_assign ? data.aws_ec2_coip_pool.coip[0].id : null 204 | 205 | outpost_arn = var.outposts_arn 206 | 207 | tags = merge( 208 | var.tags, 209 | { 210 | "Name" = "${var.vpc_name}-${var.outposts_subnets_suffix}-subnet-${count.index}" 211 | }, 212 | ) 213 | } 214 | 215 | 216 | ################### 217 | # Internet Gateway 218 | ################### 219 | 220 | resource "aws_internet_gateway" "igw" { 221 | count = var.enable_internet_gateway && length(var.public_subnets_cidr_list) > 0 ? 1 : 0 222 | vpc_id = aws_vpc.vpc.id 223 | tags = merge( 224 | var.tags, 225 | { 226 | "Name" = "${var.vpc_name}-IGW" 227 | }, 228 | ) 229 | } 230 | 231 | ################ 232 | # NAT GW 233 | ################ 234 | resource "aws_egress_only_internet_gateway" "egress_gw" { 235 | count = var.enable_nat_gateway && var.enable_ipv6 && length(var.public_subnets_cidr_list) > 0 ? 1 : 0 236 | vpc_id = aws_vpc.vpc.id 237 | } 238 | 239 | # HIGH AVAILABILITY 240 | resource "aws_eip" "nat" { 241 | count = var.enable_nat_gateway && length(var.public_subnets_cidr_list) >= var.number_AZ ? var.number_AZ : 0 242 | vpc = true 243 | 244 | tags = merge( 245 | var.tags, 246 | { 247 | "Name" = "${var.vpc_name}-NAT-EIP-${count.index}" 248 | }, 249 | ) 250 | 251 | } 252 | 253 | resource "aws_nat_gateway" "natgw" { 254 | count = var.enable_nat_gateway && length(var.public_subnets_cidr_list) >= var.number_AZ ? var.number_AZ : 0 255 | 256 | allocation_id = aws_eip.nat[count.index].id 257 | subnet_id = aws_subnet.subnet_public[count.index].id 258 | 259 | tags = merge( 260 | var.tags, 261 | { 262 | "Name" = "${var.vpc_name}-NAT-GW-${count.index}" 263 | }, 264 | ) 265 | 266 | } 267 | 268 | #IF just one public subnet - not highly available 269 | 270 | resource "aws_eip" "nat-one-az" { 271 | count = var.enable_nat_gateway && length(var.public_subnets_cidr_list) < var.number_AZ ? 1 : 0 272 | vpc = true 273 | 274 | tags = merge( 275 | var.tags, 276 | { 277 | "Name" = "${var.vpc_name}-NAT-EIP-one-AZ" 278 | }, 279 | ) 280 | 281 | } 282 | 283 | resource "aws_nat_gateway" "natgw-one-az" { 284 | count = var.enable_nat_gateway && length(var.public_subnets_cidr_list) < var.number_AZ ? 1 : 0 285 | 286 | allocation_id = aws_eip.nat-one-az[count.index].id 287 | subnet_id = aws_subnet.subnet_public[count.index].id 288 | 289 | tags = merge( 290 | var.tags, 291 | { 292 | "Name" = "${var.vpc_name}-NAT-GW-one-AZ" 293 | }, 294 | ) 295 | 296 | } 297 | 298 | 299 | ################ 300 | # Publiс routes 301 | ################ 302 | resource "aws_route_table" "subnet_public_route_table" { 303 | depends_on = [aws_internet_gateway.igw] 304 | count = length(var.public_subnets_cidr_list) > 0 ? length(var.public_subnets_cidr_list) : 0 305 | vpc_id = aws_vpc.vpc.id 306 | 307 | tags = merge( 308 | var.tags, 309 | { 310 | "Purpose" = "Route to internet for public subnets" 311 | "Name" = "${var.vpc_name}-PublicRouteTable-${count.index}" 312 | }, 313 | ) 314 | } 315 | 316 | resource "aws_route" "public_internet_gateway" { 317 | count = var.enable_internet_gateway && length(var.public_subnets_cidr_list)> 0 && var.public_subnets_internet_access_igw ? length(var.public_subnets_cidr_list) : 0 318 | 319 | route_table_id = aws_route_table.subnet_public_route_table[count.index].id 320 | destination_cidr_block = "0.0.0.0/0" 321 | gateway_id = aws_internet_gateway.igw[0].id 322 | } 323 | 324 | resource "aws_route" "public_internet_gateway_ipv6" { 325 | count = var.enable_ipv6 && length(var.public_subnets_cidr_list) > 0 ? length(var.public_subnets_cidr_list) : 0 326 | 327 | route_table_id = aws_route_table.subnet_public_route_table[count.index].id 328 | destination_ipv6_cidr_block = "::/0" 329 | gateway_id = aws_internet_gateway.igw[0].id 330 | } 331 | 332 | resource "aws_route_table_association" "public_route_assoc" { 333 | count = length(var.public_subnets_cidr_list) 334 | subnet_id = aws_subnet.subnet_public[count.index].id 335 | route_table_id = aws_route_table.subnet_public_route_table[count.index].id 336 | } 337 | 338 | 339 | ################# 340 | # Private routes 341 | ################# 342 | 343 | resource "aws_route_table" "subnet_priv_route_table" { 344 | count = length(var.private_subnets_cidr_list) 345 | vpc_id = aws_vpc.vpc.id 346 | 347 | tags = merge( 348 | var.tags, 349 | { 350 | "Purpose" = "Route table for private subnets" 351 | "Name" = "${var.vpc_name}-PrivateRoutable-${count.index}" 352 | }, 353 | ) 354 | } 355 | 356 | resource "aws_route" "private_nat_gateway" { 357 | depends_on = [aws_nat_gateway.natgw] 358 | count = var.enable_nat_gateway && length(var.public_subnets_cidr_list) > 0 && var.private_subnets_internet_access_nat_gw ? length(var.private_subnets_cidr_list) : 0 359 | 360 | route_table_id = aws_route_table.subnet_priv_route_table[count.index].id 361 | destination_cidr_block = "0.0.0.0/0" 362 | nat_gateway_id = length(var.public_subnets_cidr_list) == 1 ? aws_nat_gateway.natgw-one-az[0].id : aws_nat_gateway.natgw[count.index%var.number_AZ].id 363 | 364 | timeouts { 365 | create = "5m" 366 | } 367 | } 368 | 369 | resource "aws_route" "private_ipv6_egress" { 370 | count = var.enable_nat_gateway && var.enable_ipv6 && var.private_subnets_internet_access_nat_gw ? length(var.private_subnets_cidr_list) : 0 371 | route_table_id = aws_route_table.subnet_priv_route_table[count.index].id 372 | destination_ipv6_cidr_block = "::/0" 373 | egress_only_gateway_id = aws_egress_only_internet_gateway.egress_gw[0].id 374 | } 375 | 376 | resource "aws_route_table_association" "priv_route_assoc" { 377 | count = length(var.private_subnets_cidr_list) 378 | subnet_id = aws_subnet.subnet_priv[count.index].id 379 | route_table_id = aws_route_table.subnet_priv_route_table[count.index].id 380 | } 381 | 382 | ################# 383 | # Transit GW route table 384 | ################# 385 | 386 | resource "aws_route_table" "subnet_priv_tgw_route_table" { 387 | count = length(var.tgw_subnets_cidr_list) 388 | vpc_id = aws_vpc.vpc.id 389 | 390 | tags = merge( 391 | var.tags, 392 | { 393 | "Purpose" = "Route table for TGW endpoint subnets" 394 | "Name" = "${var.vpc_name}-TGWPrivateRoutable-${count.index}" 395 | }, 396 | ) 397 | } 398 | 399 | resource "aws_route" "private_gw_nat_gateway" { 400 | depends_on = [aws_nat_gateway.natgw] 401 | count = var.enable_nat_gateway && length(var.public_subnets_cidr_list) > 0 && var.tgw_subnets_internet_access_nat_gw? length(var.tgw_subnets_cidr_list) : 0 402 | 403 | route_table_id = aws_route_table.subnet_priv_tgw_route_table[count.index].id 404 | destination_cidr_block = "0.0.0.0/0" 405 | nat_gateway_id = length(var.public_subnets_cidr_list) == 1 ? aws_nat_gateway.natgw-one-az[0].id : aws_nat_gateway.natgw[count.index%var.number_AZ].id 406 | 407 | timeouts { 408 | create = "5m" 409 | } 410 | } 411 | 412 | 413 | resource "aws_route_table_association" "priv_tgw_route_assoc" { 414 | count = length(var.tgw_subnets_cidr_list) 415 | subnet_id = aws_subnet.subnet_priv_tgw[count.index].id 416 | route_table_id = aws_route_table.subnet_priv_tgw_route_table[count.index].id 417 | } 418 | 419 | ################# 420 | # Network FW route table 421 | ################# 422 | 423 | resource "aws_route_table" "subnet_fw_route_table" { 424 | count = length(var.fw_subnets_cidr_list) 425 | vpc_id = aws_vpc.vpc.id 426 | 427 | tags = merge( 428 | var.tags, 429 | { 430 | "Purpose" = "Route table for network FW subnets" 431 | "Name" = "${var.vpc_name}-NetworkFWRoutable-${count.index}" 432 | }, 433 | ) 434 | } 435 | 436 | resource "aws_route" "fw_nat_gateway" { 437 | depends_on = [aws_nat_gateway.natgw] 438 | count = var.enable_nat_gateway && length(var.public_subnets_cidr_list) > 0 && var.tgw_subnets_internet_access_nat_gw ? length(var.fw_subnets_cidr_list) : 0 439 | 440 | route_table_id = aws_route_table.subnet_fw_route_table[count.index].id 441 | destination_cidr_block = "0.0.0.0/0" 442 | nat_gateway_id = length(var.public_subnets_cidr_list) == 1 ? aws_nat_gateway.natgw-one-az[0].id : aws_nat_gateway.natgw[count.index%var.number_AZ].id 443 | 444 | timeouts { 445 | create = "5m" 446 | } 447 | } 448 | 449 | 450 | resource "aws_route_table_association" "fw_route_assoc" { 451 | count = length(var.fw_subnets_cidr_list) 452 | subnet_id = aws_subnet.subnet_fw[count.index].id 453 | route_table_id = aws_route_table.subnet_fw_route_table[count.index].id 454 | } 455 | 456 | 457 | #WEB TIER 458 | resource "aws_route_table" "subnet_web_tier_route_table" { 459 | count = length(var.web_tier_subnets_cidr_list) 460 | vpc_id = aws_vpc.vpc.id 461 | 462 | tags = merge( 463 | var.tags, 464 | { 465 | "Purpose" = "Route table for web tier subnets" 466 | "Name" = "${var.vpc_name}-WebTierRoutable-${count.index}" 467 | }, 468 | ) 469 | } 470 | 471 | resource "aws_route" "web_nat_gateway" { 472 | depends_on = [aws_nat_gateway.natgw] 473 | count = var.enable_nat_gateway && length(var.public_subnets_cidr_list) > 0 && var.web_subnets_internet_access_nat_gw ? length(var.web_tier_subnets_cidr_list) : 0 474 | 475 | route_table_id = aws_route_table.subnet_web_tier_route_table[count.index].id 476 | destination_cidr_block = "0.0.0.0/0" 477 | nat_gateway_id = length(var.public_subnets_cidr_list) == 1 ? aws_nat_gateway.natgw-one-az[0].id : aws_nat_gateway.natgw[count.index%var.number_AZ].id 478 | 479 | timeouts { 480 | create = "5m" 481 | } 482 | } 483 | 484 | resource "aws_route" "web_ipv6_egress" { 485 | count = var.enable_nat_gateway && var.enable_ipv6 && var.web_subnets_internet_access_nat_gw ? length(var.web_tier_subnets_cidr_list) : 0 486 | route_table_id = aws_route_table.subnet_web_tier_route_table[count.index].id 487 | destination_ipv6_cidr_block = "::/0" 488 | egress_only_gateway_id = aws_egress_only_internet_gateway.egress_gw[0].id 489 | } 490 | 491 | resource "aws_route_table_association" "web_tier_route_assoc" { 492 | count = length(var.web_tier_subnets_cidr_list) 493 | subnet_id = aws_subnet.subnet_web_tier[count.index].id 494 | route_table_id = aws_route_table.subnet_web_tier_route_table[count.index].id 495 | } 496 | 497 | #PRESENTATION TIER 498 | resource "aws_route_table" "subnet_pres_tier_route_table" { 499 | count = length(var.pres_tier_subnets_cidr_list) 500 | vpc_id = aws_vpc.vpc.id 501 | 502 | tags = merge( 503 | var.tags, 504 | { 505 | "Purpose" = "Route table for presentation tier subnets" 506 | "Name" = "${var.vpc_name}-PresTierRoutable-${count.index}" 507 | }, 508 | ) 509 | } 510 | 511 | resource "aws_route_table_association" "pres_tier_route_assoc" { 512 | count = length(var.pres_tier_subnets_cidr_list) 513 | subnet_id = aws_subnet.subnet_pres_tier[count.index].id 514 | route_table_id = aws_route_table.subnet_pres_tier_route_table[count.index].id 515 | } 516 | 517 | #DATABASE TIER 518 | resource "aws_route_table" "subnet_database_tier_route_table" { 519 | count = length(var.database_tier_subnets_cidr_list) 520 | vpc_id = aws_vpc.vpc.id 521 | 522 | tags = merge( 523 | var.tags, 524 | { 525 | "Purpose" = "Route table for database tier subnets" 526 | "Name" = "${var.vpc_name}-DatabaseTierRoutable-${count.index}" 527 | }, 528 | ) 529 | } 530 | 531 | resource "aws_route_table_association" "database_tier_route_assoc" { 532 | count = length(var.database_tier_subnets_cidr_list) 533 | subnet_id = aws_subnet.subnet_database_tier[count.index].id 534 | route_table_id = aws_route_table.subnet_database_tier_route_table[count.index].id 535 | } 536 | 537 | 538 | #OUTPOSTS 539 | resource "aws_route_table" "subnet_outposts_route_table" { 540 | count = var.outposts_arn != "" ? length(var.outposts_subnets_cidr_list) : 0 541 | vpc_id = aws_vpc.vpc.id 542 | 543 | tags = merge( 544 | var.tags, 545 | { 546 | "Purpose" = "Route table for outpost subnets" 547 | "Name" = "${var.vpc_name}-outpostsRoutable-${count.index}" 548 | }, 549 | ) 550 | } 551 | 552 | resource "aws_route_table_association" "outposts_route_assoc" { 553 | count = var.outposts_arn != "" ? length(var.outposts_subnets_cidr_list) : 0 554 | subnet_id = aws_subnet.subnet_outposts[count.index].id 555 | route_table_id = aws_route_table.subnet_outposts_route_table[count.index].id 556 | } 557 | 558 | data "aws_ec2_local_gateway_route_table" "outposts_lgw_route_table" { 559 | count = var.outposts_arn != "" ? 1 : 0 560 | outpost_arn = var.outposts_arn 561 | } 562 | 563 | resource "aws_ec2_local_gateway_route_table_vpc_association" "outposts_lgw_route_table_assoc" { 564 | count = var.outposts_arn != "" ? 1 : 0 565 | local_gateway_route_table_id = data.aws_ec2_local_gateway_route_table.outposts_lgw_route_table[0].id 566 | vpc_id = aws_vpc.vpc.id 567 | } 568 | 569 | data "aws_ec2_local_gateway" "outposts_lgw" { 570 | count = var.outposts_arn != "" ? 1 : 0 571 | filter { 572 | name = "outpost-arn" 573 | values = [var.outposts_arn] 574 | } 575 | } 576 | 577 | 578 | #route to local gw 579 | resource "aws_route" "outposts_route_to_LGW" { 580 | depends_on = [ 581 | aws_ec2_local_gateway_route_table_vpc_association.outposts_lgw_route_table_assoc 582 | ] 583 | count = var.outposts_arn != "" && var.outposts_route_to_LGW_destination != "" ? length(var.outposts_subnets_cidr_list) : 0 584 | route_table_id = aws_route_table.subnet_outposts_route_table[count.index].id 585 | destination_cidr_block = var.outposts_route_to_LGW_destination 586 | local_gateway_id = data.aws_ec2_local_gateway.outposts_lgw[0].id 587 | } 588 | 589 | -------------------------------------------------------------------------------- /dashboard.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT 4 | # 5 | # Licensed under the MIT License. See the LICENSE accompanying this file 6 | # for the specific language governing permissions and limitations under 7 | # the License. 8 | # 9 | 10 | AWSTemplateFormatVersion: "2010-09-09" 11 | Description: Sample AWS WAF Dashboard build on Amazon Elasticsearch Service. 12 | 13 | Parameters: 14 | DataNodeEBSVolumeSize: 15 | Type: Number 16 | Default: 100 17 | Description: Elasticsearch volume disk size 18 | 19 | NodeType: 20 | Type: String 21 | Default: m5.large.elasticsearch 22 | Description: Elasticsearch Node Type 23 | 24 | ElasticSerchDomainName: 25 | Type: String 26 | Default: 'waf-dashboards' 27 | AllowedPattern: "[a-z\\-]*" 28 | Description: Elasticsearch domain name 29 | 30 | UserEmail: 31 | Type: String 32 | Default: 'your@email.com' 33 | Description: Dashboard user e-mail address 34 | 35 | ESConfigBucket: 36 | Type: String 37 | AllowedPattern: ^[A-Za-z0-9][A-Za-z0-9\-_]{3,62}$ 38 | Description: ElasticSearch configuration lambda code bucket 39 | 40 | Resources: 41 | 42 | UserPool: 43 | Type: AWS::Cognito::UserPool 44 | Properties: 45 | UserPoolName: !Sub ${AWS::StackName}-user-pool 46 | AutoVerifiedAttributes: 47 | - email 48 | Schema: 49 | - Name: email 50 | AttributeDataType: String 51 | Mutable: false 52 | Required: true 53 | - Name: name 54 | AttributeDataType: String 55 | Mutable: true 56 | Required: true 57 | 58 | UserPoolDomain: 59 | Type: AWS::Cognito::UserPoolDomain 60 | Properties: 61 | UserPoolId: !Ref UserPool 62 | Domain: !Sub ${AWS::StackName}-user-pool-domain 63 | 64 | UserPoolClient: 65 | Type: AWS::Cognito::UserPoolClient 66 | Properties: 67 | ClientName: !Sub ${AWS::StackName}-user-pool-client 68 | GenerateSecret: false 69 | UserPoolId: !Ref UserPool 70 | 71 | IdentityPool: 72 | Type: AWS::Cognito::IdentityPool 73 | Properties: 74 | IdentityPoolName: "WAFKibanaIdentityPool" 75 | AllowUnauthenticatedIdentities: false 76 | CognitoIdentityProviders: 77 | - ClientId: !Ref UserPoolClient 78 | ProviderName: !GetAtt UserPool.ProviderName 79 | 80 | UserPoolUser: 81 | Type: AWS::Cognito::UserPoolUser 82 | Properties: 83 | DesiredDeliveryMediums: 84 | - EMAIL 85 | UserAttributes: 86 | - Name: email 87 | Value: !Ref UserEmail 88 | Username: !Ref UserEmail 89 | UserPoolId: !Ref UserPool 90 | 91 | CognitoAuthenticatedRole: 92 | Type: AWS::IAM::Role 93 | Properties: 94 | AssumeRolePolicyDocument: 95 | Version: "2012-10-17" 96 | Statement: 97 | - Effect: Allow 98 | Action: "sts:AssumeRoleWithWebIdentity" 99 | Principal: 100 | Federated: cognito-identity.amazonaws.com 101 | Condition: 102 | StringEquals: 103 | "cognito-identity.amazonaws.com:aud": !Ref IdentityPool 104 | ForAnyValue:StringLike: 105 | "cognito-identity.amazonaws.com:amr": authenticated 106 | Policies: 107 | - PolicyName: CognitoAuthorizedPolicy 108 | PolicyDocument: 109 | Version: 2012-10-17 110 | Statement: 111 | - Effect: Allow 112 | Action: es:ESHttp* 113 | Resource: !Sub arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticSerchDomainName}/* 114 | 115 | IdentityPoolRoleMapping: 116 | Type: AWS::Cognito::IdentityPoolRoleAttachment 117 | Properties: 118 | IdentityPoolId: !Ref IdentityPool 119 | Roles: 120 | authenticated: !GetAtt CognitoAuthenticatedRole.Arn 121 | 122 | LogEnrichmentLambdaRole: 123 | Type: AWS::IAM::Role 124 | Properties: 125 | AssumeRolePolicyDocument: 126 | Statement: 127 | - Action: sts:AssumeRole 128 | Effect: Allow 129 | Principal: 130 | Service: lambda.amazonaws.com 131 | Version: 2012-10-17 132 | ManagedPolicyArns: 133 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 134 | 135 | LogEnrichmentLambda: 136 | Type: AWS::Lambda::Function 137 | Properties: 138 | FunctionName: !Sub ${AWS::StackName}-log-enrichment 139 | Handler: index.lambda_handler 140 | MemorySize: 256 141 | Runtime: python3.7 142 | Timeout: 120 143 | Role: !GetAtt LogEnrichmentLambdaRole.Arn 144 | Code: 145 | ZipFile: | 146 | import base64 147 | import json 148 | import pyasn 149 | from user_agents import parse 150 | from counter_robots import is_machine, is_robot 151 | print('Loading AWS WAF Logs Enrichment function') 152 | asndb = pyasn.pyasn('/opt/ipasn.dat') 153 | ua = {} 154 | def getUAInfo(user_agent_string): 155 | if user_agent_string in ua: 156 | return ua[user_agent_string] 157 | else: 158 | info = {} 159 | user_agent = parse(user_agent_string) 160 | browser = {} 161 | browser['family'] = user_agent.browser.family 162 | browser['version'] = user_agent.browser.version_string 163 | info['browser'] = browser 164 | os = {} 165 | os['family'] = user_agent.os.family 166 | os['version'] = user_agent.os.version_string 167 | info['os'] = os 168 | device = {} 169 | device['family'] = user_agent.device.family 170 | device['brand'] = user_agent.device.brand 171 | device['model'] = user_agent.device.model 172 | info['device'] = device 173 | ua[user_agent_string] = info 174 | return info 175 | def lambda_handler(event, context): 176 | output = [] 177 | for record in event['records']: 178 | payload = base64.b64decode(record['data']) 179 | log = json.loads(payload) 180 | # Flattens the header fields 181 | flatheaders = {} 182 | for item in log['httpRequest']['headers']: 183 | flatheaders[item['name'].lower()] = item['value'] 184 | del(log['httpRequest']['headers']) 185 | log['httpRequest']['headers'] = flatheaders 186 | # Parse and add webacl information 187 | webacl_info = log['webaclId'].split(":") 188 | log['webaclRegion'] = webacl_info[3] 189 | log['webaclAccount'] = webacl_info[4] 190 | webacl_name = webacl_info[5].split("/") 191 | log['webaclName'] = webacl_name[2] 192 | 193 | # Adds ASN number 194 | ip = "" 195 | if 'x-forwarded-for' in log['httpRequest']['headers']: 196 | ip = log['httpRequest']['headers']['x-forwarded-for'] 197 | # XFF header may come with more than one IP address, we'll use the first appearance in the list to report the ASN 198 | if len(ip) > 15: 199 | print(f'[WARNING] XFF with multiple IP addresses: {ip}; considering last') 200 | ip = ip.split(', ')[-1] 201 | else: 202 | ip = log['httpRequest']['clientIp'] 203 | try: 204 | log['httpRequest']['asn'] = asndb.lookup(ip)[0] 205 | except Exception as e: 206 | log['httpRequest']['asn'] = 'unknown' 207 | print(f'[ERROR] Got error {e}, while processing ASN for IP {ip}') 208 | print(e) 209 | if 'user-agent' in log['httpRequest']['headers']: 210 | user_agent_string = log['httpRequest']['headers']['user-agent'] 211 | # Adds user-agent information 212 | ua_info = getUAInfo(user_agent_string) 213 | log['httpRequest']['browser'] = ua_info['browser'] 214 | log['httpRequest']['os'] = ua_info['os'] 215 | log['httpRequest']['device'] = ua_info['device'] 216 | # Adds robot information 217 | device_type = '' 218 | if is_machine(user_agent_string): 219 | device_type = 'machine' 220 | elif is_robot(user_agent_string): 221 | device_type = 'robot' 222 | else: 223 | device_type = 'other' 224 | log['httpRequest']['deviceType'] = device_type 225 | payload = json.dumps(log) 226 | output_record = { 227 | 'recordId': record['recordId'], 228 | 'result': 'Ok', 229 | 'data': base64.b64encode(payload.encode('utf-8') + b'\n').decode('utf-8') 230 | } 231 | output.append(output_record) 232 | print('Successfully processed {} records.'.format(len(event['records']))) 233 | return {'records': output} 234 | CodebuildRole: 235 | DependsOn: LogEnrichmentLambda 236 | Type: AWS::IAM::Role 237 | Properties: 238 | AssumeRolePolicyDocument: 239 | Version: 2012-10-17 240 | Statement: 241 | - Action: 242 | - sts:AssumeRole 243 | Effect: Allow 244 | Principal: 245 | Service: codebuild.amazonaws.com 246 | ManagedPolicyArns: 247 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 248 | Policies: 249 | - PolicyName: CodeBuildPolicy 250 | PolicyDocument: 251 | Version: 2012-10-17 252 | Statement: 253 | - Effect: Allow 254 | Action: 255 | - lambda:GetLayerVersion 256 | - lambda:PublishLayerVersion 257 | Resource: "*" 258 | - Effect: Allow 259 | Action: lambda:UpdateFunctionConfiguration 260 | Resource: !GetAtt LogEnrichmentLambda.Arn 261 | 262 | LayerCodebuildProject: 263 | Type: AWS::CodeBuild::Project 264 | Properties: 265 | Artifacts: 266 | Type: NO_ARTIFACTS 267 | Description: !Sub "${AWS::StackName} Lambda Layer build" 268 | Environment: 269 | ComputeType: BUILD_GENERAL1_SMALL 270 | Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0 271 | Type: LINUX_CONTAINER 272 | EnvironmentVariables: 273 | - Name: LAMBDA_FUNCTION_NAME 274 | Type: PLAINTEXT 275 | Value: !Sub ${AWS::StackName}-log-enrichment 276 | - Name: LAMBDA_LAYER_NAME 277 | Type: PLAINTEXT 278 | Value: !Sub ${AWS::StackName}-log-enrichment-layer 279 | LogsConfig: 280 | CloudWatchLogs: 281 | Status: ENABLED 282 | Name: !Sub "${AWS::StackName}-lambda-layer-build" 283 | ServiceRole: !Ref CodebuildRole 284 | Source: 285 | Type: NO_SOURCE 286 | BuildSpec: | 287 | version: 0.2 288 | phases: 289 | install: 290 | commands: 291 | - echo "Install Step" 292 | #- yum update -y 293 | - yum install -y gcc python3.7 python3-devel python3-pip python-virtualenv zip 294 | pre_build: 295 | commands: 296 | - echo "Pre-build Step" 297 | - PYTHON_PATH=build/python/lib/python3.7/site-packages 298 | - virtualenv -p python3.7 venv 299 | build: 300 | commands: 301 | - echo "Build Step" 302 | - . venv/bin/activate 303 | - $VIRTUAL_ENV/bin/pip3 install -U --target $VIRTUAL_ENV/lib/python3.7/site-packages pyasn 304 | - $VIRTUAL_ENV/bin/pip3 install -U user_agents 305 | - $VIRTUAL_ENV/bin/pip3 install -U counter_robots 306 | - $VIRTUAL_ENV/lib/python3.7/site-packages/bin/pyasn_util_download.py --latest 307 | - RIB_FILE=`ls rib.*` 308 | - $VIRTUAL_ENV/lib/python3.7/site-packages/bin/pyasn_util_convert.py --single $RIB_FILE ipasn.dat 309 | - mkdir -p ./$PYTHON_PATH 310 | - cp -a $VIRTUAL_ENV/lib/python3.7/site-packages/. ./$PYTHON_PATH 311 | - rm -rf ./$PYTHON_PATH/wheel* 312 | - rm -rf ./$PYTHON_PATH/easy-install* 313 | - rm -rf ./$PYTHON_PATH/setuptools* 314 | - rm -rf ./$PYTHON_PATH/virtualenv* 315 | - rm -rf ./$PYTHON_PATH/pip* 316 | - rm -rf ./$PYTHON_PATH/_virtualenv* 317 | - rm -rf ./$PYTHON_PATH/bin/* 318 | - mv ipasn.dat ./build 319 | - cd ./build 320 | - zip -r layer.zip . 321 | post_build: 322 | commands: 323 | - LAMBDA_LAYER_VERSION=`aws lambda publish-layer-version --layer-name $LAMBDA_LAYER_NAME --description "AWS WAF Logs Enrichment Tools and Database" --zip-file fileb://layer.zip --compatible-runtimes python3.7 --query 'LayerVersionArn' --output text` 324 | - aws lambda update-function-configuration --function-name $LAMBDA_FUNCTION_NAME --layers $LAMBDA_LAYER_VERSION 325 | TimeoutInMinutes: 15 326 | 327 | BuildEventBridgeRole: 328 | Type: AWS::IAM::Role 329 | Properties: 330 | AssumeRolePolicyDocument: 331 | Statement: 332 | - Action: sts:AssumeRole 333 | Effect: Allow 334 | Principal: 335 | Service: events.amazonaws.com 336 | Version: 2012-10-17 337 | Policies: 338 | - PolicyName: CodeBuildProjectStartBuild 339 | PolicyDocument: 340 | Version: '2012-10-17' 341 | Statement: 342 | - Effect: Allow 343 | Action: codebuild:StartBuild 344 | Resource: !GetAtt LayerCodebuildProject.Arn 345 | 346 | RebuildEnrichmentLayerTrigger: 347 | Type: AWS::Events::Rule 348 | Properties: 349 | Description: Rebuilds Lambda Layer for AWS WAF Log Enrichment 350 | Name: !Sub ${AWS::StackName}-layer-monthly-build 351 | RoleArn: !GetAtt BuildEventBridgeRole.Arn 352 | ScheduleExpression: cron(0 0 1 * ? *) 353 | State: ENABLED 354 | Targets: 355 | - Arn: !GetAtt LayerCodebuildProject.Arn 356 | Id: !Sub ${AWS::StackName}-Codebuild-Project-Start-Build 357 | RoleArn: !GetAtt BuildEventBridgeRole.Arn 358 | 359 | ESCognitoRole: 360 | Type: "AWS::IAM::Role" 361 | Properties: 362 | AssumeRolePolicyDocument: 363 | Version: "2012-10-17" 364 | Statement: 365 | - Effect: Allow 366 | Principal: 367 | Service: es.amazonaws.com 368 | Action: "sts:AssumeRole" 369 | ManagedPolicyArns: 370 | - 'arn:aws:iam::aws:policy/AmazonESCognitoAccess' 371 | Path: / 372 | 373 | ElasticsearchDomain: 374 | Type: AWS::Elasticsearch::Domain 375 | Properties: 376 | DomainName: !Ref ElasticSerchDomainName 377 | ElasticsearchVersion: 7.7 378 | ElasticsearchClusterConfig: 379 | InstanceCount: 1 380 | InstanceType: !Ref NodeType 381 | EBSOptions: 382 | EBSEnabled: true 383 | Iops: 0 384 | VolumeSize: !Ref DataNodeEBSVolumeSize 385 | VolumeType: 'gp2' 386 | SnapshotOptions: 387 | AutomatedSnapshotStartHour: '0' 388 | CognitoOptions: 389 | Enabled: true 390 | IdentityPoolId: !Ref IdentityPool 391 | UserPoolId: !Ref UserPool 392 | RoleArn: !GetAtt ESCognitoRole.Arn 393 | AccessPolicies: 394 | Version: "2012-10-17" 395 | Statement: 396 | - Effect: Allow 397 | Principal: 398 | AWS: !GetAtt ESConfigRole.Arn 399 | Action: es:ESHttp* 400 | Resource: 401 | - !Sub arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticSerchDomainName}/* 402 | - Effect: Allow 403 | Principal: 404 | AWS: !GetAtt CognitoAuthenticatedRole.Arn 405 | Action: es:ESHttp* 406 | Resource: !Sub arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticSerchDomainName}/* 407 | 408 | S3Bucket: 409 | Type: AWS::S3::Bucket 410 | DeletionPolicy: Retain 411 | 412 | DeliveryStreamRole: 413 | Type: AWS::IAM::Role 414 | Properties: 415 | AssumeRolePolicyDocument: 416 | Version: 2012-10-17 417 | Statement: 418 | - Effect: Allow 419 | Action: sts:AssumeRole 420 | Principal: 421 | Service: firehose.amazonaws.com 422 | Policies: 423 | - PolicyName: DeliveryStreamRolePolicy 424 | PolicyDocument: 425 | Version: 2012-10-17 426 | Statement: 427 | - Effect: Allow 428 | Action: 429 | - s3:AbortMultipartUpload 430 | - s3:GetBucketLocation 431 | - s3:GetObject 432 | - s3:ListBucket 433 | - s3:ListBucketMultipartUploads 434 | - s3:PutObject 435 | Resource: 436 | - !Sub arn:aws:s3:::${S3Bucket} 437 | - !Sub arn:aws:s3:::${S3Bucket}/* 438 | - Effect: Allow 439 | Action: 440 | - lambda:InvokeFunction 441 | - lambda:GetFunctionConfiguration 442 | Resource: !GetAtt LogEnrichmentLambda.Arn 443 | - Effect: Allow 444 | Action: 445 | - es:DescribeElasticsearchDomain 446 | - es:DescribeElasticsearchDomains 447 | - es:DescribeElasticsearchDomainConfig 448 | - es:ESHttp* 449 | Resource: 450 | - !Sub arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticSerchDomainName} 451 | - !Sub arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticSerchDomainName}/* 452 | - Effect: Allow 453 | Action: 454 | - logs:PutLogEvents 455 | Resource: "*" 456 | - Effect: Allow 457 | Action: 458 | - kinesis:DescribeStream 459 | - kinesis:GetShardIterator 460 | - kinesis:GetRecords 461 | Resource: !Sub arn:aws:kinesis:${AWS::Region}:${AWS::AccountId}:stream/%FIREHOSE_STREAM_NAME% 462 | 463 | KinesisFirehoseDeliveryStream: 464 | Type: AWS::KinesisFirehose::DeliveryStream 465 | Properties: 466 | DeliveryStreamName: !Sub aws-waf-logs-${AWS::StackName}-delivery 467 | DeliveryStreamType: DirectPut 468 | ElasticsearchDestinationConfiguration: 469 | BufferingHints: 470 | IntervalInSeconds: 60 471 | SizeInMBs: 5 472 | DomainARN: !GetAtt ElasticsearchDomain.Arn 473 | IndexName: waf-logs 474 | IndexRotationPeriod: OneDay 475 | ProcessingConfiguration: 476 | Enabled: true 477 | Processors: 478 | - Type: Lambda 479 | Parameters: 480 | - ParameterName: LambdaArn 481 | ParameterValue: !GetAtt LogEnrichmentLambda.Arn 482 | RetryOptions: 483 | DurationInSeconds: 10 484 | RoleARN: !GetAtt DeliveryStreamRole.Arn 485 | S3BackupMode: AllDocuments 486 | S3Configuration: 487 | BucketARN: !GetAtt S3Bucket.Arn 488 | RoleARN: !GetAtt DeliveryStreamRole.Arn 489 | BufferingHints: 490 | IntervalInSeconds: 60 491 | SizeInMBs: 50 492 | CompressionFormat: GZIP 493 | DependsOn: WAFOperationsSetUpLambda 494 | 495 | ESConfigRole: 496 | Type: AWS::IAM::Role 497 | Properties: 498 | AssumeRolePolicyDocument: 499 | Version: 2012-10-17 500 | Statement: 501 | - Effect: Allow 502 | Principal: 503 | Service: lambda.amazonaws.com 504 | Action: sts:AssumeRole 505 | ManagedPolicyArns: 506 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 507 | Policies: 508 | - PolicyName: ESConfigPolicy 509 | PolicyDocument: 510 | Version: 2012-10-17 511 | Statement: 512 | - Effect: Allow 513 | Action: es:ESHttp* 514 | Resource: !Sub arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${ElasticSerchDomainName}/* 515 | ESConfigFunction: 516 | Type: AWS::Lambda::Function 517 | Properties: 518 | FunctionName: !Sub ${AWS::StackName}-es-config-function 519 | Handler: lambda_function.lambda_handler 520 | Runtime: python3.7 521 | Timeout: 30 522 | Role: !GetAtt ESConfigRole.Arn 523 | Code: 524 | S3Bucket: !Ref ESConfigBucket 525 | S3Key: esconfig.zip 526 | 527 | ESConfiguration: 528 | Type: Custom::WAFDashboards 529 | Properties: 530 | ServiceToken: !GetAtt ESConfigFunction.Arn 531 | Host: !GetAtt ElasticsearchDomain.DomainEndpoint 532 | Region: !Ref AWS::Region 533 | 534 | WAFOperationsSetUpRole: 535 | Type: AWS::IAM::Role 536 | Properties: 537 | AssumeRolePolicyDocument: 538 | Statement: 539 | - Action: 540 | - sts:AssumeRole 541 | Effect: Allow 542 | Principal: 543 | Service: 544 | - lambda.amazonaws.com 545 | Version: 2012-10-17 546 | Policies: 547 | - PolicyName: WAFOperationsSetUpLambdaPolicy 548 | PolicyDocument: 549 | Version: 2012-10-17 550 | Statement: 551 | - Effect: Allow 552 | Action: codebuild:StartBuild 553 | Resource: !GetAtt LayerCodebuildProject.Arn 554 | - Effect: Allow 555 | Action: 556 | - s3:DeleteObject 557 | - s3:ListBucket 558 | Resource: 559 | - !Sub arn:aws:s3:::${S3Bucket} 560 | - !Sub arn:aws:s3:::${S3Bucket}/* 561 | ManagedPolicyArns: 562 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 563 | 564 | WAFOperationsSetUpLambda: 565 | Type: AWS::Lambda::Function 566 | DependsOn: 567 | - S3Bucket 568 | - LayerCodebuildProject 569 | Properties: 570 | Code: 571 | ZipFile: 572 | !Sub | 573 | import boto3 574 | import cfnresponse 575 | def lambda_handler(event, context): 576 | print(event) 577 | try: 578 | if event['RequestType'] == 'Create': 579 | client = boto3.client(service_name='codebuild', region_name='${AWS::Region}') 580 | new_build = client.start_build(projectName='${AWS::StackName}-lambda-layer-build') 581 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) 582 | return 583 | if event['RequestType'] == 'Delete': 584 | s3 = boto3.resource('s3') 585 | bucket = s3.Bucket('${S3Bucket}') 586 | bucket.objects.all().delete() 587 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) 588 | return 589 | if event['RequestType'] == 'Update': 590 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) 591 | return 592 | except Exception as err: 593 | cfnresponse.send(event, context, cfnresponse.FAILED, {}) 594 | return 595 | FunctionName: !Sub ${AWS::StackName}-waf-operations-set-up 596 | Handler: index.lambda_handler 597 | Role: !GetAtt WAFOperationsSetUpRole.Arn 598 | Runtime: python3.7 599 | Timeout: 180 600 | 601 | FirstInvokeWAFOperationsSetUpLambda: 602 | Type: AWS::CloudFormation::CustomResource 603 | DependsOn: 604 | - WAFOperationsSetUpLambda 605 | - S3Bucket 606 | Version: "1.0" 607 | Properties: 608 | ServiceToken: !GetAtt WAFOperationsSetUpLambda.Arn 609 | 610 | WAFv2Modification: 611 | Type: AWS::Events::Rule 612 | Properties: 613 | Description: WAF Dashboard - detects new WebACL and rules for WAFv2. 614 | EventPattern: 615 | source: 616 | - "aws.wafv2" 617 | detail-type: 618 | - "AWS API Call via CloudTrail" 619 | detail: 620 | eventSource: 621 | - wafv2.amazonaws.com 622 | eventName: 623 | - CreateWebACL 624 | - CreateRule 625 | Name: WAFv2Modification 626 | State: "ENABLED" 627 | Targets: 628 | - 629 | Arn: !GetAtt KibanaUpdate.Arn 630 | Id: "1" 631 | 632 | KibanaUpdate: 633 | Type: "AWS::Lambda::Function" 634 | Properties: 635 | Handler: "lambda_function.update_kibana" 636 | Role: !GetAtt KibanaCustomizerLambdaRole.Arn 637 | Code: 638 | S3Bucket: !Join 639 | - '' 640 | - - 'waf-dashboards-' 641 | - !Ref "AWS::Region" 642 | S3Key: "kibana-customizer-lambda.zip" 643 | Runtime: "python3.7" 644 | MemorySize: 128 645 | Timeout: 160 646 | Environment: 647 | Variables: 648 | ES_ENDPOINT : !GetAtt ElasticsearchDomain.DomainEndpoint 649 | REGION : !Ref "AWS::Region" 650 | ACCOUNT_ID : !Ref "AWS::AccountId" 651 | 652 | KibanaUpdateWAFv2Permission: 653 | Type: AWS::Lambda::Permission 654 | Properties: 655 | Action: 'lambda:InvokeFunction' 656 | FunctionName: !Ref KibanaUpdate 657 | Principal: "events.amazonaws.com" 658 | SourceArn: !GetAtt WAFv2Modification.Arn 659 | 660 | KibanaCustomizerLambdaRole: 661 | Type: "AWS::IAM::Role" 662 | Properties: 663 | AssumeRolePolicyDocument: 664 | Version: "2012-10-17" 665 | Statement: 666 | - Effect: Allow 667 | Principal: 668 | Service: lambda.amazonaws.com 669 | Action: "sts:AssumeRole" 670 | Policies: 671 | - PolicyName: KibanaCustomizerPolicy 672 | PolicyDocument: 673 | Version: "2012-10-17" 674 | Statement: 675 | - Effect: Allow 676 | Action: 677 | - 'es:UpdateElasticsearchDomainConfig' 678 | - 'logs:CreateLogGroup' 679 | - 'logs:CreateLogStream' 680 | - 'logs:PutLogEvents' 681 | - 'events:PutRule' 682 | - 'events:DeleteRule' 683 | - 'lambda:AddPermission' 684 | - 'events:PutTargets' 685 | - 'events:RemoveTargets' 686 | - 'lambda:RemovePermission' 687 | - 'iam:PassRole' 688 | - 'waf:ListWebACLs' 689 | - 'waf-regional:ListWebACLs' 690 | - 'waf:ListRules' 691 | - 'waf-regional:ListRules' 692 | - 'wafv2:ListWebACLs' 693 | Resource: "*" 694 | 695 | Outputs: 696 | 697 | UserPoolId: 698 | Description: UserPool::Id 699 | Value: !Ref UserPool 700 | 701 | UserPoolClientId: 702 | Description: UserPoolClient::Id 703 | Value: !Ref UserPoolClient 704 | 705 | IdentityPoolId: 706 | Description: IdentityPool::Id 707 | Value: !Ref IdentityPool 708 | 709 | ElasticSearchURL: 710 | Description: ElasticSearch URL 711 | Value: !GetAtt ElasticsearchDomain.DomainEndpoint 712 | 713 | DashboardLinkOutput: 714 | Description: Link to WAF Dashboard 715 | Value: !Join 716 | - '' 717 | - - 'https://' 718 | - !GetAtt ElasticsearchDomain.DomainEndpoint 719 | - '/_plugin/kibana/' 720 | 721 | KinesisFirehoseDeliveryStreamArn: 722 | Description: kenesis Firehose 723 | Value: !GetAtt KinesisFirehoseDeliveryStream.Arn 724 | 725 | S3bucketName: 726 | Description: S3 bucket name 727 | Value: !Sub ${S3Bucket} 728 | --------------------------------------------------------------------------------