├── cloudformation ├── parameters.json ├── references.md ├── templates │ ├── firehose.yaml │ ├── fms-shield.yaml │ ├── shield-configure-subscribe.yaml │ └── fms-wafv2.yaml └── references │ └── fms-wafv2-example1.yaml ├── CODE_OF_CONDUCT.md ├── terraform ├── core-deployment │ ├── modules │ │ └── aws-shield-advanced-base-support-infra │ │ │ ├── outputs.tf │ │ │ ├── main.tf │ │ │ ├── variables.tf │ │ │ ├── firehose.tf │ │ │ ├── glue.tf │ │ │ ├── code │ │ │ └── athena_query_lambda.py │ │ │ ├── s3iam.tf │ │ │ └── athena.tf │ ├── variables.tf │ ├── outputs.tf │ ├── main.tf │ ├── configure_shield.py │ └── athena_query_lambda.py ├── organizational-deployment │ ├── modules │ │ ├── aws-shield-advanced-configure-subscribe │ │ │ ├── code │ │ │ │ └── configure_shield.py │ │ │ ├── variables.tf │ │ │ └── main.tf │ │ ├── aws-shield-advanced-fms-shield │ │ │ ├── variables.tf │ │ │ └── main.tf │ │ └── aws-shield-advanced-wafv2 │ │ │ ├── variables.tf │ │ │ └── main.tf │ ├── main.tf │ └── variables.tf ├── tfstacks.py └── README.md ├── README.md ├── LICENSE ├── .gitignore └── CONTRIBUTING.md /cloudformation/parameters.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-shield-advanced-one-click-deployment/HEAD/cloudformation/parameters.json -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /terraform/core-deployment/modules/aws-shield-advanced-base-support-infra/outputs.tf: -------------------------------------------------------------------------------- 1 | output "waf_log_s3_bucket_name" { 2 | value = aws_s3_bucket.waf_logs.id 3 | } 4 | 5 | output "waf_log_kms_key_arn" { 6 | value = aws_kms_key.waf_logs.arn 7 | } 8 | 9 | output "waf_delivery_role_arn" { 10 | value = aws_iam_role.waf_logs.arn 11 | } 12 | -------------------------------------------------------------------------------- /terraform/core-deployment/modules/aws-shield-advanced-base-support-infra/main.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "current" {} 2 | 3 | data "aws_region" "current" {} 4 | 5 | locals { 6 | account_id = data.aws_caller_identity.current.account_id 7 | aws_region_name = data.aws_region.current.name 8 | } 9 | 10 | output "account_id" { 11 | value = data.aws_caller_identity.current.account_id 12 | } -------------------------------------------------------------------------------- /terraform/core-deployment/modules/aws-shield-advanced-base-support-infra/variables.tf: -------------------------------------------------------------------------------- 1 | variable "scope_regions" { 2 | description = "A comma separated list of AWS regions, e.g. us-east-1, us-east-2" 3 | type = list(string) 4 | } 5 | 6 | variable "use_lake_formation_permissions" { 7 | type = bool 8 | default = true 9 | description = "Determines whether Athena is configured to utlize lake formation." 10 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Shield Advanced One Click 2 | 3 | # Overview 4 | AWS Shield Advanced One Click deployments allows customers getting started with Shield Advanced to get an out of the box recommended baseline configuration. We currently offer two versions of this solution concept to better align to how you already deploy on AWS. 5 | 6 | [CloudFormation](./cloudformation/README.md) 7 | 8 | [Terraform](./terraform/README.md) 9 | -------------------------------------------------------------------------------- /terraform/core-deployment/variables.tf: -------------------------------------------------------------------------------- 1 | ### sa_base_support_infra module 2 | 3 | variable "region" { 4 | type = string 5 | } 6 | variable "scope_regions" { 7 | description = "A comma separated list of AWS regions, e.g. us-east-1, us-east-2" 8 | type = list(string) 9 | } 10 | 11 | variable "use_lake_formation_permissions" { 12 | type = bool 13 | default = true 14 | } 15 | 16 | variable "target_account_id" { 17 | type = string 18 | } -------------------------------------------------------------------------------- /terraform/core-deployment/outputs.tf: -------------------------------------------------------------------------------- 1 | output "waf_log_s3_bucket_name" { 2 | value = module.sa_base_support_infra.waf_log_s3_bucket_name 3 | } 4 | 5 | output "waf_log_kms_key_arn" { 6 | value = module.sa_base_support_infra.waf_log_kms_key_arn 7 | } 8 | 9 | output "waf_delivery_role_arn" { 10 | value = module.sa_base_support_infra.waf_delivery_role_arn 11 | } 12 | 13 | output "primary_region" { 14 | value = var.region 15 | } 16 | 17 | output "scope_regions" { 18 | value = "${join(",",var.scope_regions)}" 19 | } 20 | -------------------------------------------------------------------------------- /terraform/core-deployment/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "5.40.0" 6 | } 7 | } 8 | 9 | backend "s3" { 10 | // Backend state MUST be properly confgured and is specific to deployment environment. 11 | bucket = "" 12 | workspace_key_prefix = "" 13 | key = "" 14 | region = "" 15 | } 16 | } 17 | 18 | provider "aws" { 19 | region = var.region 20 | } 21 | 22 | module "sa_base_support_infra" { 23 | source = "./modules/aws-shield-advanced-base-support-infra" 24 | 25 | scope_regions = var.scope_regions 26 | use_lake_formation_permissions = var.use_lake_formation_permissions 27 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | 3 | # Local .terraform directories 4 | ./terraform/.terraform/* 5 | **/.checkov/* 6 | 7 | # .tfstate files 8 | *.tfstate 9 | *.tfstate.* 10 | 11 | # Crash log files 12 | crash.log 13 | crash.*.log 14 | 15 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 16 | # password, private keys, and other secrets. These should not be part of version 17 | # control as they are data points which are potentially sensitive and subject 18 | # to change depending on the environment. 19 | *.tfvars 20 | *.tfvars.json 21 | 22 | # Ignore override files as they are usually used to override resources locally and so 23 | # are not checked in 24 | override.tf 25 | override.tf.json 26 | *_override.tf 27 | *_override.tf.json 28 | 29 | # Include override files you do wish to add to version control using negated pattern 30 | # !example_override.tf 31 | 32 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 33 | # example: *tfplan* 34 | 35 | # Ignore CLI configuration files 36 | .terraformrc 37 | terraform.rc 38 | **/*.zip 39 | 40 | Pipfile* 41 | *.plan 42 | terraform/terraform/* 43 | **/planoutput.txt/* -------------------------------------------------------------------------------- /cloudformation/references.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## How to enable AWS Config 4 | Specific to using AWS Configuration with Firewall Manager, while you can elect to enable additional/all resources, if you only are using AWS Config for Firewall manager this provide 5 | https://docs.aws.amazon.com/waf/latest/developerguide/enable-config.html 6 | 7 | 8 | ## Enable just AWS Shield Advanced 9 | Includes no Firewall Manager configuration. Can be deployed as a stand alone template or StackSet to enable across an AWS Organization 10 | https://github.com/aws-samples/aws-shield-advanced-examples 11 | 12 | ## Automation to Create Amazon Route 53 Health Checks for Shield Protected Resources 13 | https://github.com/aws-samples/aws-shield-advanced-rapid-deployment/tree/main/code/route53/config-proactive-engagement 14 | 15 | ## AWS Shield Advanced Protection - Global Accelerator & Amazon Route 53 Hosted ZOnes 16 | Firewall Manager does not support enabling Shield protection. The below code "mimics" Firewall Manager behavior to enable Shield Protection using AWS Config. 17 | 18 | https://github.com/aws-samples/aws-shield-advanced-rapid-deployment/tree/main/code/fms/fms-mimic-shield-protect-global-accelerator 19 | https://github.com/aws-samples/aws-shield-advanced-rapid-deployment/tree/main/code/fms/fms-mimic-shield-protect-route53-hosted-zones -------------------------------------------------------------------------------- /terraform/core-deployment/configure_shield.py: -------------------------------------------------------------------------------- 1 | import botocore 2 | try: 3 | import cfnresponse 4 | except: 5 | print ("no cfnresponse module") 6 | import logging 7 | 8 | logger = logging.getLogger('hc') 9 | logger.setLevel('DEBUG') 10 | 11 | shield_client = boto3.client('shield') 12 | 13 | def lambda_handler(event, context): 14 | logger.debug(event) 15 | responseData = {} 16 | if "RequestType" in event: 17 | if event['RequestType'] in ['Create','Update']: 18 | try: 19 | logger.debug("Start Create Subscription") 20 | shield_client.create_subscription() 21 | logger.info ("Shield Enabled!") 22 | responseData['Message'] = "Subscription Created" 23 | cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "SubscriptionCreated") 24 | return () 25 | except botocore.exceptions.ClientError as error: 26 | if error.response['Error']['Code'] == 'ResourceAlreadyExistsException': 27 | logger.info ("Subscription already active") 28 | responseData['Message'] = "Already Subscribed to Shield Advanced" 29 | cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "AlreadySubscribedOk") 30 | return 31 | else: 32 | logger.error(error.response['Error']) 33 | responseData['Message'] = error.response['Error'] 34 | cfnresponse.send(event, context, cfnresponse.FAILED, responseData, "SubscribeFailed") 35 | return () 36 | else: 37 | responseData['Message'] = "CFN Delete, no action taken" 38 | cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "CFNDeleteGracefulContinue") 39 | return() -------------------------------------------------------------------------------- /terraform/organizational-deployment/modules/aws-shield-advanced-configure-subscribe/code/configure_shield.py: -------------------------------------------------------------------------------- 1 | import botocore 2 | import boto3 3 | try: 4 | import cfnresponse 5 | except: 6 | print ("no cfnresponse module") 7 | import logging 8 | 9 | logger = logging.getLogger('hc') 10 | logger.setLevel('DEBUG') 11 | 12 | shield_client = boto3.client('shield') 13 | 14 | def lambda_handler(event, context): 15 | logger.debug(event) 16 | responseData = {} 17 | if "RequestType" in event: 18 | if event['RequestType'] in ['Create','Update']: 19 | try: 20 | logger.debug("Start Create Subscription") 21 | shield_client.create_subscription() 22 | logger.info ("Shield Enabled!") 23 | responseData['Message'] = "Subscription Created" 24 | #cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "SubscriptionCreated") 25 | return () 26 | except botocore.exceptions.ClientError as error: 27 | if error.response['Error']['Code'] == 'ResourceAlreadyExistsException': 28 | logger.info ("Subscription already active") 29 | responseData['Message'] = "Already Subscribed to Shield Advanced" 30 | #cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "AlreadySubscribedOk") 31 | return 32 | else: 33 | logger.error(error.response['Error']) 34 | responseData['Message'] = error.response['Error'] 35 | #cfnresponse.send(event, context, cfnresponse.FAILED, responseData, "SubscribeFailed") 36 | return () 37 | else: 38 | responseData['Message'] = "CFN Delete, no action taken" 39 | #cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "CFNDeleteGracefulContinue") 40 | return() -------------------------------------------------------------------------------- /terraform/organizational-deployment/modules/aws-shield-advanced-fms-shield/variables.tf: -------------------------------------------------------------------------------- 1 | variable "scope_details" { 2 | type = list(string) 3 | default = [] 4 | } 5 | 6 | variable "include_exclude_scope" { 7 | type = string 8 | default = "Include" 9 | } 10 | 11 | variable "scope_type" { 12 | description = "Should Firewall Manager Policies be scoped to the entire org (root) or a specific list of OUs (OU)" 13 | type = string 14 | default = "Org" 15 | validation { 16 | condition = contains(["Org", "OU", "Accounts"], var.scope_type) 17 | error_message = "scope_type must be one of [ Org | OU | Accounts ]" 18 | } 19 | } 20 | 21 | variable "resource_tag_usage" { 22 | description = "Include will scope to only include when ResourceTags match, Exclude will exclude when target resource tags match ResourceTags" 23 | type = string 24 | default = "Include" 25 | } 26 | 27 | variable "protect_regional_resource_types" { 28 | description = "AWS::ElasticLoadBalancingV2::LoadBalancer,AWS::ElasticLoadBalancing::LoadBalancer,AWS::EC2::EIP" 29 | type = list(string) 30 | default = ["AWS::ElasticLoadBalancingV2::LoadBalancer","AWS::ElasticLoadBalancing::LoadBalancer","AWS::EC2::EIP"] 31 | } 32 | 33 | variable "protect_cloud_front" { 34 | type = string 35 | default = "" 36 | } 37 | 38 | variable "shield_auto_remediate" { 39 | description = "Should in scope AWS resource types have Shield Advanced protection automatically be remediated?" 40 | type = string 41 | default = "Yes" 42 | } 43 | 44 | variable "scope_tag_name1" { 45 | type = string 46 | default = "" 47 | } 48 | 49 | variable "scope_tag_name2" { 50 | type = string 51 | default = "" 52 | } 53 | 54 | variable "scope_tag_name3" { 55 | type = string 56 | default = "" 57 | } 58 | 59 | variable "scope_tag_value1" { 60 | type = string 61 | default = "" 62 | } 63 | 64 | variable "scope_tag_value2" { 65 | type = string 66 | default = "" 67 | } 68 | 69 | variable "scope_tag_value3" { 70 | type = string 71 | default = "" 72 | } -------------------------------------------------------------------------------- /terraform/core-deployment/modules/aws-shield-advanced-base-support-infra/firehose.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | workspace_name = terraform.workspace 3 | } 4 | 5 | resource "aws_cloudwatch_log_group" "waf_delivery_log_group" { 6 | name = "/aws/kinesisfirehose/${local.workspace_name}" 7 | kms_key_id = aws_kms_key.waf_delivery_log_key.arn 8 | retention_in_days = 365 9 | 10 | depends_on = [aws_kms_key.waf_delivery_log_key] 11 | } 12 | 13 | resource "aws_cloudwatch_log_stream" "waf_delivery_log_stream" { 14 | name = "WAFDeliveryLogStram" 15 | log_group_name = "/aws/kinesisfirehose/${local.workspace_name}" 16 | } 17 | 18 | resource "aws_kinesis_firehose_delivery_stream" "waf_delivery_stream" { 19 | name = "aws-waf-logs-${local.workspace_name}-${local.aws_region_name}" 20 | destination = "extended_s3" 21 | 22 | server_side_encryption { 23 | enabled = true 24 | key_type = "CUSTOMER_MANAGED_CMK" 25 | key_arn = aws_kms_key.waf_delivery_log_key.arn 26 | } 27 | 28 | extended_s3_configuration { 29 | bucket_arn = aws_s3_bucket.waf_logs.arn 30 | role_arn = aws_iam_role.waf_logs.arn 31 | 32 | cloudwatch_logging_options { 33 | enabled = true 34 | log_group_name = "/aws/kinesisfirehose/${local.workspace_name}" 35 | log_stream_name = aws_cloudwatch_log_stream.waf_delivery_log_stream.name 36 | } 37 | buffering_interval = 60 38 | buffering_size = 50 39 | compression_format = "UNCOMPRESSED" 40 | prefix = "firehose/${local.aws_region_name}" 41 | 42 | kms_key_arn = aws_kms_key.waf_delivery_log_key.arn 43 | } 44 | } 45 | 46 | resource "aws_kms_key" "waf_delivery_log_key" { 47 | description = "CloudWatch Log Encryption Key" 48 | enable_key_rotation = true 49 | deletion_window_in_days = 20 50 | policy = <<-EOF 51 | { 52 | "Version": "2012-10-17", 53 | "Id": "key-default-1", 54 | "Statement": [ 55 | { 56 | "Sid": "Enable IAM User Permissions", 57 | "Effect" : "Allow", 58 | "Principal" : { 59 | "AWS" : "arn:aws:iam::${local.account_id}:root" 60 | }, 61 | "Action" : "kms:*", 62 | "Resource" : "*" 63 | }, 64 | { 65 | "Sid" : "Allow administration of the key", 66 | "Effect" : "Allow", 67 | "Principal" : { 68 | "Service" : "logs.${local.aws_region_name}.amazonaws.com" 69 | }, 70 | "Action" : [ 71 | "kms:Encrypt*", 72 | "kms:Decrypt*", 73 | "kms:ReEncrypt*", 74 | "kms:GenerateDataKey*", 75 | "kms:Describe*" 76 | ], 77 | "Resource" : "*" 78 | 79 | } 80 | ] 81 | } 82 | EOF 83 | } -------------------------------------------------------------------------------- /cloudformation/templates/firehose.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: 2010-09-09 3 | Parameters: 4 | WAFLogS3BucketName: 5 | Type: String 6 | WAFDeliveryRoleArn: 7 | Type: String 8 | Resources: 9 | WAFDeliveryLogGroup: 10 | Type: AWS::Logs::LogGroup 11 | Properties: 12 | LogGroupName: !Sub "/aws/kinesisfirehose/${AWS::StackName}" 13 | KmsKeyId: !GetAtt KMSKey.Arn 14 | RetentionInDays: 365 15 | WAFDeliveryLogStream: 16 | DependsOn: WAFDeliveryLogGroup 17 | Type: AWS::Logs::LogStream 18 | Properties: 19 | LogGroupName: !Sub "/aws/kinesisfirehose/${AWS::StackName}" 20 | WAFDeliverystream: 21 | Type: AWS::KinesisFirehose::DeliveryStream 22 | Metadata: 23 | cfn_nag: 24 | rules_to_suppress: 25 | - id: W88 26 | reason: "There is no Delivery Stream, these are from FMS, not applicable" 27 | Properties: 28 | DeliveryStreamName: !Sub "aws-waf-logs-${AWS::StackName}-${AWS::Region}" 29 | DeliveryStreamEncryptionConfigurationInput: 30 | KeyARN: !GetAtt KMSKey.Arn 31 | KeyType: CUSTOMER_MANAGED_CMK 32 | ExtendedS3DestinationConfiguration: 33 | BucketARN: !Sub "arn:aws:s3:::${WAFLogS3BucketName}" 34 | CloudWatchLoggingOptions: 35 | Enabled: true 36 | LogGroupName: !Sub "/aws/kinesisfirehose/${AWS::StackName}" 37 | LogStreamName: !Ref WAFDeliveryLogStream 38 | BufferingHints: 39 | IntervalInSeconds: 60 40 | SizeInMBs: 50 41 | CompressionFormat: UNCOMPRESSED 42 | EncryptionConfiguration: 43 | KMSEncryptionConfig: 44 | AWSKMSKeyARN: !GetAtt "KMSKey.Arn" 45 | Prefix: !Sub 'firehose/${AWS::Region}/' 46 | RoleARN: !Ref WAFDeliveryRoleArn 47 | KMSKey: 48 | Type: AWS::KMS::Key 49 | Properties: 50 | Description: CloudWatch Log Encryption Key 51 | EnableKeyRotation: true 52 | PendingWindowInDays: 20 53 | KeyPolicy: 54 | Version: '2012-10-17' 55 | Id: key-default-1 56 | Statement: 57 | - Sid: Enable IAM User Permissions 58 | Effect: Allow 59 | Principal: 60 | AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" 61 | Action: kms:* 62 | Resource: '*' 63 | - Sid: Allow administration of the key 64 | Effect: Allow 65 | Principal: 66 | Service: !Sub "logs.${AWS::Region}.amazonaws.com" 67 | Action: 68 | - kms:Encrypt 69 | - kms:Decrypt 70 | - kms:ReEncrypt* 71 | - kms:GenerateDataKey 72 | - kms:Describe* 73 | Resource: '*' 74 | Condition: 75 | ArnEquals: 76 | "kms:EncryptionContext:aws:logs:arn": !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/kinesisfirehose/${AWS::StackName}" -------------------------------------------------------------------------------- /terraform/organizational-deployment/modules/aws-shield-advanced-fms-shield/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | } 6 | } 7 | 8 | } 9 | 10 | data "aws_region" "current" {} 11 | 12 | locals { 13 | workspace_name = terraform.workspace 14 | ScopeTagName1Flag = var.scope_tag_name1 != "" 15 | ScopeTagName2Flag = var.scope_tag_name2 != "" 16 | ScopeTagName3Flag = var.scope_tag_name3 != "" 17 | ScopeTagValue1Flag = var.scope_tag_value1 != "" 18 | ScopeTagValue2Flag = var.scope_tag_value2 != "" 19 | ScopeTagValue3Flag = var.scope_tag_value3 != "" 20 | ShieldAutoRemediateFlag = var.shield_auto_remediate != "No" 21 | OUScopeFlag = var.scope_type == "OU" 22 | AccountScopeFlag = var.scope_type == "Accounts" 23 | ExcludeResourceTagFlag = var.resource_tag_usage == "Exclude" 24 | IncludeScopeFlag = var.include_exclude_scope == "Include" 25 | ExcludeScopeFlag = var.include_exclude_scope == "Exclude" 26 | CreateRegionalPolicyFlag = length(var.protect_regional_resource_types) > 0 27 | CreateCloudFrontPolicyFlag = alltrue([ 28 | var.protect_cloud_front != "", 29 | data.aws_region.current.name == "us-east-1" 30 | ]) 31 | stack_name = "fms-shield" 32 | 33 | resource_tags = { 34 | 35 | } 36 | } 37 | 38 | resource "aws_fms_policy" "shield_regional_resources" { 39 | count = local.CreateRegionalPolicyFlag ? 1 : 0 40 | name = "OneClickShieldRegional-${local.workspace_name}" 41 | //resource_type = length(var.protect_regional_resource_types) == 1 ? element(var.protect_regional_resource_types, 0) : "" 42 | resource_type_list = var.protect_regional_resource_types 43 | //resource_type_list = length(var.protect_regional_resource_types) > 1 ? var.protect_regional_resource_types : null 44 | exclude_resource_tags = local.ExcludeResourceTagFlag 45 | 46 | security_service_policy_data { 47 | type = "SHIELD_ADVANCED" 48 | 49 | managed_service_data = jsonencode({ type = "SHIELD_ADVANCED" }) 50 | } 51 | 52 | dynamic include_map { 53 | for_each = local.IncludeScopeFlag && length(var.scope_details) > 0 ? [1] : [] 54 | content { 55 | account = local.AccountScopeFlag ? var.scope_details : null 56 | orgunit = local.OUScopeFlag ? var.scope_details : null 57 | } 58 | } 59 | 60 | dynamic exclude_map { 61 | for_each = local.ExcludeScopeFlag && length(var.scope_details) > 0 ? [1] : [] 62 | content { 63 | account = local.AccountScopeFlag ? var.scope_details : null 64 | orgunit = local.OUScopeFlag ? var.scope_details : null 65 | } 66 | } 67 | 68 | remediation_enabled = local.ShieldAutoRemediateFlag 69 | delete_all_policy_resources = false 70 | resource_tags = local.ScopeTagName1Flag ? tomap([ 71 | local.ScopeTagName1Flag ? { 72 | "Key" = var.scope_tag_name1, 73 | "Value" = local.ScopeTagName1Flag ? var.scope_tag_value1 : "" 74 | } : null, 75 | local.ScopeTagName2Flag ? { 76 | "Key" = var.scope_tag_name2 77 | "Value" = local.ScopeTagName2Flag ? var.scope_tag_value2 : "" 78 | } : null, 79 | local.ScopeTagName3Flag ? { 80 | Key = var.scope_tag_name3 81 | Value = local.ScopeTagName3Flag ? var.scope_tag_value3 : "" 82 | } : null]) : null 83 | } 84 | 85 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /terraform/core-deployment/modules/aws-shield-advanced-base-support-infra/glue.tf: -------------------------------------------------------------------------------- 1 | resource "aws_glue_catalog_database" "glue_waf_logs" { 2 | catalog_id = local.account_id 3 | name = "oneclickshieldwaf-core" 4 | location_uri = "s3://${aws_s3_bucket.waf_logs.id}/${local.aws_region_name}" 5 | } 6 | 7 | resource "aws_glue_catalog_table" "glue_waf_logs" { 8 | catalog_id = local.account_id 9 | database_name = aws_glue_catalog_database.glue_waf_logs.name 10 | name = "waf_logs_raw" 11 | table_type = "EXTERNAL_TABLE" 12 | partition_keys { 13 | name = "datehour" 14 | type = "string" 15 | } 16 | partition_keys { 17 | name = "region" 18 | type = "string" 19 | } 20 | 21 | parameters = { 22 | EXTERNAL = "TRUE" 23 | "projection.region.type" = "enum" 24 | "projection.region.values" = "${join(",", var.scope_regions)}" 25 | "projection.datehour.format" = "yyyy/MM/dd/HH" 26 | "projection.datehour.interval" = "1" 27 | "projection.datehour.interval.unit" = "HOURS" 28 | "projection.datehour.range" = "2023/06/01/00,NOW" 29 | "projection.datehour.type" = "date" 30 | "projection.enabled" = "true" 31 | "storage.location.template" = "s3://${aws_s3_bucket.waf_logs.id}/firehose/$${region}/$${datehour}" 32 | } 33 | 34 | storage_descriptor { 35 | input_format = "org.apache.hadoop.mapred.TextInputFormat" 36 | output_format = "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat" 37 | location = "s3://${aws_s3_bucket.waf_logs.id}/firehose/" 38 | ser_de_info { 39 | parameters = { 40 | "serialization.format" = "1" 41 | } 42 | } 43 | columns { 44 | name = "timestamp" 45 | type = "bigint" 46 | } 47 | 48 | columns { 49 | name = "formatversion" 50 | type = "int" 51 | } 52 | columns { 53 | name = "webaclid" 54 | type = "string" 55 | } 56 | columns { 57 | name = "terminatingruleid" 58 | type = "string" 59 | } 60 | columns { 61 | name = "terminatingruletype" 62 | type = "string" 63 | } 64 | columns { 65 | name = "action" 66 | type = "string" 67 | } 68 | columns { 69 | name = "terminatingrulematchdetails" 70 | type = "array>>" 71 | } 72 | columns { 73 | name = "httpsourcename" 74 | type = "string" 75 | } 76 | columns { 77 | name = "httpsourceid" 78 | type = "string" 79 | } 80 | columns { 81 | name = "rulegrouplist" 82 | type = "array,nonterminatingmatchingrules:array>>>>,excludedrules:array>>>" 83 | } 84 | columns { 85 | name = "ratebasedrulelist" 86 | type = "array>" 87 | } 88 | columns { 89 | name = "nonterminatingmatchingrules" 90 | type = "array>" 91 | } 92 | columns { 93 | name = "requestheadersinserted" 94 | type = "string" 95 | } 96 | columns { 97 | name = "responsecodesent" 98 | type = "string" 99 | } 100 | columns { 101 | name = "httprequest" 102 | type = "struct>,uri:string,args:string,httpversion:string,httpmethod:string,requestid:string>" 103 | } 104 | columns { 105 | name = "labels" 106 | type = "array>" 107 | } 108 | } 109 | } 110 | 111 | output "glue_waf_logs" { 112 | value = aws_glue_catalog_database.glue_waf_logs.id 113 | } -------------------------------------------------------------------------------- /terraform/tfstacks.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | import boto3 4 | import sys, getopt 5 | import os 6 | import json 7 | 8 | def init(upgrade=''): 9 | subprocess.run(f"terraform init {upgrade}", check=True, shell=True) 10 | 11 | def get_accounts(): 12 | organizations = boto3.client('organizations') 13 | paginator = organizations.get_paginator("list_accounts") 14 | 15 | return [ 16 | account["Id"] 17 | for page in paginator.paginate() 18 | for account in page["Accounts"] 19 | ] 20 | 21 | def get_master_account_id(): 22 | organizations = boto3.client('organizations') 23 | orgInfo = organizations.describe_organization() 24 | return orgInfo['Organization']['MasterAccountId'] 25 | 26 | def workspace_exists(workspace_name): 27 | completed_process = subprocess.run(f"terraform workspace list | grep {workspace_name}", shell=True) 28 | return completed_process.returncode == 0 29 | 30 | def create_workspace(workspace_name): 31 | subprocess.run(f"terraform workspace new {workspace_name}", check=True, shell=True) 32 | 33 | def switch_to_workspace(workspace_name): 34 | subprocess.run(f"terraform workspace select {workspace_name}", check=True, shell=True) 35 | 36 | def plan(account, plan_file, tfvarsfile, opttfvars='', region=''): 37 | optional_var_file = f"-var-file={opttfvars}" if opttfvars != '' else '' 38 | optional_region = f"-var target_aws_account_region={region}" if region != '' else '' 39 | print("Going to execute plan") 40 | runcmd = f"terraform plan -var target_account_id={account} {optional_region} -var-file={tfvarsfile} {optional_var_file} -out={plan_file}" 41 | print(runcmd) 42 | subprocess.run(runcmd, check=True, shell=True) 43 | 44 | def apply(plan_file): 45 | subprocess.run(f"terraform apply {plan_file}", check=True, shell=True) 46 | 47 | def save_output(filename, filenamejson): 48 | subprocess.run(f"terraform output > {filename}", check=True, shell=True) 49 | subprocess.run(f"terraform output -json> {filenamejson}", check=True, shell=True) 50 | 51 | def get_scope_regions(filename): 52 | f = open(filename) 53 | data = json.load(f) 54 | scope_regions = data['scope_regions']["value"].split(',') 55 | f.close() 56 | return scope_regions 57 | 58 | def run(argv): 59 | opts, args = getopt.getopt(argv,"hi:o:c:u",["core-var-file=",'org-var-file=',"upgrade"]) 60 | corepath = './core-deployment' 61 | corevarsfile = '' 62 | coreoutputfile = 'core-deployment-output.tfvars' 63 | coreoutputfilejson = 'core-deployment-output.json' 64 | orgvarsfile = '' 65 | upgrade = '' 66 | 67 | for opt, arg in opts: 68 | if opt == '-h': 69 | print ('tfstacks.py --var-file=') 70 | sys.exit() 71 | elif opt in ("-c", "--core-var-file"): 72 | print ("Looking for core vars file") 73 | corevarsfile = arg 74 | print (corevarsfile) 75 | elif opt in ("-o", "--org-var-file"): 76 | orgvarsfile = arg 77 | elif opt in ("-u", "--upgrade"): 78 | upgrade = '--upgrade' 79 | 80 | 81 | os.chdir('./core-deployment') 82 | if not workspace_exists("core-deployment"): 83 | create_workspace("core-deployment") 84 | switch_to_workspace("core-deployment") 85 | init(upgrade) 86 | primaryAccountId = get_master_account_id() 87 | plan_file = f"{primaryAccountId}.plan" 88 | plan(primaryAccountId, plan_file, corevarsfile) 89 | apply(plan_file) 90 | save_output(coreoutputfile, coreoutputfilejson) 91 | 92 | os.chdir('..') 93 | os.chdir('./organizational-deployment') 94 | 95 | for account in get_accounts(): 96 | scope_regions = get_scope_regions(os.path.normpath(f"../{corepath}/{coreoutputfilejson}")) 97 | for region in scope_regions: 98 | print(f"Working on workspace {account}-{region}") 99 | workspace_name = f"{account}-{region}" 100 | if not workspace_exists(workspace_name): 101 | create_workspace(workspace_name) 102 | switch_to_workspace(workspace_name) 103 | init(upgrade) 104 | plan_file = f"{account}-{region}.plan" 105 | plan(account, plan_file, orgvarsfile, os.path.normpath(f"../{corepath}/{coreoutputfile}"), region) 106 | apply(plan_file) 107 | 108 | if __name__ == "__main__": 109 | run(sys.argv[1:]) -------------------------------------------------------------------------------- /terraform/core-deployment/athena_query_lambda.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import datetime 3 | import os 4 | import time 5 | import json 6 | import urllib3 7 | import botocore 8 | s3BasePath = os.environ['s3BasePath'] 9 | workGroupName = os.environ['workGroupName'] 10 | database = os.environ['glueDatabase'] 11 | athena_client = boto3.client('athena') 12 | http = urllib3.PoolManager() 13 | responseData = {} 14 | def cfnrespond(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False): 15 | responseUrl = event['ResponseURL'] 16 | responseBody = {} 17 | responseBody['Status'] = responseStatus 18 | responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + context.log_stream_name 19 | responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name 20 | responseBody['StackId'] = event['StackId'] 21 | responseBody['RequestId'] = event['RequestId'] 22 | responseBody['LogicalResourceId'] = event['LogicalResourceId'] 23 | responseBody['NoEcho'] = noEcho 24 | responseBody['Data'] = responseData 25 | json_responseBody = json.dumps(responseBody) 26 | print("Response body:\n" + json_responseBody) 27 | headers = { 28 | 'content-type' : '', 29 | 'content-length' : str(len(json_responseBody)) 30 | } 31 | try: 32 | response = http.request('PUT',responseUrl,body=json_responseBody.encode('utf-8'),headers=headers) 33 | print("Status code: " + response.reason) 34 | except Exception as e: 35 | print("send(..) failed executing requests.put(..): " + str(e)) 36 | def wait_for_queries_to_finish(executionIdList): 37 | while (executionIdList != []): 38 | for eId in executionIdList: 39 | currentState = athena_client.get_query_execution(QueryExecutionId=eId)['QueryExecution']['Status']['State'] 40 | if currentState in ['SUCCEEDED']: 41 | executionIdList.remove (eId) 42 | elif currentState in ['FAILED','CANCELLED']: 43 | return (executionIdList) 44 | time.sleep(1) 45 | return ([]) 46 | def lambda_handler(event, context): 47 | print (json.dumps(event)) 48 | if event['RequestType'] == 'Delete': 49 | try: 50 | athena_client.delete_work_group( 51 | WorkGroup=workGroupName, 52 | RecursiveDeleteOption=True 53 | ) 54 | cfnrespond(event, context, "SUCCESS", {}, "Graceful Delete") 55 | return () 56 | except: 57 | cfnrespond(event, context, "FAILED", {}, "") 58 | return () 59 | else: 60 | executionIdList = [] 61 | transformQuery = False 62 | transformQuery = True 63 | detailedViewQueryId = event['ResourceProperties']['DetailedViewQueryId'] 64 | baseQueryString = athena_client.get_named_query( 65 | NamedQueryId=detailedViewQueryId)['NamedQuery']['QueryString'] 66 | queryString = "CREATE OR REPLACE VIEW waf_detailed AS " + baseQueryString 67 | try: 68 | r = athena_client.start_query_execution( 69 | QueryString=queryString, 70 | QueryExecutionContext={ 71 | 'Database': database, 72 | 'Catalog': 'AwsDataCatalog'}, 73 | WorkGroup=workGroupName 74 | ) 75 | except botocore.exceptions.ClientError as error: 76 | print (error.response) 77 | cfnrespond(event, context, "FAILED", {}, "") 78 | return () 79 | #Wait for query to finish, it should take a second but wait just in case 80 | if wait_for_queries_to_finish([r['QueryExecutionId']]) != []: 81 | cfnrespond(event, context, "FAILED", responseData, "CreateViewQueriesFailed") 82 | return ("QueriesFailed") 83 | #Get all named query IDs in WorkGroup 84 | namedQueries = athena_client.list_named_queries( 85 | WorkGroup=workGroupName)['NamedQueryIds'] 86 | #Get all Named Queries 87 | for queryId in namedQueries: 88 | queryResults = athena_client.get_named_query( 89 | NamedQueryId=queryId 90 | )['NamedQuery'] 91 | print (queryResults) 92 | if queryResults['Name'] != 'waf_detailed': 93 | outputLocation = s3BasePath + queryResults['Name'].split('-')[-1] + '/' 94 | if transformQuery: 95 | queryString = "CREATE OR REPLACE VIEW " + '"' + database + '".' + queryResults['Name'].replace('-','_') + " AS " + queryResults['QueryString'] 96 | else: 97 | queryString = queryResults['QueryString'] 98 | print ("queryString") 99 | print (queryString) 100 | try: 101 | r = athena_client.start_query_execution( 102 | QueryString=queryString, 103 | ResultConfiguration={ 104 | 'OutputLocation': outputLocation, 105 | 'EncryptionConfiguration': { 106 | 'EncryptionOption': 'SSE_S3', 107 | } 108 | }, 109 | WorkGroup=workGroupName 110 | ) 111 | executionIdList.append(r['QueryExecutionId']) 112 | except botocore.exceptions.ClientError as error: 113 | print (error.response) 114 | cfnrespond(event, context, "FAILED", {}, "") 115 | return () 116 | print (executionIdList) 117 | if wait_for_queries_to_finish(executionIdList) != []: 118 | cfnrespond(event, context, "FAILED", responseData, "CreateViewQueriesFailed") 119 | return ("QueriesFailed") 120 | else: 121 | cfnrespond(event, context, "SUCCESS", responseData, "CreateViewsSuccessful") -------------------------------------------------------------------------------- /terraform/core-deployment/modules/aws-shield-advanced-base-support-infra/code/athena_query_lambda.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import datetime 3 | import os 4 | import time 5 | import json 6 | import urllib3 7 | import botocore 8 | s3BasePath = os.environ['s3BasePath'] 9 | workGroupName = os.environ['workGroupName'] 10 | database = os.environ['glueDatabase'] 11 | athena_client = boto3.client('athena') 12 | http = urllib3.PoolManager() 13 | responseData = {} 14 | def cfnrespond(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False): 15 | responseUrl = event['ResponseURL'] 16 | responseBody = {} 17 | responseBody['Status'] = responseStatus 18 | responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + context.log_stream_name 19 | responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name 20 | responseBody['StackId'] = event['StackId'] 21 | responseBody['RequestId'] = event['RequestId'] 22 | responseBody['LogicalResourceId'] = event['LogicalResourceId'] 23 | responseBody['NoEcho'] = noEcho 24 | responseBody['Data'] = responseData 25 | json_responseBody = json.dumps(responseBody) 26 | print("Response body:\n" + json_responseBody) 27 | headers = { 28 | 'content-type' : '', 29 | 'content-length' : str(len(json_responseBody)) 30 | } 31 | try: 32 | response = http.request('PUT',responseUrl,body=json_responseBody.encode('utf-8'),headers=headers) 33 | print("Status code: " + response.reason) 34 | except Exception as e: 35 | print("send(..) failed executing requests.put(..): " + str(e)) 36 | def wait_for_queries_to_finish(executionIdList): 37 | while (executionIdList != []): 38 | for eId in executionIdList: 39 | currentState = athena_client.get_query_execution(QueryExecutionId=eId)['QueryExecution']['Status']['State'] 40 | if currentState in ['SUCCEEDED']: 41 | executionIdList.remove (eId) 42 | elif currentState in ['FAILED','CANCELLED']: 43 | return (executionIdList) 44 | time.sleep(1) 45 | return ([]) 46 | def lambda_handler(event, context): 47 | print (json.dumps(event)) 48 | # if event['RequestType'] == 'Delete': 49 | # try: 50 | # athena_client.delete_work_group( 51 | # WorkGroup=workGroupName, 52 | # RecursiveDeleteOption=True 53 | # ) 54 | # cfnrespond(event, context, "SUCCESS", {}, "Graceful Delete") 55 | # return () 56 | # except: 57 | # cfnrespond(event, context, "FAILED", {}, "") 58 | # return () 59 | # else: 60 | executionIdList = [] 61 | transformQuery = False 62 | transformQuery = True 63 | detailedViewQueryId = event['DetailedViewQueryId'] 64 | # detailedViewQueryId = event['ResourceProperties']['DetailedViewQueryId'] 65 | baseQueryString = athena_client.get_named_query( 66 | NamedQueryId=detailedViewQueryId)['NamedQuery']['QueryString'] 67 | queryString = "CREATE OR REPLACE VIEW waf_detailed AS " + baseQueryString 68 | try: 69 | r = athena_client.start_query_execution( 70 | QueryString=queryString, 71 | QueryExecutionContext={ 72 | 'Database': database, 73 | 'Catalog': 'AwsDataCatalog'}, 74 | WorkGroup=workGroupName 75 | ) 76 | except botocore.exceptions.ClientError as error: 77 | print (error.response) 78 | # cfnrespond(event, context, "FAILED", {}, "") 79 | return () 80 | #Wait for query to finish, it should take a second but wait just in case 81 | if wait_for_queries_to_finish([r['QueryExecutionId']]) != []: 82 | # cfnrespond(event, context, "FAILED", responseData, "CreateViewQueriesFailed") 83 | return ("QueriesFailed") 84 | #Get all named query IDs in WorkGroup 85 | namedQueries = athena_client.list_named_queries( 86 | WorkGroup=workGroupName)['NamedQueryIds'] 87 | #Get all Named Queries 88 | for queryId in namedQueries: 89 | queryResults = athena_client.get_named_query( 90 | NamedQueryId=queryId 91 | )['NamedQuery'] 92 | print (queryResults) 93 | if queryResults['Name'] != 'waf_detailed': 94 | outputLocation = s3BasePath + queryResults['Name'].split('-')[-1] + '/' 95 | if transformQuery: 96 | queryString = "CREATE OR REPLACE VIEW " + '"' + database + '".' + queryResults['Name'].replace('-','_') + " AS " + queryResults['QueryString'] 97 | else: 98 | queryString = queryResults['QueryString'] 99 | print ("queryString") 100 | print (queryString) 101 | try: 102 | r = athena_client.start_query_execution( 103 | QueryString=queryString, 104 | ResultConfiguration={ 105 | 'OutputLocation': outputLocation, 106 | 'EncryptionConfiguration': { 107 | 'EncryptionOption': 'SSE_S3', 108 | } 109 | }, 110 | WorkGroup=workGroupName 111 | ) 112 | executionIdList.append(r['QueryExecutionId']) 113 | except botocore.exceptions.ClientError as error: 114 | print (error.response) 115 | # cfnrespond(event, context, "FAILED", {}, "") 116 | return () 117 | print (executionIdList) 118 | if wait_for_queries_to_finish(executionIdList) != []: 119 | # cfnrespond(event, context, "FAILED", responseData, "CreateViewQueriesFailed") 120 | return ("QueriesFailed") 121 | else: 122 | return ("QueriesSuccess") 123 | # cfnrespond(event, context, "SUCCESS", responseData, "CreateViewsSuccessful") -------------------------------------------------------------------------------- /terraform/core-deployment/modules/aws-shield-advanced-base-support-infra/s3iam.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket" "logging" { 2 | lifecycle { 3 | prevent_destroy = true 4 | } 5 | } 6 | 7 | data "aws_iam_policy_document" "logging" { 8 | statement { 9 | principals { 10 | type = "Service" 11 | identifiers = ["logging.s3.amazonaws.com"] 12 | } 13 | effect = "Allow" 14 | actions = [ 15 | "s3:PutObject" 16 | ] 17 | resources = [ 18 | aws_s3_bucket.logging.arn, 19 | "${aws_s3_bucket.logging.arn}/*" 20 | ] 21 | condition { 22 | test = "ArnLike" 23 | variable = "aws:SourceARN" 24 | values = [aws_s3_bucket.waf_logs.arn] 25 | } 26 | } 27 | statement { 28 | principals { 29 | type = "*" 30 | identifiers = ["*"] 31 | } 32 | effect = "Deny" 33 | actions = [ 34 | "s3:*" 35 | ] 36 | resources = [ 37 | aws_s3_bucket.logging.arn, 38 | "${aws_s3_bucket.logging.arn}/*" 39 | ] 40 | condition { 41 | test = "Bool" 42 | variable = "aws:SecureTransport" 43 | values = ["false"] 44 | } 45 | 46 | } 47 | } 48 | 49 | resource "aws_s3_bucket_policy" "logging" { 50 | bucket = aws_s3_bucket.logging.id 51 | policy = data.aws_iam_policy_document.logging.json 52 | } 53 | 54 | resource "aws_s3_bucket_server_side_encryption_configuration" "logging" { 55 | bucket = aws_s3_bucket.logging.id 56 | rule { 57 | apply_server_side_encryption_by_default { 58 | sse_algorithm = "AES256" 59 | } 60 | } 61 | } 62 | 63 | resource "aws_s3_bucket_ownership_controls" "logging" { 64 | bucket = aws_s3_bucket.logging.id 65 | rule { 66 | object_ownership = "BucketOwnerPreferred" 67 | } 68 | } 69 | 70 | resource "aws_s3_bucket" "waf_logs" { 71 | lifecycle { 72 | prevent_destroy = true 73 | } 74 | } 75 | 76 | resource "aws_s3_bucket_server_side_encryption_configuration" "waf_logs" { 77 | bucket = aws_s3_bucket.waf_logs.id 78 | rule { 79 | apply_server_side_encryption_by_default { 80 | sse_algorithm = "aws:kms" 81 | kms_master_key_id = aws_kms_key.waf_logs.arn 82 | } 83 | } 84 | } 85 | 86 | resource "aws_kms_key" "waf_logs" { 87 | lifecycle { 88 | prevent_destroy = true 89 | } 90 | description = "WAF Log Key" 91 | enable_key_rotation = true 92 | 93 | } 94 | 95 | resource "aws_kms_key_policy" "waf_logs" { 96 | key_id = aws_kms_key.waf_logs.id 97 | policy = jsonencode({ 98 | Version = "2012-10-17" 99 | Id = "key-default-1" 100 | Statement = [ 101 | { 102 | Sid = "Enable IAM UserPermissions" 103 | Effect = "Allow" 104 | Principal = { 105 | AWS = "arn:aws:iam::${local.account_id}:root" 106 | } 107 | Action = "kms:*" 108 | Resource = "*" 109 | } 110 | ] 111 | }) 112 | } 113 | 114 | data "aws_iam_policy_document" "waf_logs" { 115 | statement { 116 | actions = ["sts:AssumeRole"] 117 | effect = "Allow" 118 | principals { 119 | type = "Service" 120 | identifiers = ["firehose.amazonaws.com"] 121 | } 122 | condition { 123 | test = "StringEquals" 124 | variable = "sts:ExternalId" 125 | values = ["${local.account_id}"] 126 | } 127 | } 128 | } 129 | 130 | resource "aws_iam_role" "waf_logs" { 131 | assume_role_policy = data.aws_iam_policy_document.waf_logs.json 132 | } 133 | 134 | resource "aws_iam_role_policy_attachment" "waf_logs" { 135 | role = aws_iam_role.waf_logs.name 136 | policy_arn = aws_iam_policy.waf_delivery.arn 137 | } 138 | 139 | data "aws_iam_policy_document" "waf_delivery" { 140 | statement { 141 | effect = "Allow" 142 | actions = [ 143 | "kms:Decrypt", 144 | "kms:GenerateDataKey" 145 | ] 146 | resources = [ 147 | aws_kms_key.waf_logs.arn 148 | ] 149 | condition { 150 | test = "StringEquals" 151 | variable = "kms:ViaService" 152 | values = [ 153 | "s3.${local.aws_region_name}.amazonaws" 154 | ] 155 | } 156 | condition { 157 | test = "StringLike" 158 | variable = "kms:EncryptionContext:aws:s3:arn" 159 | values = [ 160 | "arn:aws:s3:::${aws_s3_bucket.waf_logs.arn}/*", 161 | "arn:aws:s3:::${aws_s3_bucket.logging.arn}/*" 162 | ] 163 | } 164 | } 165 | statement { 166 | effect = "Allow" 167 | actions = [ 168 | "s3:AbortMultipartUpload", 169 | "s3:GetBucketLocation", 170 | "s3:GetObject", 171 | "s3:ListBucket", 172 | "s3:ListBucketMultipartUploads", 173 | "s3:PutObject" 174 | ] 175 | resources = [ 176 | "arn:aws:s3:::${aws_s3_bucket.waf_logs.arn}", 177 | "arn:aws:s3:::${aws_s3_bucket.waf_logs.arn}/*", 178 | "arn:aws:s3:::${aws_s3_bucket.logging.arn}", 179 | "arn:aws:s3:::${aws_s3_bucket.logging.arn}/*" 180 | ] 181 | } 182 | statement { 183 | effect = "Allow" 184 | actions = [ 185 | "logs:PutLogEvents" 186 | ] 187 | resources = [ 188 | "arn:aws:logs:${local.aws_region_name}:${local.aws_region_name}:log-group:/aws/kinesisfirehose:log-stream:aws-waf-logs-delivery-${local.account_id}-${local.aws_region_name}}" 189 | ] 190 | } 191 | statement { 192 | effect = "Allow" 193 | actions = [ 194 | "logs:PutLogEvents" 195 | ] 196 | resources = [ 197 | "arn:aws:logs:${local.aws_region_name}:${local.aws_region_name}:log-group:/aws/kinesisfirehose:log-stream:aws-waf-logs-delivery-${local.account_id}-${local.aws_region_name}}" 198 | ] 199 | } 200 | } 201 | 202 | resource "aws_iam_policy" "waf_delivery" { 203 | name = "firehose_delivery_policy-core" 204 | policy = data.aws_iam_policy_document.waf_delivery.json 205 | } 206 | -------------------------------------------------------------------------------- /terraform/organizational-deployment/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "5.40.0" 6 | } 7 | } 8 | 9 | backend "s3" { 10 | // Backend state MUST be properly confgured and is specific to deployment environment. 11 | bucket = "" 12 | workspace_key_prefix = "" 13 | key = "" 14 | region = "" 15 | } 16 | } 17 | 18 | provider "aws" { 19 | region = var.terraform_state_bucket_region 20 | } 21 | 22 | provider "aws" { 23 | alias = "target" 24 | region = var.target_aws_account_region 25 | assume_role { 26 | role_arn = "arn:aws:iam::${var.target_account_id}:role/OrganizationAccountAccessRole" 27 | } 28 | } 29 | 30 | data "aws_caller_identity" "current" {} 31 | 32 | locals { 33 | aws_account = data.aws_caller_identity.current.account_id 34 | 35 | # Based on the target account ID and the FMS Admin account ID, certain modules are only executed within an FMS Administration account 36 | execute_fms_modules = var.target_account_id == var.fms_admin_account_id 37 | } 38 | module "sa_configure_subscribe" { 39 | providers = { 40 | aws = aws.target 41 | } 42 | source = "./modules/aws-shield-advanced-configure-subscribe" 43 | 44 | target_aws_account_region = var.target_aws_account_region 45 | srt_access_role_name = var.srt_access_role_name 46 | srt_access_role_action = var.srt_access_role_action 47 | enable_srt_access = var.enable_srt_access 48 | enable_proactive_engagement = var.enable_proactive_engagement 49 | srt_buckets = var.srt_buckets 50 | acknowledge_service_terms_pricing = var.acknowledge_service_terms_pricing 51 | acknowledge_service_terms_dto = var.acknowledge_service_terms_dto 52 | acknowledge_service_terms_commitment = var.acknowledge_service_terms_commitment 53 | acknowledge_service_terms_auto_renew = var.acknowledge_service_terms_auto_renew 54 | acknowledge_no_unsubscribe = var.acknowledge_no_unsubscribe 55 | emergency_contact_email1 = var.emergency_contact_email1 56 | emergency_contact_email2 = var.emergency_contact_email2 57 | emergency_contact_email3 = var.emergency_contact_email3 58 | emergency_contact_email4 = var.emergency_contact_email4 59 | emergency_contact_email5 = var.emergency_contact_email5 60 | emergency_contact_phone1 = var.emergency_contact_phone1 61 | emergency_contact_phone2 = var.emergency_contact_phone2 62 | emergency_contact_phone3 = var.emergency_contact_phone3 63 | emergency_contact_phone4 = var.emergency_contact_phone4 64 | emergency_contact_phone5 = var.emergency_contact_phone5 65 | emergency_contact_note1 = var.emergency_contact_note1 66 | emergency_contact_note2 = var.emergency_contact_note2 67 | emergency_contact_note3 = var.emergency_contact_note3 68 | emergency_contact_note4 = var.emergency_contact_note4 69 | emergency_contact_note5 = var.emergency_contact_note5 70 | } 71 | 72 | module "sa_fms_shield" { 73 | count = local.execute_fms_modules ? 1 : 0 74 | 75 | providers = { 76 | aws = aws.target 77 | } 78 | source = "./modules/aws-shield-advanced-fms-shield" 79 | 80 | scope_details = var.scope_details 81 | include_exclude_scope = var.include_exclude_scope 82 | scope_type = var.scope_type 83 | resource_tag_usage = var.resource_tag_usage 84 | protect_regional_resource_types = var.protect_regional_resource_types 85 | protect_cloud_front = var.protect_cloud_front 86 | shield_auto_remediate = var.shield_auto_remediate 87 | scope_tag_name1 = var.scope_tag_name1 88 | scope_tag_name2 = var.scope_tag_name2 89 | scope_tag_name3 = var.scope_tag_name3 90 | scope_tag_value1 = var.scope_tag_value1 91 | scope_tag_value2 = var.scope_tag_value2 92 | scope_tag_value3 = var.scope_tag_value3 93 | } 94 | 95 | module "sa_wafv2" { 96 | count = local.execute_fms_modules ? 1 : 0 97 | 98 | providers = { 99 | aws = aws.target 100 | } 101 | source = "./modules/aws-shield-advanced-wafv2" 102 | 103 | scope_details = var.scope_details 104 | include_exclude_scope = var.include_exclude_scope 105 | scope_type = var.scope_type 106 | resource_tag_usage = var.resource_tag_usage 107 | wafv2_protect_regional_resource_types = var.wafv2_protect_regional_resource_types 108 | wafv2_protect_cloudfront = var.wafv2_protect_cloudfront 109 | wafv2_auto_remediate = var.wafv2_auto_remediate 110 | scope_tag_name1 = var.scope_tag_name1 111 | scope_tag_name2 = var.scope_tag_name2 112 | scope_tag_name3 = var.scope_tag_name3 113 | scope_tag_value1 = var.scope_tag_value1 114 | scope_tag_value2 = var.scope_tag_value2 115 | scope_tag_value3 = var.scope_tag_value3 116 | rate_limit_value = var.rate_limit_value 117 | rate_limit_action = var.rate_limit_action 118 | anonymous_ip_amr_action = var.anonymous_ip_amr_action 119 | ip_reputation_amr_action = var.ip_reputation_amr_action 120 | core_rule_set_amr_action = var.core_rule_set_amr_action 121 | known_bad_inputs_amr_actions = var.known_bad_inputs_amr_actions 122 | waf_log_s3_bucket_name = var.waf_log_s3_bucket_name 123 | waf_delivery_role_arn = var.waf_delivery_role_arn 124 | waf_log_kms_key_arn = var.waf_log_kms_key_arn 125 | } 126 | -------------------------------------------------------------------------------- /terraform/organizational-deployment/modules/aws-shield-advanced-wafv2/variables.tf: -------------------------------------------------------------------------------- 1 | variable scope_details { 2 | type = list(string) 3 | default = [] 4 | } 5 | 6 | variable "include_exclude_scope" { 7 | type = string 8 | default = "Include" 9 | validation { 10 | condition = contains(["Include", "Exclude"], var.include_exclude_scope) 11 | error_message = "Allowed values are [ Include | Exclude ]" 12 | } 13 | } 14 | 15 | variable "scope_type" { 16 | description = "Should Firewall Manager Policies be scoped to the entire org (root) or a specific list of OUs (OU)" 17 | type = string 18 | default = "Org" 19 | validation { 20 | condition = contains(["Org", "OU", "Accounts"], var.scope_type) 21 | error_message = "Allowed values are [ Org | OU | Accounts ]" 22 | } 23 | } 24 | 25 | variable "resource_tag_usage" { 26 | description = "Include will scope to only include when ResourceTags match, Exclude will exclude when target resource tags match ResourceTags" 27 | type = string 28 | default = "Include" 29 | validation { 30 | condition = contains(["Include", "Exclude"], var.resource_tag_usage) 31 | error_message = "Allowed values are [ Include | Exclude ]" 32 | } 33 | } 34 | 35 | variable "wafv2_protect_regional_resource_types" { 36 | type = string 37 | default = "AWS::ApiGateway::Stage,AWS::ElasticLoadBalancingV2::LoadBalancer" 38 | validation { 39 | condition = contains(["AWS::ApiGateway::Stage,AWS::ElasticLoadBalancingV2::LoadBalancer", "AWS::ElasticLoadBalancingV2::LoadBalancer", "AWS::ApiGateway::Stage", ""], var.wafv2_protect_regional_resource_types) 40 | error_message = "Allowed values are [ AWS::ApiGateway::Stage,AWS::ElasticLoadBalancingV2::LoadBalancer | AWS::ElasticLoadBalancingV2::LoadBalancer | AWS::ApiGateway::Stage | ]" 41 | } 42 | } 43 | 44 | variable "wafv2_protect_cloudfront" { 45 | type = string 46 | default = "" 47 | validation { 48 | condition = contains(["AWS::CloudFront::Distribution", ""], var.wafv2_protect_cloudfront) 49 | error_message = "Allowed values are [ AWS::CloudFront::Distribution | ]" 50 | } 51 | } 52 | 53 | variable "wafv2_auto_remediate" { 54 | description = "Should in scope AWS resource types that support AWS WAF automatically be remediated?" 55 | type = string 56 | validation { 57 | condition = contains(["Yes | Replace existing existing WebACL", "Yes | If no current WebACL", "No"], var.wafv2_auto_remediate) 58 | error_message = "Allowed values are [ 'Yes | Replace existing existing WebACL' | 'Yes | If no current WebACL' | 'No']" 59 | } 60 | } 61 | 62 | variable "scope_tag_name1" { 63 | type = string 64 | default = "" 65 | } 66 | 67 | variable "scope_tag_name2" { 68 | type = string 69 | default = "" 70 | } 71 | 72 | variable "scope_tag_name3" { 73 | type = string 74 | default = "" 75 | } 76 | 77 | variable "scope_tag_value1" { 78 | type = string 79 | default = "" 80 | } 81 | 82 | variable "scope_tag_value2" { 83 | type = string 84 | default = "" 85 | } 86 | 87 | variable "scope_tag_value3" { 88 | type = string 89 | default = "" 90 | } 91 | 92 | variable "rate_limit_value" { 93 | description = "AWS WAF rate limits use the previous 5 minutes to determine if an IP has exceeded the defined limit." 94 | type = string 95 | default = 10000 96 | } 97 | 98 | variable "rate_limit_action" { 99 | type = string 100 | default = "Block" 101 | validation { 102 | condition = contains(["Block", "Count"], var.rate_limit_action) 103 | error_message = "Allowed values are [ Block | Count ]" 104 | } 105 | } 106 | 107 | variable "anonymous_ip_amr_action" { 108 | description = "The Anonymous IP list rule group contains rules to block requests from services that permit the obfuscation of viewer identity. These include requests from VPNs, proxies, Tor nodes, and hosting providers. This rule group is useful if you want to filter out viewers that might be trying to hide their identity from your application." 109 | type = string 110 | default = "Block" 111 | validation { 112 | condition = contains(["Block", "Count"], var.anonymous_ip_amr_action) 113 | error_message = "Allowed values are [ Block | Count ]" 114 | } 115 | } 116 | 117 | variable "ip_reputation_amr_action" { 118 | description = "The Amazon IP reputation list rule group contains rules that are based on Amazon internal threat intelligence. This is useful if you would like to block IP addresses typically associated with bots or other threats. Blocking these IP addresses can help mitigate bots and reduce the risk of a malicious actor discovering a vulnerable application." 119 | type = string 120 | default = "Block" 121 | validation { 122 | condition = contains(["Block", "Count"], var.ip_reputation_amr_action) 123 | error_message = "Allowed values are [ Block | Count ]" 124 | } 125 | } 126 | 127 | variable "core_rule_set_amr_action" { 128 | description = "The Core rule set (CRS) rule group contains rules that are generally applicable to web applications. This provides protection against exploitation of a wide range of vulnerabilities, including some of the high risk and commonly occurring vulnerabilities described in OWASP publications such as OWASP Top 10" 129 | type = string 130 | default = "Count" 131 | validation { 132 | condition = contains(["Block", "Count"], var.core_rule_set_amr_action) 133 | error_message = "Allowed values are [ Block | Count ]" 134 | } 135 | } 136 | 137 | variable "known_bad_inputs_amr_actions" { 138 | description = "The Known bad inputs rule group contains rules to block request patterns that are known to be invalid and are associated with exploitation or discovery of vulnerabilities. This can help reduce the risk of a malicious actor discovering a vulnerable application." 139 | type = string 140 | default = "Count" 141 | validation { 142 | condition = contains(["Block", "Count"], var.known_bad_inputs_amr_actions) 143 | error_message = "Allowed values are [ Block | Count ]" 144 | } 145 | } 146 | 147 | variable "waf_log_s3_bucket_name" { 148 | type = string 149 | } 150 | 151 | variable "waf_delivery_role_arn" { 152 | type = string 153 | } 154 | 155 | variable "waf_log_kms_key_arn" { 156 | type = string 157 | } -------------------------------------------------------------------------------- /terraform/organizational-deployment/modules/aws-shield-advanced-configure-subscribe/variables.tf: -------------------------------------------------------------------------------- 1 | variable "srt_access_role_name" { 2 | type = string 3 | default = "" 4 | } 5 | 6 | variable "target_aws_account_region" { 7 | type = string 8 | } 9 | 10 | variable "srt_access_role_action" { 11 | type = string 12 | default = "CreateRole" 13 | validation { 14 | condition = contains(["CreateRole", "UseExisting"], var.srt_access_role_action) 15 | error_message = "Allowed values are [ CreateRole | UseExisting ]" 16 | } 17 | } 18 | 19 | variable "enable_srt_access" { 20 | type = bool 21 | default = false 22 | } 23 | 24 | variable "enable_proactive_engagement" { 25 | type = bool 26 | default = false 27 | description = "Enable Proactive Engagement. Note you must also configure emergency contact(s) and health checks for protected resources for this feature to be effective" 28 | } 29 | 30 | variable "srt_buckets" { 31 | type = list(string) 32 | default = [] 33 | validation { 34 | condition = length(var.srt_buckets) <= 10 35 | error_message = "At most 10 buckets are allowed" 36 | } 37 | } 38 | 39 | variable "acknowledge_service_terms_pricing" { 40 | description = "Shield Advanced Service Term | Pricing | $3000 / month subscription fee for a consolidated billing family" 41 | type = bool 42 | default = false 43 | } 44 | 45 | variable "acknowledge_service_terms_dto" { 46 | description = "Shield Advanced Service Term | Pricing | Data transfer out usage fees for all protected resources." 47 | type = bool 48 | default = false 49 | } 50 | 51 | variable "acknowledge_service_terms_commitment" { 52 | description = "Shield Advanced Term | Commitment | I am committing to a 12 month subscription." 53 | type = bool 54 | default = false 55 | } 56 | 57 | variable "acknowledge_service_terms_auto_renew" { 58 | description = "Shield Advanced Term | Auto renewal | Subscription will be auto-renewed after 12 months. However, I can opt out of renewal 30 days prior to the renewal date." 59 | type = bool 60 | default = false 61 | } 62 | 63 | variable "acknowledge_no_unsubscribe" { 64 | description = "Shield Advanced does not un-subscribe on delete | Shield Advanced does not un-subscribe if you delete this stack/this resource." 65 | type = bool 66 | default = false 67 | } 68 | 69 | variable "emergency_contact_email1" { 70 | type = string 71 | default = "" 72 | validation { 73 | condition = can(regex("^\\S+@\\S+\\.\\S+$|", var.emergency_contact_email1)) 74 | error_message = "Invalid input for variable" 75 | } 76 | } 77 | 78 | variable "emergency_contact_email2" { 79 | type = string 80 | default = "" 81 | validation { 82 | condition = can(regex("^\\S+@\\S+\\.\\S+$|", var.emergency_contact_email2)) 83 | error_message = "Invalid input for variable" 84 | } 85 | } 86 | 87 | variable "emergency_contact_email3" { 88 | type = string 89 | default = "" 90 | validation { 91 | condition = can(regex("^\\S+@\\S+\\.\\S+$|", var.emergency_contact_email3)) 92 | error_message = "Invalid input for variable" 93 | } 94 | } 95 | 96 | variable "emergency_contact_email4" { 97 | type = string 98 | default = "" 99 | validation { 100 | condition = can(regex("^\\S+@\\S+\\.\\S+$|", var.emergency_contact_email4)) 101 | error_message = "Invalid input for variable" 102 | } 103 | } 104 | 105 | variable "emergency_contact_email5" { 106 | type = string 107 | default = "" 108 | validation { 109 | condition = can(regex("^\\S+@\\S+\\.\\S+$|", var.emergency_contact_email5)) 110 | error_message = "Invalid input for variable" 111 | } 112 | } 113 | 114 | variable "emergency_contact_phone1" { 115 | type = string 116 | default = "" 117 | validation { 118 | condition = can(regex("^\\+[0-9]{11}|", var.emergency_contact_phone1)) 119 | error_message = "Invalid input for variable" 120 | } 121 | } 122 | 123 | variable "emergency_contact_phone2" { 124 | type = string 125 | default = "" 126 | validation { 127 | condition = can(regex("^\\+[0-9]{11}|", var.emergency_contact_phone2)) 128 | error_message = "Invalid input for variable" 129 | } 130 | } 131 | 132 | variable "emergency_contact_phone3" { 133 | type = string 134 | default = "" 135 | validation { 136 | condition = can(regex("^\\+[0-9]{11}|", var.emergency_contact_phone3)) 137 | error_message = "Invalid input for variable" 138 | } 139 | } 140 | 141 | variable "emergency_contact_phone4" { 142 | type = string 143 | default = "" 144 | validation { 145 | condition = can(regex("^\\+[0-9]{11}|", var.emergency_contact_phone4)) 146 | error_message = "Invalid input for variable" 147 | } 148 | } 149 | 150 | variable "emergency_contact_phone5" { 151 | type = string 152 | default = "" 153 | validation { 154 | condition = can(regex("^\\+[0-9]{11}|", var.emergency_contact_phone5)) 155 | error_message = "Invalid input for variable" 156 | } 157 | } 158 | 159 | variable "emergency_contact_note1" { 160 | type = string 161 | default = "" 162 | nullable = true 163 | validation { 164 | condition = can(regex("^[\\w\\s\\.\\-,:/()+@]*$|", var.emergency_contact_note1)) 165 | error_message = "Invalid input for variable" 166 | } 167 | } 168 | 169 | variable "emergency_contact_note2" { 170 | type = string 171 | default = "" 172 | nullable = true 173 | validation { 174 | condition = can(regex("^[\\w\\s\\.\\-,:/()+@]*$|", var.emergency_contact_note2)) 175 | error_message = "Invalid input for variable" 176 | } 177 | } 178 | 179 | variable "emergency_contact_note3" { 180 | type = string 181 | default = "" 182 | nullable = true 183 | validation { 184 | condition = can(regex("^[\\w\\s\\.\\-,:/()+@]*$|", var.emergency_contact_note3)) 185 | error_message = "Invalid input for variable" 186 | } 187 | } 188 | 189 | variable "emergency_contact_note4" { 190 | type = string 191 | default = "" 192 | nullable = true 193 | validation { 194 | condition = can(regex("^[\\w\\s\\.\\-,:/()+@]*$|", var.emergency_contact_note4)) 195 | error_message = "Invalid input for variable" 196 | } 197 | } 198 | 199 | variable "emergency_contact_note5" { 200 | type = string 201 | default = "" 202 | nullable = true 203 | validation { 204 | condition = can(regex("^[\\w\\s\\.\\-,:/()+@]*$|", var.emergency_contact_note5)) 205 | error_message = "Invalid input for variable" 206 | } 207 | } -------------------------------------------------------------------------------- /terraform/organizational-deployment/modules/aws-shield-advanced-configure-subscribe/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | } 6 | } 7 | 8 | } 9 | 10 | data "aws_caller_identity" "current" {} 11 | 12 | data "aws_partition" "current" {} 13 | 14 | locals { 15 | FirstEmergencyContact = var.emergency_contact_email1 != "" ? true : false 16 | SecondEmergencyContact = var.emergency_contact_email2 != "" ? true : false 17 | ThirdEmergencyContact = var.emergency_contact_email3 != "" ? true : false 18 | FourthEmergencyContact = var.emergency_contact_email4 != "" ? true : false 19 | FifthEmergencyContact = var.emergency_contact_email5 != "" ? true : false 20 | FirstEmergencyContactNote = var.emergency_contact_note1 != "" ? true : false 21 | SecondEmergencyContactNote = var.emergency_contact_note2 != "" ? true : false 22 | ThirdEmergencyContactNote = var.emergency_contact_note3 != "" ? true : false 23 | FourthEmergencyContactNote = var.emergency_contact_note4 != "" ? true : false 24 | FifthEmergencyContactNote = var.emergency_contact_note5 != "" ? true : false 25 | EnableSRTAccessCondition = var.enable_srt_access 26 | SRTCreateRoleCondition = var.srt_access_role_action == "CreateRole" ? true : false 27 | SRTBucketsCondition = !(join(",", var.srt_buckets) != "") ? true : false 28 | ProactiveEngagementCondition = var.enable_proactive_engagement ? true : false 29 | GenerateSRTRoleNameFlag = var.srt_access_role_name == "" ? true : false 30 | AcceptedShieldTerms = alltrue([ 31 | "True" == var.acknowledge_service_terms_pricing, 32 | "True" == var.acknowledge_service_terms_dto, 33 | "True" == var.acknowledge_service_terms_commitment, 34 | "True" == var.acknowledge_service_terms_auto_renew, 35 | "True" == var.acknowledge_no_unsubscribe 36 | ]) 37 | } 38 | 39 | resource "aws_iam_role" "shield_lambda_role" { 40 | assume_role_policy = <<-EOF 41 | { 42 | "Version": "2012-10-17", 43 | "Statement": [ 44 | { 45 | "Action": "sts:AssumeRole", 46 | "Principal": { 47 | "Service": "lambda.amazonaws.com" 48 | }, 49 | "Effect": "Allow" 50 | } 51 | ] 52 | } 53 | EOF 54 | path = "/" 55 | } 56 | 57 | resource "aws_iam_role_policy_attachment" "shield_lambda_policy" { 58 | role = aws_iam_role.shield_lambda_role.name 59 | policy_arn = aws_iam_policy.configure_shield_lambda_policy.arn 60 | } 61 | 62 | resource "aws_iam_policy" "configure_shield_lambda_policy" { 63 | name = "ConfigureShieldLambdaPolicy-${var.target_aws_account_region}" 64 | policy = jsonencode({ 65 | Version = "2012-10-17" 66 | Statement = [ 67 | { 68 | Effect = "Allow" 69 | Action = [ 70 | "logs:CreateLogGroup", 71 | "logs:CreateLogStream", 72 | "logs:PutLogEvents" 73 | ] 74 | Resource = "arn:aws:logs:*:*:*" 75 | }, 76 | { 77 | Sid = "ShieldSubscription" 78 | Effect = "Allow" 79 | Action = [ 80 | "shield:CreateSubscription", 81 | "shield:UpdateSubscription", 82 | "xray:PutTraceSegments", 83 | "xray:PutTelemetryRecords" 84 | ] 85 | Resource = "*" 86 | } 87 | ] 88 | }) 89 | } 90 | 91 | data "archive_file" "configure_shield_lambda" { 92 | type = "zip" 93 | source_file = "${path.module}/code/configure_shield.py" 94 | output_path = "configure_shield_lambda.zip" 95 | } 96 | 97 | resource "aws_lambda_function" "configure_shield_lambda" { 98 | #checkov:skip=CKV_AWS_115:No need to set reserved concurrency 99 | #checkov:skip=CKV_AWS_272:Code signing not covered as part of this example. 100 | #checkov:skip=CKV_AWS_116:DLQ Not part of this solution example 101 | #checkov:skip=CKV_AWS_117:The lambda function does not need to be in a VPC for this example. 102 | function_name = "configure_shield" 103 | filename = data.archive_file.configure_shield_lambda.output_path 104 | source_code_hash = data.archive_file.configure_shield_lambda.output_base64sha256 105 | tracing_config { 106 | mode = "Active" 107 | } 108 | 109 | runtime = "python3.12" 110 | timeout = 15 111 | role = aws_iam_role.shield_lambda_role.arn 112 | handler = "configure_shield.lambda_handler" 113 | 114 | } 115 | 116 | data "aws_lambda_invocation" "configure_shield_lambda" { 117 | function_name = aws_lambda_function.configure_shield_lambda.function_name 118 | 119 | input = <<-JSON 120 | { 121 | "RequestType": "Create" 122 | } 123 | JSON 124 | 125 | depends_on = [ 126 | aws_lambda_function.configure_shield_lambda 127 | ] 128 | } 129 | 130 | resource "aws_iam_role" "srt_access_role" { 131 | count = local.SRTCreateRoleCondition ? 1 : 0 132 | name = local.GenerateSRTRoleNameFlag ? null : var.srt_access_role_name 133 | managed_policy_arns = [ 134 | "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AWSShieldDRTAccessPolicy" 135 | ] 136 | assume_role_policy = <<-EOF 137 | { 138 | "Version": "2012-10-17", 139 | "Statement": [ 140 | { 141 | "Action": "sts:AssumeRole", 142 | "Principal": { 143 | "Service": "drt.shield.amazonaws.com" 144 | }, 145 | "Effect": "Allow", 146 | "Sid": "" 147 | } 148 | ] 149 | } 150 | EOF 151 | } 152 | 153 | locals { 154 | srt_access_role_arn = local.GenerateSRTRoleNameFlag ? "${aws_iam_role.srt_access_role[0].arn}" : "arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.current.account_id}:role/${var.srt_access_role_name}" 155 | } 156 | 157 | resource "aws_shield_drt_access_role_arn_association" "enable_srt_access" { 158 | count = local.EnableSRTAccessCondition ? 1 : 0 159 | 160 | role_arn = local.srt_access_role_arn 161 | } 162 | 163 | resource "aws_shield_drt_access_log_bucket_association" "enable_srt_access" { 164 | for_each = local.EnableSRTAccessCondition ? toset(var.srt_buckets) : toset([]) 165 | 166 | log_bucket = each.key 167 | role_arn_association_id = aws_shield_drt_access_role_arn_association.enable_srt_access[0].role_arn 168 | } 169 | 170 | resource "aws_shield_proactive_engagement" "proactive_engagement" { 171 | count = local.ProactiveEngagementCondition ? 1 : 0 172 | 173 | enabled = true 174 | 175 | dynamic "emergency_contact" { 176 | for_each = local.FirstEmergencyContact ? [1] : [] 177 | content { 178 | email_address = var.emergency_contact_email1 179 | phone_number = var.emergency_contact_phone1 180 | contact_notes = var.emergency_contact_note1 181 | } 182 | } 183 | 184 | dynamic "emergency_contact" { 185 | for_each = local.SecondEmergencyContact ? [1] : [] 186 | content { 187 | email_address = var.emergency_contact_email2 188 | phone_number = var.emergency_contact_phone2 189 | contact_notes = var.emergency_contact_note2 190 | } 191 | } 192 | 193 | dynamic "emergency_contact" { 194 | for_each = local.ThirdEmergencyContact ? [1] : [] 195 | content { 196 | email_address = var.emergency_contact_email3 197 | phone_number = var.emergency_contact_phone3 198 | contact_notes = var.emergency_contact_note3 199 | } 200 | } 201 | 202 | dynamic "emergency_contact" { 203 | for_each = local.FourthEmergencyContact ? [1] : [] 204 | content { 205 | email_address = var.emergency_contact_email4 206 | phone_number = var.emergency_contact_phone4 207 | contact_notes = var.emergency_contact_note4 208 | } 209 | } 210 | 211 | dynamic "emergency_contact" { 212 | for_each = local.FifthEmergencyContact ? [1] : [] 213 | content { 214 | email_address = var.emergency_contact_email5 215 | phone_number = var.emergency_contact_phone5 216 | contact_notes = var.emergency_contact_note5 217 | } 218 | 219 | } 220 | } -------------------------------------------------------------------------------- /cloudformation/templates/fms-shield.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: 2010-09-09 3 | Parameters: 4 | ScopeDetails: 5 | Type: String 6 | Default: 7 | IncludeExcludeScope: 8 | Type: String 9 | Default: Include 10 | AllowedValues: 11 | - Include 12 | - Exclude 13 | ScopeType: 14 | Type: String 15 | Description: "Should Firewall Manager Policies be scoped to the entire org (root) or a specific list of OUs (OU)" 16 | Default: Org 17 | AllowedValues: 18 | - Org 19 | - OU 20 | - Accounts 21 | ResourceTagUsage: 22 | Type: String 23 | Default: Include 24 | Description: Include will scope to only include when ResourceTags match, Exclude will exclude when target resource tags match ResourceTags 25 | AllowedValues: 26 | - Include 27 | - Exclude 28 | optimizeUnassociatedWebACLValue: 29 | Type: String 30 | Description: If True, only create WebACL if at least one resource needs a WebACL 31 | Default: True 32 | AllowedValues: 33 | - True 34 | - False 35 | ProtectRegionalResourceTypes: 36 | Type: String 37 | Description: AWS::ElasticLoadBalancingV2::LoadBalancer,AWS::ElasticLoadBalancing::LoadBalancer,AWS::EC2::EIP 38 | Default: AWS::ElasticLoadBalancingV2::LoadBalancer,AWS::ElasticLoadBalancing::LoadBalancer,AWS::EC2::EIP 39 | AllowedValues: 40 | - AWS::ElasticLoadBalancingV2::LoadBalancer,AWS::ElasticLoadBalancing::LoadBalancer,AWS::EC2::EIP 41 | - AWS::ElasticLoadBalancingV2::LoadBalancer,AWS::ElasticLoadBalancing::LoadBalancer 42 | - AWS::ElasticLoadBalancingV2::LoadBalancer,AWS::EC2::EIP 43 | - AWS::ElasticLoadBalancing::LoadBalancer,AWS::EC2::EIP 44 | - AWS::ElasticLoadBalancingV2::LoadBalancer 45 | - AWS::ElasticLoadBalancing::LoadBalancer 46 | - AWS::EC2::EIP 47 | - 48 | RegionalAutomaticResponseStatus: 49 | Type: String 50 | Default: ENABLED 51 | AllowedValues: 52 | - ENABLED 53 | - DISABLED 54 | RegionalAutomaticResponseAction: 55 | Type: String 56 | Default: COUNT 57 | AllowedValues: 58 | - COUNT 59 | - BLOCK 60 | CloudFrontAutomaticResponseStatus: 61 | Type: String 62 | Default: ENABLED 63 | AllowedValues: 64 | - ENABLED 65 | - DISABLED 66 | CloudFrontAutomaticResponseAction: 67 | Type: String 68 | Default: COUNT 69 | AllowedValues: 70 | - COUNT 71 | - BLOCK 72 | ProtectCloudFront: 73 | Type: String 74 | Default: AWS::CloudFront::Distribution 75 | AllowedValues: 76 | - AWS::CloudFront::Distribution 77 | - 78 | ShieldAutoRemediate: 79 | Type: String 80 | Description: "Should in scope AWS resource types have Shield Advanced protection automatically be remediated?" 81 | Default: "Yes" 82 | AllowedValues: 83 | - "Yes" 84 | - "No" 85 | ScopeTagName1: 86 | Type: String 87 | Default: 88 | ScopeTagName2: 89 | Type: String 90 | Default: 91 | ScopeTagName3: 92 | Type: String 93 | Default: 94 | ScopeTagValue1: 95 | Type: String 96 | Default: 97 | ScopeTagValue2: 98 | Type: String 99 | Default: 100 | ScopeTagValue3: 101 | Type: String 102 | Default: 103 | 104 | 105 | Conditions: 106 | ScopeTagName1Flag: !Not [!Equals [!Ref ScopeTagName1, ""]] 107 | ScopeTagName2Flag: !Not [!Equals [!Ref ScopeTagName2, ""]] 108 | ScopeTagName3Flag: !Not [!Equals [!Ref ScopeTagName3, ""]] 109 | ScopeTagValue1Flag: !Not [!Equals [!Ref ScopeTagValue1, ""]] 110 | ScopeTagValue2Flag: !Not [!Equals [!Ref ScopeTagValue2, ""]] 111 | ScopeTagValue3Flag: !Not [!Equals [!Ref ScopeTagValue3, ""]] 112 | ShieldAutoRemediateFlag: !Not [!Equals [!Ref ShieldAutoRemediate, "No"]] 113 | OUScopeFlag: !Equals [!Ref ScopeType, "OU"] 114 | AccountScopeFlag: !Equals [!Ref ScopeType, "Accounts"] 115 | ExcludeResourceTagFlag: !Equals [!Ref ResourceTagUsage, "Exclude"] 116 | IncludeScopeFlag: !Equals [!Ref IncludeExcludeScope, "Include"] 117 | ExcludeScopeFlag: !Equals [!Ref IncludeExcludeScope, "Exclude"] 118 | CreateRegionalPolicyFlag: !Not [!Equals [!Ref ProtectRegionalResourceTypes, ''] ] 119 | CreateCloudFrontPolicyFlag: !And [ !Not [!Equals [!Ref ProtectCloudFront, ""] ], !Equals [!Ref AWS::Region, 'us-east-1']] 120 | RegionalAutomaticResourceFlag: !Equals [!Ref "RegionalAutomaticResponseStatus", "ENABLED"] 121 | CloudFrontAutomaticResourceFlag: !Equals [!Ref "CloudFrontAutomaticResponseStatus", "ENABLED"] 122 | Resources: 123 | ShieldRegionalResources: 124 | Condition: CreateRegionalPolicyFlag 125 | Type: AWS::FMS::Policy 126 | Properties: 127 | PolicyName: !Sub OneClickShieldRegional-${AWS::StackName} 128 | ResourceType: ResourceTypeList 129 | ResourceTypeList: !Split [",", !Ref ProtectRegionalResourceTypes] 130 | ExcludeResourceTags: !If [ExcludeResourceTagFlag, true, false] 131 | IncludeMap: 132 | ORGUNIT: 133 | !If [IncludeScopeFlag, !If [OUScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 134 | ACCOUNT: 135 | !If [IncludeScopeFlag, !If [AccountScopeFlag, !Split [",", !Ref ScopeDetails] , !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 136 | ExcludeMap: 137 | ORGUNIT: 138 | !If [ExcludeScopeFlag, !If [OUScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 139 | ACCOUNT: 140 | !If [ExcludeScopeFlag, !If [AccountScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 141 | RemediationEnabled: !If [ShieldAutoRemediateFlag, true, false] 142 | SecurityServicePolicyData: 143 | Type: SHIELD_ADVANCED 144 | ManagedServiceData: 145 | !If 146 | - RegionalAutomaticResourceFlag 147 | - !Sub "{\"type\":\"SHIELD_ADVANCED\",\"automaticResponseConfiguration\": {\"automaticResponseStatus\":\"${RegionalAutomaticResponseStatus}\", \"automaticResponseAction\":\"${RegionalAutomaticResponseAction}\"}}" 148 | - !Ref "AWS::NoValue" 149 | DeleteAllPolicyResources: false 150 | ResourceTags: 151 | !If 152 | - ScopeTagName1Flag 153 | - 154 | - !If 155 | - ScopeTagName1Flag 156 | - Key: !Ref ScopeTagName1 157 | Value: !If [ScopeTagName1Flag, !Ref ScopeTagValue1, ""] 158 | - !Ref "AWS::NoValue" 159 | - !If 160 | - ScopeTagName2Flag 161 | - Key: !Ref ScopeTagName2 162 | Value: !If [ScopeTagName2Flag, !Ref ScopeTagValue2, ""] 163 | - !Ref "AWS::NoValue" 164 | - !If 165 | - ScopeTagName3Flag 166 | - Key: !Ref ScopeTagName3 167 | Value: !If [ScopeTagName3Flag, !Ref ScopeTagValue3, ""] 168 | - !Ref "AWS::NoValue" 169 | - !Ref "AWS::NoValue" 170 | Tags: 171 | - Key: aws-sample-project-name 172 | Value: aws-shield-advanced-one-click-deployment 173 | ShieldCloudFrontResources: 174 | Condition: CreateCloudFrontPolicyFlag 175 | Type: AWS::FMS::Policy 176 | Properties: 177 | PolicyName: !Sub OneClickShieldGlobal-${AWS::StackName} 178 | ResourceType: AWS::CloudFront::Distribution 179 | ExcludeResourceTags: !If [ExcludeResourceTagFlag, true, false] 180 | IncludeMap: 181 | ORGUNIT: 182 | !If [IncludeScopeFlag, !If [OUScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 183 | ACCOUNT: 184 | !If [IncludeScopeFlag, !If [AccountScopeFlag, !Split [",", !Ref ScopeDetails] , !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 185 | ExcludeMap: 186 | ORGUNIT: 187 | !If [ExcludeScopeFlag, !If [OUScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 188 | ACCOUNT: 189 | !If [ExcludeScopeFlag, !If [AccountScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 190 | RemediationEnabled: !If [ShieldAutoRemediateFlag, true, false] 191 | SecurityServicePolicyData: 192 | Type: SHIELD_ADVANCED 193 | ManagedServiceData: 194 | !If 195 | - CloudFrontAutomaticResourceFlag 196 | - !Sub "{\"type\":\"SHIELD_ADVANCED\",\"automaticResponseConfiguration\":{\"automaticResponseStatus\":\"${CloudFrontAutomaticResponseStatus}\",\"automaticResponseAction\":\"${CloudFrontAutomaticResponseAction}\"},\"optimizeUnassociatedWebACL\":\"${optimizeUnassociatedWebACLValue}\"}" 197 | - !Ref "AWS::NoValue" 198 | DeleteAllPolicyResources: false 199 | ResourceTags: 200 | !If 201 | - ScopeTagName1Flag 202 | - 203 | - !If 204 | - ScopeTagName1Flag 205 | - Key: !Ref ScopeTagName1 206 | Value: !If [ScopeTagValue1Flag, !Ref ScopeTagValue1, ""] 207 | - !Ref "AWS::NoValue" 208 | - !If 209 | - ScopeTagName2Flag 210 | - Key: !Ref ScopeTagName2 211 | Value: !If [ScopeTagValue2Flag, !Ref ScopeTagValue2, ""] 212 | - !Ref "AWS::NoValue" 213 | - !If 214 | - ScopeTagName3Flag 215 | - Key: !Ref ScopeTagName3 216 | Value: !If [ScopeTagValue3Flag, !Ref ScopeTagValue3, ""] 217 | - !Ref "AWS::NoValue" 218 | - !Ref "AWS::NoValue" 219 | Tags: 220 | - Key: aws-sample-project-name 221 | Value: aws-shield-advanced-one-click-deployment -------------------------------------------------------------------------------- /terraform/core-deployment/modules/aws-shield-advanced-base-support-infra/athena.tf: -------------------------------------------------------------------------------- 1 | resource "aws_athena_workgroup" "athena" { 2 | lifecycle { 3 | prevent_destroy = true 4 | } 5 | name = "oneclickshieldwaf-core-Workgroup" 6 | state = "ENABLED" 7 | configuration { 8 | enforce_workgroup_configuration = true 9 | result_configuration { 10 | output_location = "s3://${aws_s3_bucket.waf_logs.id}/athenaOutput" 11 | encryption_configuration { 12 | encryption_option = "SSE_S3" 13 | } 14 | } 15 | } 16 | } 17 | 18 | data "aws_lambda_invocation" "build_athena_views_call_with_lake_formation_permissions" { 19 | count = var.use_lake_formation_permissions ? 1 : 0 20 | depends_on = [ 21 | aws_lakeformation_permissions.athena_table_lake_formation_select_permissions 22 | ] 23 | function_name = aws_lambda_function.athena_query_lambda.function_name 24 | 25 | input = <<-JSON 26 | { 27 | "DetailedViewQueryId": "${aws_athena_named_query.athena_named_query_ip_detailed.id}" 28 | } 29 | JSON 30 | } 31 | 32 | data "aws_lambda_invocation" "build_athena_views_call" { 33 | count = var.use_lake_formation_permissions ? 0 : 1 34 | depends_on = [ 35 | aws_iam_policy.athena_query_lambda 36 | ] 37 | function_name = aws_lambda_function.athena_query_lambda.function_name 38 | 39 | input = <<-JSON 40 | { 41 | "DetailedViewQueryId": "${aws_athena_named_query.athena_named_query_ip_detailed.id}", 42 | } 43 | JSON 44 | } 45 | 46 | resource "aws_iam_role" "athena_query_lambda" { 47 | assume_role_policy = <<-EOF 48 | { 49 | "Version": "2012-10-17", 50 | "Statement": [ 51 | { 52 | "Action": "sts:AssumeRole", 53 | "Principal": { 54 | "Service": "lambda.amazonaws.com" 55 | }, 56 | "Effect": "Allow" 57 | } 58 | ] 59 | } 60 | EOF 61 | path = "/" 62 | } 63 | 64 | 65 | resource "aws_iam_role_policy_attachment" "athena_query_lambda" { 66 | role = aws_iam_role.athena_query_lambda.name 67 | policy_arn = aws_iam_policy.athena_query_lambda.arn 68 | } 69 | 70 | resource "aws_iam_policy" "athena_query_lambda" { 71 | name = "LocalPolicy-core" 72 | policy = jsonencode({ 73 | Version = "2012-10-17" 74 | Statement = [ 75 | { 76 | Effect = "Allow" 77 | Action = [ 78 | "athena:GetQueryExecution", 79 | "athena:GetNamedQuery", 80 | "athena:ListNamedQueries", 81 | "athena:StartQueryExecution" 82 | ] 83 | Resource = "arn:aws:athena:${local.aws_region_name}:${local.account_id}:workgroup/${aws_athena_workgroup.athena.name}" 84 | }, 85 | { 86 | Effect = "Allow" 87 | Action = [ 88 | "logs:CreateLogGroup", 89 | "logs:CreateLogStream", 90 | "logs:PutLogEvents" 91 | ] 92 | Resource = "arn:aws:logs:*:*:*" 93 | }, 94 | { 95 | Effect = "Allow" 96 | Action = [ 97 | "glue:Get*", 98 | "glue:Update*", 99 | "glue:CreateTable" 100 | ] 101 | Resource = [ 102 | "arn:aws:glue:${local.aws_region_name}:${local.account_id}:catalog", 103 | "arn:aws:glue:${local.aws_region_name}:${local.account_id}:database/default", 104 | "arn:aws:glue:${local.aws_region_name}:${local.account_id}:database/default/*", 105 | "arn:aws:glue:${local.aws_region_name}:${local.account_id}:database/oneclickshieldwaf", 106 | "arn:aws:glue:${local.aws_region_name}:${local.account_id}:table/oneclickshieldwaf/*" 107 | ] 108 | }, 109 | { 110 | Effect = "Allow" 111 | Action = [ 112 | "athena:DeleteWorkGroup" 113 | ] 114 | Resource = [ 115 | "arn:aws:athena:${local.aws_region_name}:${local.account_id}:workgroup/${aws_athena_workgroup.athena.name}" 116 | ] 117 | }, 118 | { 119 | Effect = "Allow" 120 | Action = [ 121 | "s3:Get*", 122 | "s3:Put*", 123 | "s3:List*" 124 | ] 125 | Resource = [ 126 | "arn:aws:s3:::${aws_s3_bucket.waf_logs.id}/athenaOutput/*", 127 | "arn:aws:s3:::${aws_s3_bucket.waf_logs.id}" 128 | ] 129 | } 130 | ] 131 | }) 132 | } 133 | 134 | data "archive_file" "athena_query_lambda" { 135 | type = "zip" 136 | source_file = "${path.module}/code/athena_query_lambda.py" 137 | output_path = "athena_query_lambda.zip" 138 | } 139 | 140 | resource "aws_kms_key" "athena_lambda_environment" { 141 | lifecycle { 142 | prevent_destroy = true 143 | } 144 | description = "Athena Lambda environment encryption" 145 | enable_key_rotation = true 146 | 147 | } 148 | 149 | resource "aws_lambda_function" "athena_query_lambda" { 150 | #checkov:skip=CKV_AWS_115:No need to set reserved concurrency 151 | #checkov:skip=CKV_AWS_272:Code signing not covered as part of this example. 152 | #checkov:skip=CKV_AWS_116:DLQ Not part of this solution example 153 | #checkov:skip=CKV_AWS_117:The lambda function does not need to be in a VPC for this example. 154 | runtime = "python3.12" 155 | function_name = "athena_create_views_query-core" 156 | filename = "athena_query_lambda.zip" 157 | source_code_hash = data.archive_file.athena_query_lambda.output_base64sha256 158 | 159 | role = aws_iam_role.athena_query_lambda.arn 160 | handler = "athena_query_lambda.lambda_handler" 161 | timeout = 300 162 | 163 | kms_key_arn = aws_kms_key.athena_lambda_environment.arn 164 | environment { 165 | variables = { 166 | s3BasePath = "s3://${aws_s3_bucket.waf_logs.id}/athenaOutput/" 167 | workGroupName = aws_athena_workgroup.athena.name 168 | glueDatabase = aws_glue_catalog_database.glue_waf_logs.id 169 | } 170 | } 171 | 172 | tracing_config { 173 | mode = "Active" 174 | } 175 | 176 | } 177 | 178 | resource "aws_athena_named_query" "athena_named_query_ip_detailed" { 179 | database = aws_glue_catalog_database.glue_waf_logs.arn 180 | description = "Detailed and Formatted Core RBR Data" 181 | name = "waf_detailed" 182 | workgroup = aws_athena_workgroup.athena.name 183 | query = <<-EOF 184 | SELECT 185 | tz_window 186 | , sourceip 187 | , COALESCE(NULLIF(args, ''), args) args 188 | , COALESCE(NULLIF(httpSourceName, ''), httpSourceName) httpSourceName 189 | , country 190 | , uri 191 | , labels 192 | , accountId 193 | , webACLName 194 | , method 195 | , requestId 196 | , ntRules 197 | , region 198 | , scope 199 | , terminatingRuleId 200 | , action 201 | , datehour 202 | FROM 203 | ( 204 | SELECT 205 | httprequest.clientip sourceip 206 | , httprequest.country country 207 | , httprequest.uri uri 208 | , httprequest.args args 209 | , httprequest.httpMethod method 210 | , httprequest.requestId requestId 211 | , httpSourceName 212 | , transform(filter(httprequest.headers, (x) -> x.name = 'Host'),(x) -> x.value) as domainName 213 | , "split_part"(webaclId, ':', 5) accountId 214 | , "split"("split_part"(webaclId, ':', 6), '/', 4)[4] webACLName 215 | , "split_part"(webaclId, ':', 4) region 216 | , "split"("split_part"(webaclId, ':', 6), '/', 4)[1] scope 217 | , webaclId 218 | , "array_join"("transform"(nonTerminatingMatchingRules, (x) -> x.ruleId), ',') ntRules 219 | , concat("transform"("filter"(labels, (x) -> (x.name LIKE 'awswaf:managed:aws:%')), (x) -> "split"(x.name, 'awswaf:managed:aws:')[2]), 220 | "transform"("filter"(labels, (x) -> (NOT (x.name LIKE 'awswaf%'))), (x) -> x.name)) as labels 221 | , terminatingRuleId 222 | , "from_unixtime"(("floor"((timestamp / (1000 * 300))) * 300)) tz_window 223 | , action 224 | , datehour 225 | FROM "${aws_glue_catalog_database.glue_waf_logs.arn}"."waf_logs_raw" 226 | ) 227 | EOF 228 | } 229 | 230 | resource "aws_athena_named_query" "athena_named_query_by_uriip" { 231 | database = aws_glue_catalog_database.glue_waf_logs.arn 232 | description = "Count by URI then Source IP over time." 233 | name = "URIRate" 234 | workgroup = aws_athena_workgroup.athena.name 235 | query = <<-EOF 236 | SELECT 237 | count(sourceip) as count 238 | , tz_window 239 | , sourceip 240 | , uri 241 | FROM( 242 | SELECT * 243 | FROM "${aws_glue_catalog_database.glue_waf_logs.arn}"."waf_detailed" 244 | ) 245 | GROUP BY tz_window, sourceip, uri 246 | ORDER BY tz_window desc, count DESC" 247 | EOF 248 | } 249 | 250 | resource "aws_athena_named_query" "athena_named_query_by_country" { 251 | database = aws_glue_catalog_database.glue_waf_logs.arn 252 | description = "Count by Country then Source IP over time." 253 | name = "CountryRate" 254 | workgroup = aws_athena_workgroup.athena.name 255 | query = <<-EOF 256 | SELECT 257 | count(sourceip) as count 258 | , tz_window 259 | , sourceip 260 | , country 261 | FROM ( 262 | SELECT * 263 | FROM "${aws_glue_catalog_database.glue_waf_logs.arn}"."waf_detailed" 264 | ) 265 | GROUP BY tz_window, sourceip, country 266 | ORDER BY tz_window desc, count DESC 267 | EOF 268 | } 269 | 270 | resource "aws_athena_named_query" "athena_named_query_ip_rep" { 271 | database = aws_glue_catalog_database.glue_waf_logs.arn 272 | description = "Identify Top Client IP to specific URI path by IP" 273 | name = "SourceIPReputations" 274 | workgroup = aws_athena_workgroup.athena.name 275 | query = <<-EOF 276 | SELECT 277 | reputation, 278 | count(sourceip) AS count, 279 | sourceip, 280 | uri, 281 | tz_window 282 | FROM ( 283 | SELECT sourceip, 284 | uri, 285 | tz_window, 286 | ntRules, 287 | filter (labels, (x)->(x LIKE '%IPReputationList')) as reputation 288 | FROM "${aws_glue_catalog_database.glue_waf_logs.arn}"."waf_detailed" 289 | ) 290 | where reputation <> array [] 291 | GROUP BY tz_window, 292 | sourceip, 293 | uri, 294 | reputation 295 | order by reputation, count desc; 296 | EOF 297 | } 298 | 299 | resource "aws_athena_named_query" "athena_named_query_ip_anon" { 300 | database = aws_glue_catalog_database.glue_waf_logs.arn 301 | description = "Identify Top Client IP to specific URI path by IP" 302 | name = "SourceIPAnonymousorHiddenOwner" 303 | workgroup = aws_athena_workgroup.athena.name 304 | query = <<-EOF 305 | SELECT if(anonymous = array[], 306 | Null, 307 | array_join(anonymous, ','))as anonymous, 308 | count(sourceip) AS count, 309 | sourceip, 310 | uri, 311 | tz_window 312 | FROM ( 313 | SELECT sourceip, 314 | uri, 315 | tz_window, 316 | filter( labels, x -> x LIKE '%anonymous-ip-list%') as anonymous 317 | FROM "${aws_glue_catalog_database.glue_waf_logs.arn}"."waf_detailed") 318 | WHERE anonymous <> array [] 319 | GROUP BY tz_window,sourceip,uri,anonymous 320 | order by anonymous desc, count; 321 | EOF 322 | } 323 | 324 | resource "aws_athena_named_query" "athena_named_query_bot_control" { 325 | database = aws_glue_catalog_database.glue_waf_logs.arn 326 | description = "Identify Bot Traffic" 327 | name = "BotControlMatch" 328 | workgroup = aws_athena_workgroup.athena.name 329 | query = <<-EOF 330 | SELECT 331 | IF((botSignal = ARRAY[]), null, "split"(botSignal[1], 'bot-control:')[2]) botSignal, 332 | IF((botCategory = ARRAY[]), null, "split"(botCategory[1], 'bot-control:')[2]) botCategory, 333 | IF((botName = ARRAY[]), null, "split"(botName[1], 'bot-control:')[2]) botName, 334 | count(sourceip) as count, 335 | tz_window, 336 | sourceip, 337 | uri 338 | FROM ( 339 | SELECT 340 | filter(botLabels, x -> split(x,':')[2] = 'signal') as botSignal, 341 | filter(botLabels, x -> split(x,':')[3] = 'category') as botCategory, 342 | filter(botLabels, x -> split(x,':')[3] = 'name') as botName, 343 | tz_window, 344 | sourceip, 345 | uri, 346 | datehour 347 | FROM ( 348 | SELECT sourceip, 349 | tz_window, 350 | filter(labels, 351 | x -> x LIKE 'bot-control%') AS botLabels, action, labels, uri, datehour 352 | FROM "${aws_glue_catalog_database.glue_waf_logs.arn}"."waf_detailed" 353 | ) 354 | WHERE botLabels <> array[] 355 | ) 356 | GROUP BY tz_window, sourceip, botSignal, botCategory, botName, uri 357 | EOF 358 | } 359 | 360 | resource "aws_lakeformation_permissions" "athena_table_lake_formation_select_permissions" { 361 | count = var.use_lake_formation_permissions ? 1 : 0 362 | principal = aws_iam_role.athena_query_lambda.arn 363 | database { 364 | catalog_id = local.account_id 365 | name = "oneclickshieldwaf" 366 | } 367 | permissions = [ 368 | "CREATE_TABLE", 369 | "ALTER", 370 | "DESCRIBE" 371 | ] 372 | permissions_with_grant_option = [ 373 | "CREATE_TABLE", 374 | "ALTER", 375 | "DESCRIBE" 376 | ] 377 | } 378 | 379 | resource "aws_lakeformation_permissions" "athena_table_lake_formation_permissions" { 380 | count = var.use_lake_formation_permissions ? 1 : 0 381 | principal = aws_iam_role.athena_query_lambda.arn 382 | table { 383 | name = aws_glue_catalog_table.glue_waf_logs.name 384 | catalog_id = local.account_id 385 | database_name = "oneclickshieldwaf" 386 | } 387 | permissions = [ 388 | "SELECT" 389 | ] 390 | permissions_with_grant_option = [ 391 | "SELECT" 392 | ] 393 | 394 | depends_on = [ 395 | data.aws_lambda_invocation.build_athena_views_call_with_lake_formation_permissions, 396 | data.aws_lambda_invocation.build_athena_views_call 397 | ] 398 | } -------------------------------------------------------------------------------- /terraform/organizational-deployment/variables.tf: -------------------------------------------------------------------------------- 1 | ### Logic Control variables 2 | variable "fms_admin_account_id" { 3 | type = string 4 | description = "The account ID which has been designated as the FMS Administrator account (Default)" 5 | } 6 | 7 | variable "terraform_state_bucket_region" { 8 | type = string 9 | } 10 | 11 | variable "target_aws_account_region" { 12 | type = string 13 | } 14 | 15 | variable "target_account_id" { 16 | description = "DO NOT SET. This is generated by the python script. Used to identify the target account for each of the organizational deployment runs. " 17 | type = string 18 | } 19 | 20 | variable "scope_regions" { 21 | description = "DO NOT SET. This is generated by the output from core-deployment and passed via the pythong script. A comma separated list of AWS regions, e.g. us-east-1, us-east-2" 22 | type = string 23 | } 24 | 25 | ### sa_configure_subscribe module 26 | 27 | variable "srt_access_role_name" { 28 | type = string 29 | default = "" 30 | } 31 | 32 | variable "srt_access_role_action" { 33 | type = string 34 | default = "CreateRole" 35 | validation { 36 | condition = contains(["CreateRole", "UseExisting"], var.srt_access_role_action) 37 | error_message = "Allowed values are [ CreateRole | UseExisting ]" 38 | } 39 | } 40 | 41 | variable "enable_srt_access" { 42 | type = bool 43 | default = false 44 | } 45 | 46 | variable "enable_proactive_engagement" { 47 | type = bool 48 | description = "Enable Proactive Engagement. Note you must also configure emergency contact(s) and health checks for protected resources for this feature to be effective" 49 | default = false 50 | } 51 | 52 | variable "srt_buckets" { 53 | type = list(string) 54 | default = [] 55 | validation { 56 | condition = length(var.srt_buckets) <= 10 57 | error_message = "At most 10 buckets are allowed" 58 | } 59 | } 60 | 61 | variable "acknowledge_service_terms_pricing" { 62 | description = "Shield Advanced Service Term | Pricing | $3000 / month subscription fee for a consolidated billing family" 63 | type = bool 64 | default = false 65 | } 66 | 67 | variable "acknowledge_service_terms_dto" { 68 | description = "Shield Advanced Service Term | Pricing | Data transfer out usage fees for all protected resources." 69 | type = bool 70 | default = false 71 | } 72 | 73 | variable "acknowledge_service_terms_commitment" { 74 | description = "Shield Advanced Term | Commitment | I am committing to a 12 month subscription." 75 | type = bool 76 | default = false 77 | } 78 | 79 | variable "acknowledge_service_terms_auto_renew" { 80 | description = "Shield Advanced Term | Auto renewal | Subscription will be auto-renewed after 12 months. However, I can opt out of renewal 30 days prior to the renewal date." 81 | type = bool 82 | default = false 83 | } 84 | 85 | variable "acknowledge_no_unsubscribe" { 86 | description = "Shield Advanced does not un-subscribe on delete | Shield Advanced does not un-subscribe if you delete this stack/this resource." 87 | type = bool 88 | default = false 89 | } 90 | 91 | variable "emergency_contact_email1" { 92 | type = string 93 | default = "" 94 | validation { 95 | condition = can(regex("^\\S+@\\S+\\.\\S+$|", var.emergency_contact_email1)) 96 | error_message = "Invalid input for variable" 97 | } 98 | } 99 | 100 | variable "emergency_contact_email2" { 101 | type = string 102 | default = "" 103 | validation { 104 | condition = can(regex("^\\S+@\\S+\\.\\S+$|", var.emergency_contact_email2)) 105 | error_message = "Invalid input for variable" 106 | } 107 | } 108 | 109 | variable "emergency_contact_email3" { 110 | type = string 111 | default = "" 112 | validation { 113 | condition = can(regex("^\\S+@\\S+\\.\\S+$|", var.emergency_contact_email3)) 114 | error_message = "Invalid input for variable" 115 | } 116 | } 117 | 118 | variable "emergency_contact_email4" { 119 | type = string 120 | default = "" 121 | validation { 122 | condition = can(regex("^\\S+@\\S+\\.\\S+$|", var.emergency_contact_email4)) 123 | error_message = "Invalid input for variable" 124 | } 125 | } 126 | 127 | variable "emergency_contact_email5" { 128 | type = string 129 | default = "" 130 | validation { 131 | condition = can(regex("^\\S+@\\S+\\.\\S+$|", var.emergency_contact_email5)) 132 | error_message = "Invalid input for variable" 133 | } 134 | } 135 | 136 | variable "emergency_contact_phone1" { 137 | type = string 138 | default = "" 139 | validation { 140 | condition = can(regex("^\\+[0-9]{11}|", var.emergency_contact_phone1)) 141 | error_message = "Invalid input for variable" 142 | } 143 | } 144 | 145 | variable "emergency_contact_phone2" { 146 | type = string 147 | default = "" 148 | validation { 149 | condition = can(regex("^\\+[0-9]{11}|", var.emergency_contact_phone2)) 150 | error_message = "Invalid input for variable" 151 | } 152 | } 153 | 154 | variable "emergency_contact_phone3" { 155 | type = string 156 | default = "" 157 | validation { 158 | condition = can(regex("^\\+[0-9]{11}|", var.emergency_contact_phone3)) 159 | error_message = "Invalid input for variable" 160 | } 161 | } 162 | 163 | variable "emergency_contact_phone4" { 164 | type = string 165 | default = "" 166 | validation { 167 | condition = can(regex("^\\+[0-9]{11}|", var.emergency_contact_phone4)) 168 | error_message = "Invalid input for variable" 169 | } 170 | } 171 | 172 | variable "emergency_contact_phone5" { 173 | type = string 174 | default = "" 175 | validation { 176 | condition = can(regex("^\\+[0-9]{11}|", var.emergency_contact_phone5)) 177 | error_message = "Invalid input for variable" 178 | } 179 | } 180 | 181 | variable "emergency_contact_note1" { 182 | type = string 183 | default = "" 184 | nullable = true 185 | validation { 186 | condition = can(regex("^[\\w\\s\\.\\-,:/()+@]*$|", var.emergency_contact_note1)) 187 | error_message = "Invalid input for variable" 188 | } 189 | } 190 | 191 | variable "emergency_contact_note2" { 192 | type = string 193 | default = "" 194 | nullable = true 195 | 196 | validation { 197 | condition = can(regex("^[\\w\\s\\.\\-,:/()+@]*$|", var.emergency_contact_note2)) 198 | error_message = "Invalid input for variable" 199 | } 200 | } 201 | 202 | variable "emergency_contact_note3" { 203 | type = string 204 | default = "" 205 | nullable = true 206 | validation { 207 | condition = can(regex("^[\\w\\s\\.\\-,:/()+@]*$|", var.emergency_contact_note3)) 208 | error_message = "Invalid input for variable" 209 | } 210 | } 211 | 212 | variable "emergency_contact_note4" { 213 | type = string 214 | default = "" 215 | nullable = true 216 | validation { 217 | condition = can(regex("^[\\w\\s\\.\\-,:/()+@]*$|", var.emergency_contact_note4)) 218 | error_message = "Invalid input for variable" 219 | } 220 | } 221 | 222 | variable "emergency_contact_note5" { 223 | type = string 224 | default = "" 225 | nullable = true 226 | validation { 227 | condition = can(regex("^[\\w\\s\\.\\-,:/()+@]*$|", var.emergency_contact_note5)) 228 | error_message = "Invalid input for variable" 229 | } 230 | } 231 | 232 | ### sa_fms_shield module 233 | 234 | variable "scope_type" { 235 | description = "Should Firewall Manager Policies be scoped to the entire org (root) or a specific list of OUs (OU)" 236 | type = string 237 | default = "Org" 238 | validation { 239 | condition = contains(["Org", "OU", "Accounts"], var.scope_type) 240 | error_message = "scope_type must be one of [ Org | OU | Accounts ]" 241 | } 242 | } 243 | 244 | variable "protect_regional_resource_types" { 245 | description = "AWS::ElasticLoadBalancingV2::LoadBalancer,AWS::ElasticLoadBalancing::LoadBalancer,AWS::EC2::EIP" 246 | type = list(string) 247 | default = ["AWS::ElasticLoadBalancingV2::LoadBalancer","AWS::ElasticLoadBalancing::LoadBalancer","AWS::EC2::EIP"] 248 | } 249 | 250 | variable "protect_cloud_front" { 251 | type = string 252 | default = "" 253 | } 254 | 255 | variable "shield_auto_remediate" { 256 | description = "Should in scope AWS resource types have Shield Advanced protection automatically be remediated?" 257 | type = string 258 | default = "Yes" 259 | } 260 | 261 | variable "scope_tag_name1" { 262 | type = string 263 | default = "" 264 | } 265 | 266 | variable "scope_tag_name2" { 267 | type = string 268 | default = "" 269 | } 270 | 271 | variable "scope_tag_name3" { 272 | type = string 273 | default = "" 274 | } 275 | 276 | variable "scope_tag_value1" { 277 | type = string 278 | default = "" 279 | } 280 | 281 | variable "scope_tag_value2" { 282 | type = string 283 | default = "" 284 | } 285 | 286 | variable "scope_tag_value3" { 287 | type = string 288 | default = "" 289 | } 290 | 291 | ### sa_wafv2 module 292 | 293 | variable "scope_details" { 294 | type = list(string) 295 | default = [] 296 | } 297 | 298 | variable "include_exclude_scope" { 299 | type = string 300 | default = "Include" 301 | validation { 302 | condition = contains(["Include", "Exclude"], var.include_exclude_scope) 303 | error_message = "Allowed values are [ Include | Exclude ]" 304 | } 305 | } 306 | 307 | variable "resource_tag_usage" { 308 | description = "Include will scope to only include when ResourceTags match, Exclude will exclude when target resource tags match ResourceTags" 309 | type = string 310 | default = "Include" 311 | validation { 312 | condition = contains(["Include", "Exclude"], var.resource_tag_usage) 313 | error_message = "Allowed values are [ Include | Exclude ]" 314 | } 315 | } 316 | 317 | variable "wafv2_protect_regional_resource_types" { 318 | type = string 319 | default = "AWS::ApiGateway::Stage,AWS::ElasticLoadBalancingV2::LoadBalancer" 320 | validation { 321 | condition = contains(["AWS::ApiGateway::Stage,AWS::ElasticLoadBalancingV2::LoadBalancer", "AWS::ElasticLoadBalancingV2::LoadBalancer", "AWS::ApiGateway::Stage", ""], var.wafv2_protect_regional_resource_types) 322 | error_message = "Allowed values are [ AWS::ApiGateway::Stage,AWS::ElasticLoadBalancingV2::LoadBalancer | AWS::ElasticLoadBalancingV2::LoadBalancer | AWS::ApiGateway::Stage | ]" 323 | } 324 | } 325 | 326 | variable "wafv2_protect_cloudfront" { 327 | type = string 328 | default = "" 329 | validation { 330 | condition = contains(["AWS::CloudFront::Distribution", ""], var.wafv2_protect_cloudfront) 331 | error_message = "Allowed values are [ AWS::CloudFront::Distribution | ]" 332 | } 333 | } 334 | 335 | variable "wafv2_auto_remediate" { 336 | description = "Should in scope AWS resource types that support AWS WAF automatically be remediated?" 337 | type = string 338 | validation { 339 | condition = contains(["Yes | Replace existing existing WebACL", "Yes | If no current WebACL", "No"], var.wafv2_auto_remediate) 340 | error_message = "Allowed values are [ 'Yes | Replace existing existing WebACL' | 'Yes | If no current WebACL' | 'No']" 341 | } 342 | } 343 | 344 | variable "scope_tag_name_1" { 345 | type = string 346 | default = "" 347 | } 348 | 349 | variable "scope_tag_name_2" { 350 | type = string 351 | default = "" 352 | } 353 | 354 | variable "scope_tag_name_3" { 355 | type = string 356 | default = "" 357 | } 358 | 359 | variable "scope_tag_value_1" { 360 | type = string 361 | default = "" 362 | } 363 | 364 | variable "scope_tag_value_2" { 365 | type = string 366 | default = "" 367 | } 368 | 369 | variable "scope_tag_value_3" { 370 | type = string 371 | default = "" 372 | } 373 | 374 | variable "rate_limit_value" { 375 | description = "AWS WAF rate limits use the previous 5 minutes to determine if an IP has exceeded the defined limit." 376 | type = string 377 | default = 10000 378 | } 379 | 380 | variable "rate_limit_action" { 381 | type = string 382 | default = "Block" 383 | validation { 384 | condition = contains(["Block", "Count"], var.rate_limit_action) 385 | error_message = "Allowed values are [ Block | Count ]" 386 | } 387 | } 388 | 389 | variable "anonymous_ip_amr_action" { 390 | description = "The Anonymous IP list rule group contains rules to block requests from services that permit the obfuscation of viewer identity. These include requests from VPNs, proxies, Tor nodes, and hosting providers. This rule group is useful if you want to filter out viewers that might be trying to hide their identity from your application." 391 | type = string 392 | default = "Block" 393 | validation { 394 | condition = contains(["Block", "Count"], var.anonymous_ip_amr_action) 395 | error_message = "Allowed values are [ Block | Count ]" 396 | } 397 | } 398 | 399 | variable "ip_reputation_amr_action" { 400 | description = "The Amazon IP reputation list rule group contains rules that are based on Amazon internal threat intelligence. This is useful if you would like to block IP addresses typically associated with bots or other threats. Blocking these IP addresses can help mitigate bots and reduce the risk of a malicious actor discovering a vulnerable application." 401 | type = string 402 | default = "Block" 403 | validation { 404 | condition = contains(["Block", "Count"], var.ip_reputation_amr_action) 405 | error_message = "Allowed values are [ Block | Count ]" 406 | } 407 | } 408 | 409 | variable "core_rule_set_amr_action" { 410 | description = "The Core rule set (CRS) rule group contains rules that are generally applicable to web applications. This provides protection against exploitation of a wide range of vulnerabilities, including some of the high risk and commonly occurring vulnerabilities described in OWASP publications such as OWASP Top 10" 411 | type = string 412 | default = "Count" 413 | validation { 414 | condition = contains(["Block", "Count"], var.core_rule_set_amr_action) 415 | error_message = "Allowed values are [ Block | Count ]" 416 | } 417 | } 418 | 419 | variable "known_bad_inputs_amr_actions" { 420 | description = "The Known bad inputs rule group contains rules to block request patterns that are known to be invalid and are associated with exploitation or discovery of vulnerabilities. This can help reduce the risk of a malicious actor discovering a vulnerable application." 421 | type = string 422 | default = "Count" 423 | validation { 424 | condition = contains(["Block", "Count"], var.known_bad_inputs_amr_actions) 425 | error_message = "Allowed values are [ Block | Count ]" 426 | } 427 | } 428 | 429 | variable "waf_log_s3_bucket_name" { 430 | type = string 431 | } 432 | 433 | variable "waf_delivery_role_arn" { 434 | type = string 435 | } 436 | 437 | variable "waf_log_kms_key_arn" { 438 | type = string 439 | } -------------------------------------------------------------------------------- /cloudformation/templates/shield-configure-subscribe.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: 2010-09-09 3 | Metadata: 4 | AWS::CloudFormation::Interface: 5 | ParameterGroups: 6 | - 7 | Label: 8 | default: "AWS Shield Advanced Subscription" 9 | Parameters: 10 | - AcknowledgeServiceTermsPricing 11 | - AcknowledgeServiceTermsDTO 12 | - AcknowledgeServiceTermsCommitment 13 | - AcknowledgeServiceTermsAutoRenew 14 | - AcknowledgeNoUnsubscribe 15 | - 16 | Label: 17 | default: "AWS Shield Advanced Proactive Engagement" 18 | Parameters: 19 | - EnabledProactiveEngagement 20 | - EmergencyContactEmail1 21 | - EmergencyContactPhone1 22 | - EmergencyContactNote1 23 | - EmergencyContactEmail2 24 | - EmergencyContactPhone2 25 | - EmergencyContactNote2 26 | - EmergencyContactEmail3 27 | - EmergencyContactPhone3 28 | - EmergencyContactNote3 29 | - EmergencyContactEmail4 30 | - EmergencyContactPhone4 31 | - EmergencyContactNote4 32 | - EmergencyContactEmail5 33 | - EmergencyContactPhone5 34 | - EmergencyContactNote5 35 | - 36 | Label: 37 | default: "AWS Shield Advanced SRT Access" 38 | Parameters: 39 | - EnableSRTAccess 40 | - SRTAccessRoleName 41 | - SRTAccessRoleAction 42 | - SRTBuckets 43 | 44 | Parameters: 45 | AcknowledgeServiceTermsPricing: 46 | Type: String 47 | Default: "False" 48 | Description: Shield Advanced Service Term | Pricing | $3000 / month subscription fee for a consolidated billing family 49 | AllowedValues: 50 | - "True" 51 | - "False" 52 | AcknowledgeServiceTermsDTO: 53 | Type: String 54 | Default: "False" 55 | Description: Shield Advanced Service Term | Pricing | Data transfer out usage fees for all protected resources. 56 | AllowedValues: 57 | - "True" 58 | - "False" 59 | AcknowledgeServiceTermsCommitment: 60 | Type: String 61 | Default: "False" 62 | Description: Shield Advanced Term | Commitment | I am committing to a 12 month subscription. 63 | AllowedValues: 64 | - "True" 65 | - "False" 66 | AcknowledgeServiceTermsAutoRenew: 67 | Type: String 68 | Default: "False" 69 | Description: Shield Advanced Term | Auto renewal | Subscription will be auto-renewed after 12 months. However, I can opt out of renewal 30 days prior to the renewal date. 70 | AllowedValues: 71 | - "True" 72 | - "False" 73 | AcknowledgeNoUnsubscribe: 74 | Type: String 75 | Default: "False" 76 | Description: Shield Advanced does not un-subscribe on delete | Shield Advanced does not un-subscribe if you delete this stack/this resource. 77 | AllowedValues: 78 | - "True" 79 | - "False" 80 | EmergencyContactEmail1: 81 | Type: String 82 | Default: 83 | AllowedPattern: ^\S+@\S+\.\S+$|\ 84 | EmergencyContactEmail2: 85 | Type: String 86 | Default: 87 | AllowedPattern: ^\S+@\S+\.\S+$|\ 88 | EmergencyContactEmail3: 89 | Type: String 90 | Default: 91 | AllowedPattern: ^\S+@\S+\.\S+$|\ 92 | EmergencyContactEmail4: 93 | Type: String 94 | Default: 95 | AllowedPattern: ^\S+@\S+\.\S+$|\ 96 | EmergencyContactEmail5: 97 | Type: String 98 | Default: 99 | AllowedPattern: ^\S+@\S+\.\S+$|\ 100 | EmergencyContactPhone1: 101 | Type: String 102 | Default: 103 | AllowedPattern: ^\+[0-9]{11}|\ 104 | EmergencyContactPhone2: 105 | Type: String 106 | Default: 107 | AllowedPattern: ^\+[0-9]{11}|\ 108 | EmergencyContactPhone3: 109 | Type: String 110 | Default: 111 | AllowedPattern: ^\+[0-9]{11}|\ 112 | EmergencyContactPhone4: 113 | Type: String 114 | Default: 115 | AllowedPattern: ^\+[0-9]{11}|\ 116 | EmergencyContactPhone5: 117 | Type: String 118 | Default: 119 | AllowedPattern: ^\+[0-9]{11}|\ 120 | EmergencyContactNote1: 121 | Type: String 122 | Default: 123 | AllowedPattern: ^[\w\s\.\-,:/()+@]*$|\ 124 | EmergencyContactNote2: 125 | Type: String 126 | Default: 127 | AllowedPattern: ^[\w\s\.\-,:/()+@]*$|\ 128 | EmergencyContactNote3: 129 | Type: String 130 | Default: 131 | AllowedPattern: ^[\w\s\.\-,:/()+@]*$|\ 132 | EmergencyContactNote4: 133 | Type: String 134 | Default: 135 | AllowedPattern: ^[\w\s\.\-,:/()+@]*$|\ 136 | EmergencyContactNote5: 137 | Type: String 138 | Default: 139 | AllowedPattern: ^[\w\s\.\-,:/()+@]*$|\ 140 | EnabledProactiveEngagement: 141 | Type: String 142 | Default: "False" 143 | Description: Enable Proactive Engagement. Note you must also configure emergency contact(s) and health checks for protected resources for this feature to be effective 144 | AllowedValues: 145 | - "True" 146 | - "False" 147 | SRTBuckets: 148 | Type: CommaDelimitedList 149 | Default: 150 | SRTAccessRoleName: 151 | Type: String 152 | Default: 153 | SRTAccessRoleAction: 154 | Type: String 155 | Default: "CreateRole" 156 | AllowedValues: 157 | - "UseExisting" 158 | - "CreateRole" 159 | EnableSRTAccess: 160 | Type: String 161 | Default: "False" 162 | AllowedValues: 163 | - "True" 164 | - "False" 165 | Conditions: 166 | FirstEmergencyContact: !Not [ !Equals [!Ref EmergencyContactEmail1 , ""]] 167 | SecondEmergencyContact: !Not [ !Equals [!Ref EmergencyContactEmail2 , ""]] 168 | ThirdEmergencyContact: !Not [ !Equals [!Ref EmergencyContactEmail3 , ""]] 169 | FourthEmergencyContact: !Not [ !Equals [!Ref EmergencyContactEmail4 , ""]] 170 | FifthEmergencyContact: !Not [ !Equals [!Ref EmergencyContactEmail5 , ""]] 171 | FirstEmergencyContactNote: !Not [ !Equals [!Ref EmergencyContactNote1 , ""]] 172 | SecondEmergencyContactNote: !Not [ !Equals [!Ref EmergencyContactNote2 , ""]] 173 | ThirdEmergencyContactNote: !Not [ !Equals [!Ref EmergencyContactNote3 , ""]] 174 | FourthEmergencyContactNote: !Not [ !Equals [!Ref EmergencyContactNote4 , ""]] 175 | FifthEmergencyContactNote: !Not [ !Equals [!Ref EmergencyContactNote5 , ""]] 176 | EnableSRTAccessCondition: !Equals [!Ref "EnableSRTAccess", "True"] 177 | SRTCreateRoleCondition: !Equals [!Ref "SRTAccessRoleAction", "CreateRole"] 178 | SRTBucketsCondition: !Not [!Equals [ !Join [",",!Ref "SRTBuckets"], "" ] ] 179 | ProactiveEngagementCondition: !Equals [ !Ref "EnabledProactiveEngagement", "True"] 180 | GenerateSRTRoleNameFlag: !Equals [!Ref SRTAccessRoleName, "" ] 181 | AcceptedShieldTerms: !And 182 | - Fn::Equals: 183 | - "True" 184 | - !Ref AcknowledgeServiceTermsPricing 185 | - Fn::Equals: 186 | - "True" 187 | - !Ref AcknowledgeServiceTermsDTO 188 | - Fn::Equals: 189 | - "True" 190 | - !Ref AcknowledgeServiceTermsCommitment 191 | - Fn::Equals: 192 | - "True" 193 | - !Ref AcknowledgeServiceTermsAutoRenew 194 | - Fn::Equals: 195 | - "True" 196 | - !Ref AcknowledgeNoUnsubscribe 197 | 198 | 199 | Resources: 200 | ConfigureShieldLambdaRole: 201 | Type: AWS::IAM::Role 202 | Properties: 203 | AssumeRolePolicyDocument: 204 | Version: 2012-10-17 205 | Statement: 206 | - Effect: Allow 207 | Principal: 208 | Service: 209 | - lambda.amazonaws.com 210 | Action: 211 | - 'sts:AssumeRole' 212 | Path: / 213 | ConfigureShieldLambdaPolicy: 214 | Type: 'AWS::IAM::Policy' 215 | Metadata: 216 | cfn_nag: 217 | rules_to_suppress: 218 | - id: W12 219 | reason: "Wildcard IAM policy required, APIs do not support resource scoping" 220 | - id: W58 221 | reason: "CFN Nag checks for managed policy, permissions granted inline" 222 | Properties: 223 | PolicyName: ConfigureShieldLambdaPolicy 224 | PolicyDocument: 225 | Version: 2012-10-17 226 | Statement: 227 | - Effect: Allow 228 | Action: 229 | - "logs:CreateLogGroup" 230 | - "logs:CreateLogStream" 231 | - "logs:PutLogEvents" 232 | Resource: "arn:aws:logs:*:*:*" 233 | - Sid: ShieldSubscription 234 | Effect: Allow 235 | Action: 236 | - shield:CreateSubscription 237 | - shield:UpdateSubscription 238 | - "xray:PutTraceSegments" 239 | - "xray:PutTelemetryRecords" 240 | Resource: "*" 241 | Roles: 242 | - !Ref ConfigureShieldLambdaRole 243 | SubscribeShieldAdvanced: 244 | Condition: AcceptedShieldTerms 245 | DependsOn: ConfigureShieldLambdaPolicy 246 | Type: Custom::SubscribeShieldAdvanced 247 | Properties: 248 | ServiceToken: !GetAtt ConfigureShieldLambda.Arn 249 | ConfigureShieldLambda: 250 | Type: AWS::Lambda::Function 251 | Metadata: 252 | cfn_nag: 253 | rules_to_suppress: 254 | - id: W58 255 | reason: "Permissions granted, CFN_Nag not parsing correctly?" 256 | - id: W89 257 | reason: "Not applicable for use case" 258 | - id: W92 259 | reason: "Not applicable for use case" 260 | Properties: 261 | TracingConfig: 262 | Mode: Active 263 | Runtime: python3.12 264 | Timeout: 15 265 | Role: !GetAtt ConfigureShieldLambdaRole.Arn 266 | Handler: index.lambda_handler 267 | Code: 268 | ZipFile: | 269 | import boto3 270 | import botocore 271 | try: 272 | import cfnresponse 273 | except: 274 | print ("no cfnresponse module") 275 | import logging 276 | 277 | logger = logging.getLogger('hc') 278 | logger.setLevel('DEBUG') 279 | 280 | shield_client = boto3.client('shield') 281 | 282 | def lambda_handler(event, context): 283 | logger.debug(event) 284 | responseData = {} 285 | if "RequestType" in event: 286 | if event['RequestType'] in ['Create','Update']: 287 | try: 288 | logger.debug("Start Create Subscription") 289 | shield_client.create_subscription() 290 | logger.info ("Shield Enabled!") 291 | responseData['Message'] = "Subscription Created" 292 | cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "SubscriptionCreated") 293 | return () 294 | except botocore.exceptions.ClientError as error: 295 | if error.response['Error']['Code'] == 'ResourceAlreadyExistsException': 296 | logger.info ("Subscription already active") 297 | responseData['Message'] = "Already Subscribed to Shield Advanced" 298 | cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "AlreadySubscribedOk") 299 | return 300 | else: 301 | logger.error(error.response['Error']) 302 | responseData['Message'] = error.response['Error'] 303 | cfnresponse.send(event, context, cfnresponse.FAILED, responseData, "SubscribeFailed") 304 | return () 305 | else: 306 | responseData['Message'] = "CFN Delete, no action taken" 307 | cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "CFNDeleteGracefulContinue") 308 | return() 309 | ProactiveEngagement: 310 | Metadata: 311 | cfn-lint: 312 | config: 313 | ignore_checks: 314 | - E3005 315 | - E3001 316 | DeletionPolicy: Delete 317 | DependsOn: SubscribeShieldAdvanced 318 | Condition: ProactiveEngagementCondition 319 | Type: AWS::Shield::ProactiveEngagement 320 | Properties: 321 | ProactiveEngagementStatus: ENABLED 322 | EmergencyContactList: 323 | !If 324 | - FirstEmergencyContact 325 | - 326 | - !If 327 | - FirstEmergencyContact 328 | - EmailAddress: !Ref "EmergencyContactEmail1" 329 | ContactNotes: !If [FirstEmergencyContactNote, !Ref "EmergencyContactNote1", !Ref "AWS::NoValue"] 330 | PhoneNumber: !Ref "EmergencyContactPhone1" 331 | - !Ref "AWS::NoValue" 332 | - !If 333 | - SecondEmergencyContact 334 | - EmailAddress: !Ref "EmergencyContactEmail2" 335 | ContactNotes: !If [SecondEmergencyContactNote, !Ref "EmergencyContactNote2", !Ref "AWS::NoValue"] 336 | PhoneNumber: !Ref "EmergencyContactPhone2" 337 | - !Ref "AWS::NoValue" 338 | - !If 339 | - ThirdEmergencyContact 340 | - EmailAddress: !Ref "EmergencyContactEmail3" 341 | ContactNotes: !If [ThirdEmergencyContactNote, !Ref "EmergencyContactNote3", !Ref "AWS::NoValue"] 342 | PhoneNumber: !Ref "EmergencyContactPhone3" 343 | - !Ref "AWS::NoValue" 344 | - !If 345 | - FourthEmergencyContact 346 | - EmailAddress: !Ref "EmergencyContactEmail4" 347 | ContactNotes: !If [FourthEmergencyContactNote, !Ref "EmergencyContactNote4", !Ref "AWS::NoValue"] 348 | PhoneNumber: !Ref "EmergencyContactPhone4" 349 | - !Ref "AWS::NoValue" 350 | - !If 351 | - FifthEmergencyContact 352 | - EmailAddress: !Ref "EmergencyContactEmail5" 353 | ContactNotes: !If [FifthEmergencyContactNote, !Ref "EmergencyContactNote5", !Ref "AWS::NoValue"] 354 | PhoneNumber: !Ref "EmergencyContactPhone5" 355 | - !Ref "AWS::NoValue" 356 | - !Ref "AWS::NoValue" 357 | SRTAccess: 358 | Metadata: 359 | cfn-lint: 360 | config: 361 | ignore_checks: 362 | - E3001 363 | - E3005 364 | - W1001 365 | Condition: EnableSRTAccessCondition 366 | DependsOn: SubscribeShieldAdvanced 367 | Type: AWS::Shield::DRTAccess 368 | Properties: 369 | LogBucketList: 370 | !If 371 | - SRTBucketsCondition 372 | - !Ref "SRTBuckets" 373 | - !Ref "AWS::NoValue" 374 | RoleArn: !If [ GenerateSRTRoleNameFlag, !GetAtt "SRTAccessRole.Arn", !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${SRTAccessRoleName}" ] 375 | SRTAccessRole: 376 | Metadata: 377 | cfn_nag: 378 | rules_to_suppress: 379 | - id: W28 380 | reason: End user can elect to name role, default will allow CFN to set name 381 | Condition: SRTCreateRoleCondition 382 | Type: AWS::IAM::Role 383 | Properties: 384 | RoleName: !If [GenerateSRTRoleNameFlag, !Ref "AWS::NoValue", !Ref "SRTAccessRoleName"] 385 | ManagedPolicyArns: 386 | - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSShieldDRTAccessPolicy' 387 | AssumeRolePolicyDocument: 388 | Version: "2012-10-17" 389 | Statement: 390 | - Effect: Allow 391 | Principal: 392 | Service: 393 | - 'drt.shield.amazonaws.com' 394 | Action: 395 | - 'sts:AssumeRole' 396 | -------------------------------------------------------------------------------- /terraform/organizational-deployment/modules/aws-shield-advanced-wafv2/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | } 6 | } 7 | 8 | } 9 | 10 | data "aws_caller_identity" "current" {} 11 | 12 | data "aws_region" "current" {} 13 | 14 | locals { 15 | workspace_name = terraform.workspace 16 | aws_region = data.aws_region.current.name 17 | aws_account = data.aws_caller_identity.current.account_id 18 | ScopeTagName1Flag = var.scope_tag_name1 != "" ? true : false 19 | ScopeTagName2Flag = var.scope_tag_name2 != "" ? true : false 20 | ScopeTagName3Flag = var.scope_tag_name3 != "" ? true : false 21 | ScopeTagValue1Flag = var.scope_tag_value1 != "" ? true : false 22 | ScopeTagValue2Flag = var.scope_tag_value2 != "" ? true : false 23 | ScopeTagValue3Flag = var.scope_tag_value3 != "" ? true : false 24 | AutoRemediateForceFlag = var.wafv2_auto_remediate == "Yes | Replace existing existing WebACL" ? true : false 25 | AutoRemediateFlag = var.wafv2_auto_remediate == "Yes | If no current WebACL" || var.wafv2_auto_remediate == "Yes | Replace existing existing WebACL" ? true : false 26 | OUScopeFlag = var.scope_type == "OU" ? true : false 27 | AccountScopeFlag = var.scope_type == "Accounts" ? true : false 28 | ExcludeResourceTagFlag = var.resource_tag_usage == "Exclude" ? true : false 29 | IncludeScopeFlag = var.include_exclude_scope == "Include" ? true : false 30 | ExcludeScopeFlag = var.include_exclude_scope == "Exclude" ? true : false 31 | CreateRegionalPolicyFlag = var.wafv2_protect_regional_resource_types != "" ? true : false 32 | CreateCloudFrontPolicyFlag = var.wafv2_protect_cloudfront != "" && data.aws_region.current.name == "us-east-1" ? true : false 33 | RateLimitActionFlag = var.rate_limit_action == "Block" ? true : false 34 | AnonymousIPAMRActionFlag = var.anonymous_ip_amr_action == "Block" ? true : false 35 | IPReputationAMRActionFlag = var.ip_reputation_amr_action == "Block" ? true : false 36 | CoreRuleSetAMRActionFlag = var.core_rule_set_amr_action == "Block" ? true : false 37 | KnownBadInputsAMRActionFlag = var.known_bad_inputs_amr_actions == "Block" ? true : false 38 | } 39 | 40 | locals { 41 | IPReputationActionValue = local.IPReputationAMRActionFlag == true ? "NONE" : "COUNT" 42 | AnonymousIPActionValue = local.AnonymousIPAMRActionFlag == true ? "NONE" : "COUNT" 43 | KnownBadInputsActionValue = local.KnownBadInputsAMRActionFlag == true ? "NONE" : "COUNT" 44 | CoreRuleSetActionValue = local.CoreRuleSetAMRActionFlag == true ? "NONE" : "COUNT" 45 | AutoRemediateForceValue = local.AutoRemediateForceFlag == true ? true : false 46 | 47 | } 48 | 49 | resource "aws_fms_policy" "regional_wafv2_security_policy" { 50 | count = local.CreateRegionalPolicyFlag ? 1 : 0 51 | 52 | name = "OneClickShieldRegional-${local.workspace_name}" 53 | resource_type_list = split(",", var.wafv2_protect_regional_resource_types) 54 | exclude_resource_tags = local.ExcludeResourceTagFlag ? true : false 55 | include_map { 56 | orgunit = local.IncludeScopeFlag ? (local.OUScopeFlag ? var.scope_details : null) : null 57 | account = local.IncludeScopeFlag ? (local.AccountScopeFlag ? var.scope_details : null) : null 58 | } 59 | exclude_map { 60 | orgunit = local.ExcludeScopeFlag ? (local.OUScopeFlag ? var.scope_details : null) : null 61 | account = local.ExcludeScopeFlag ? (local.AccountScopeFlag ? var.scope_details : null) : null 62 | } 63 | remediation_enabled = local.AutoRemediateFlag ? true : false 64 | delete_all_policy_resources = false 65 | resource_tags = local.ScopeTagName1Flag ? tomap([ 66 | local.ScopeTagName1Flag ? { 67 | "Key" = var.scope_tag_name1, 68 | "Value" = local.ScopeTagName1Flag ? var.scope_tag_value1 : "" 69 | } : null, 70 | local.ScopeTagName2Flag ? { 71 | "Key" = var.scope_tag_name2 72 | "Value" = local.ScopeTagName2Flag ? var.scope_tag_value2 : "" 73 | } : null, 74 | local.ScopeTagName3Flag ? { 75 | Key = var.scope_tag_name3 76 | Value = local.ScopeTagName3Flag ? var.scope_tag_value3 : "" 77 | } : null]) : null 78 | 79 | security_service_policy_data { 80 | type = "WAFV2" 81 | 82 | managed_service_data = jsonencode({ 83 | type = "WAFV2", 84 | preProcessRuleGroups = [ 85 | { 86 | ruleGroupArn = "${aws_wafv2_rule_group.regional_rate_limit_rule_group[0].arn}", 87 | overrideAction = { 88 | type = "NONE" 89 | }, 90 | ruleGroupType = "RuleGroup", 91 | excludeRules = [], 92 | sampledRequestsEnabled = true 93 | }, 94 | { 95 | managedRuleGroupIdentifier = { 96 | vendorName = "AWS", 97 | managedRuleGroupName = "AWSManagedRulesAmazonIpReputationList" 98 | }, 99 | overrideAction = { 100 | type = "${local.IPReputationActionValue}" 101 | }, 102 | ruleGroupType = "ManagedRuleGroup", 103 | excludeRules = [], 104 | sampledRequestsEnabled = true 105 | }, 106 | { 107 | managedRuleGroupIdentifier = { 108 | vendorName = "AWS", 109 | managedRuleGroupName = "AWSManagedRulesAnonymousIpList" 110 | }, 111 | overrideAction = { 112 | type = "${local.AnonymousIPActionValue}" 113 | }, 114 | ruleGroupType = "ManagedRuleGroup", 115 | excludeRules = [], 116 | sampledRequestsEnabled = true 117 | }, 118 | { 119 | managedRuleGroupIdentifier = { 120 | vendorName = "AWS", 121 | managedRuleGroupName = "AWSManagedRulesKnownBadInputsRuleSet" 122 | }, 123 | overrideAction = { 124 | type = "${local.KnownBadInputsActionValue}" 125 | }, 126 | ruleGroupType = "ManagedRuleGroup", 127 | excludeRules = [], 128 | sampledRequestsEnabled = true 129 | }, 130 | { 131 | managedRuleGroupIdentifier = { 132 | vendorName = "AWS", 133 | managedRuleGroupName = "AWSManagedRulesCommonRuleSet" 134 | }, 135 | overrideAction = { 136 | type = "${local.CoreRuleSetActionValue}" 137 | }, 138 | ruleGroupType = "ManagedRuleGroup", 139 | excludeRules = [], 140 | sampledRequestsEnabled = true 141 | } 142 | ], 143 | postProcessRuleGroups = [], 144 | defaultAction = { 145 | type = "ALLOW" 146 | }, 147 | overrideCustomerWebACLAssociation = "${local.AutoRemediateForceValue}", 148 | sampledRequestsEnabledForDefaultActions = true, 149 | loggingConfiguration = { 150 | redactedFields = [], 151 | logDestinationConfigs = [ 152 | "${aws_kinesis_firehose_delivery_stream.waf_delivery_stream.arn}" 153 | ] 154 | } 155 | }) 156 | } 157 | } 158 | 159 | resource "aws_fms_policy" "cloudfront_wafv2_security_policy" { 160 | count = local.CreateCloudFrontPolicyFlag ? 1 : 0 161 | 162 | name = "OneClickShieldGlobal-${local.workspace_name}" 163 | resource_type = "AWS::CloudFront::Distribution" 164 | exclude_resource_tags = local.ExcludeResourceTagFlag ? true : false 165 | include_map { 166 | account = local.IncludeScopeFlag ? (local.OUScopeFlag ? var.scope_details : null) : null 167 | orgunit = local.IncludeScopeFlag ? (local.AccountScopeFlag ? var.scope_details : null) : null 168 | } 169 | exclude_map { 170 | account = local.ExcludeScopeFlag ? (local.OUScopeFlag ? var.scope_details : null) : null 171 | orgunit = local.ExcludeScopeFlag ? (local.AccountScopeFlag ? var.scope_details : null) : null 172 | } 173 | remediation_enabled = local.AutoRemediateFlag ? true : false 174 | delete_all_policy_resources = false 175 | resource_tags = local.ScopeTagName1Flag ? tomap([ 176 | local.ScopeTagName1Flag ? { 177 | Key = var.scope_tag_name1, 178 | Value = local.ScopeTagName1Flag ? var.scope_tag_value1 : "" 179 | } : null, 180 | local.ScopeTagName2Flag ? { 181 | Key = var.scope_tag_name2 182 | Value = local.ScopeTagName2Flag ? var.scope_tag_value2 : "" 183 | } : null, 184 | local.ScopeTagName3Flag ? { 185 | Key = var.scope_tag_name3 186 | Value = local.ScopeTagName3Flag ? var.scope_tag_value3 : "" 187 | } : null]) : null 188 | 189 | security_service_policy_data { 190 | type = "WAFV2" 191 | 192 | managed_service_data = jsonencode({ 193 | type = "WAFV2", 194 | preProcessRuleGroups = [ 195 | { 196 | ruleGroupArn = "${aws_wafv2_rule_group.cloudfront_rate_limit_rule_group[0].arn}", 197 | overrideAction = { 198 | type = "NONE" 199 | }, 200 | ruleGroupType = "RuleGroup", 201 | excludeRules = [], 202 | sampledRequestsEnabled = true 203 | }, 204 | { 205 | managedRuleGroupIdentifier = { 206 | vendorName = "AWS", 207 | managedRuleGroupName = "AWSManagedRulesAmazonIpReputationList" 208 | }, 209 | overrideAction = { 210 | type = "${local.IPReputationActionValue}" 211 | }, 212 | ruleGroupType = "ManagedRuleGroup", 213 | excludeRules = [], 214 | sampledRequestsEnabled = true 215 | }, 216 | { 217 | managedRuleGroupIdentifier = { 218 | vendorName = "AWS", 219 | managedRuleGroupName = "AWSManagedRulesAnonymousIpList" 220 | }, 221 | overrideAction = { 222 | type = "${local.AnonymousIPActionValue}" 223 | }, 224 | ruleGroupType = "ManagedRuleGroup", 225 | excludeRules = [], 226 | sampledRequestsEnabled = true 227 | }, 228 | { 229 | managedRuleGroupIdentifier = { 230 | vendorName = "AWS", 231 | managedRuleGroupName = "AWSManagedRulesKnownBadInputsRuleSet" 232 | }, 233 | overrideAction = { 234 | type = "${local.KnownBadInputsActionValue}" 235 | }, 236 | ruleGroupType = "ManagedRuleGroup", 237 | excludeRules = [], 238 | sampledRequestsEnabled = true 239 | }, 240 | { 241 | managedRuleGroupIdentifier = { 242 | vendorName = "AWS", 243 | managedRuleGroupName = "AWSManagedRulesCommonRuleSet" 244 | }, 245 | overrideAction = { 246 | type = "${local.CoreRuleSetActionValue}" 247 | }, 248 | ruleGroupType = "ManagedRuleGroup", 249 | excludeRules = [], 250 | sampledRequestsEnabled = true 251 | } 252 | ], 253 | postProcessRuleGroups = [], 254 | defaultAction = { 255 | type = "ALLOW" 256 | }, 257 | overrideCustomerWebACLAssociation = "${local.AutoRemediateForceValue}", 258 | sampledRequestsEnabledForDefaultActions = true, 259 | loggingConfiguration = { 260 | redactedFields = [], 261 | logDestinationConfigs = [ 262 | "${aws_kinesis_firehose_delivery_stream.waf_delivery_stream.arn}" 263 | ] 264 | } 265 | }) 266 | } 267 | } 268 | 269 | resource "aws_wafv2_rule_group" "regional_rate_limit_rule_group" { 270 | count = local.CreateRegionalPolicyFlag ? 1 : 0 271 | 272 | name = "OneClickDeployRuleGroup-${local.workspace_name}" 273 | description = "OneClickDeployRuleGroup-${local.workspace_name}" 274 | scope = "REGIONAL" 275 | capacity = 10 276 | rule { 277 | name = "RateLimit" 278 | priority = 0 279 | dynamic "action" { 280 | for_each = local.RateLimitActionFlag == true ? [1] : [] 281 | content { 282 | block {} 283 | } 284 | } 285 | dynamic "action" { 286 | for_each = local.RateLimitActionFlag != true ? [1] : [] 287 | content { 288 | count {} 289 | } 290 | } 291 | statement { 292 | rate_based_statement { 293 | limit = var.rate_limit_value 294 | aggregate_key_type = "IP" 295 | } 296 | } 297 | visibility_config { 298 | cloudwatch_metrics_enabled = true 299 | metric_name = "RateLimit" 300 | sampled_requests_enabled = true 301 | } 302 | } 303 | visibility_config { 304 | cloudwatch_metrics_enabled = true 305 | metric_name = "OneClickDeployRuleGroup-${local.workspace_name}" 306 | sampled_requests_enabled = true 307 | } 308 | } 309 | 310 | resource "aws_wafv2_rule_group" "cloudfront_rate_limit_rule_group" { 311 | count = local.CreateCloudFrontPolicyFlag ? 1 : 0 312 | 313 | name = "OneClickDeployRuleGroup-${local.workspace_name}" 314 | description = "OneClickDeployRuleGroup-${local.workspace_name}" 315 | scope = "CLOUDFRONT" 316 | capacity = 10 317 | rule { 318 | name = "RateLimit" 319 | priority = 0 320 | dynamic "action" { 321 | for_each = local.RateLimitActionFlag == true ? [1] : [] 322 | content { 323 | block {} 324 | } 325 | } 326 | dynamic "action" { 327 | for_each = local.RateLimitActionFlag != true ? [1] : [] 328 | content { 329 | count {} 330 | } 331 | } 332 | statement { 333 | rate_based_statement { 334 | limit = var.rate_limit_value 335 | aggregate_key_type = "IP" 336 | } 337 | } 338 | visibility_config { 339 | cloudwatch_metrics_enabled = true 340 | metric_name = "RateLimit" 341 | sampled_requests_enabled = true 342 | } 343 | } 344 | visibility_config { 345 | cloudwatch_metrics_enabled = true 346 | metric_name = "OneClickDeployRuleGroup-${local.workspace_name}" 347 | sampled_requests_enabled = true 348 | } 349 | } 350 | 351 | resource "aws_cloudwatch_log_group" "waf_delivery_log_group" { 352 | name = "/aws/kinesisfirehose/${local.workspace_name}" 353 | kms_key_id = aws_kms_key.kms_key.arn 354 | retention_in_days = 365 355 | 356 | depends_on = [aws_kms_key.kms_key] 357 | } 358 | 359 | resource "aws_cloudwatch_log_stream" "waf_delivery_log_stream" { 360 | name = "aws-waf-logs-${local.workspace_name}-${local.aws_region}" 361 | log_group_name = "/aws/kinesisfirehose/${local.workspace_name}" 362 | } 363 | 364 | resource "aws_kinesis_firehose_delivery_stream" "waf_delivery_stream" { 365 | name = "aws-waf-logs-${local.workspace_name}-${local.aws_region}" 366 | destination = "extended_s3" 367 | 368 | server_side_encryption { 369 | enabled = true 370 | key_type = "CUSTOMER_MANAGED_CMK" 371 | key_arn = var.waf_log_kms_key_arn 372 | } 373 | 374 | extended_s3_configuration { 375 | role_arn = var.waf_delivery_role_arn 376 | bucket_arn = "arn:aws:s3:::${var.waf_log_s3_bucket_name}" 377 | prefix = "firehose/${local.aws_region}/}" 378 | 379 | cloudwatch_logging_options { 380 | enabled = true 381 | log_group_name = "/aws/kinesisfirehose/${local.workspace_name}" 382 | log_stream_name = "aws-waf-logs-${local.workspace_name}-${local.aws_region}" 383 | 384 | } 385 | 386 | buffering_size = 50 387 | buffering_interval = 60 388 | compression_format = "UNCOMPRESSED" 389 | kms_key_arn = var.waf_log_kms_key_arn 390 | } 391 | } 392 | 393 | resource "aws_kms_key" "kms_key" { 394 | description = "CloudWatch Log Encryption Key" 395 | enable_key_rotation = true 396 | deletion_window_in_days = 20 397 | } 398 | 399 | resource "aws_kms_key_policy" "kms_key" { 400 | key_id = aws_kms_key.kms_key.id 401 | policy = jsonencode({ 402 | Id = "key-default-1" 403 | Statement = [ 404 | { 405 | Action = "kms:*" 406 | Effect = "Allow" 407 | Principal = { 408 | AWS = "arn:aws:iam::${local.aws_account}:root" 409 | } 410 | Resource = "*" 411 | Sid = "Enable IAM User Permissions" 412 | }, 413 | { 414 | Action = ["kms:Encrypt*", "kms:Decrypt*", "kms:ReEncrypt*", "kms:GenerateDataKey*", "kms:Describe*"] 415 | Effect = "Allow" 416 | Principal = { 417 | Service = "logs.${local.aws_region}.amazonaws.com" 418 | } 419 | Condition = { 420 | ArnEquals = { 421 | "kms:EncryptionContext:aws:logs:arn" : "arn:aws:logs:${local.aws_region}:${local.aws_account}:log-group:/aws/kinesisfirehose/${local.workspace_name}" 422 | } 423 | } 424 | Resource = "*" 425 | Sid = "Allow administration of the key" 426 | } 427 | ] 428 | Version = "2012-10-17" 429 | }) 430 | } -------------------------------------------------------------------------------- /cloudformation/references/fms-wafv2-example1.yaml: -------------------------------------------------------------------------------- 1 | #This example adds BotControl as a post process rule. It is also configured with several rules with an override action of COUNT 2 | --- 3 | AWSTemplateFormatVersion: 2010-09-09 4 | Parameters: 5 | TopStackName: 6 | Type: String 7 | ScopeDetails: 8 | Type: String 9 | Default: 10 | IncludeExcludeScope: 11 | Type: String 12 | Default: Include 13 | AllowedValues: 14 | - Include 15 | - Exclude 16 | ScopeType: 17 | Type: String 18 | Description: "Should Firewall Manager Policies be scoped to the entire org (root) or a specific list of OUs (OU)" 19 | Default: Org 20 | AllowedValues: 21 | - Org 22 | - OU 23 | - Accounts 24 | ResourceTagUsage: 25 | Type: String 26 | Default: Include 27 | Description: Include will scope to only include when ResourceTags match, Exclude will exclude when target resource tags match ResourceTags 28 | AllowedValues: 29 | - Include 30 | - Exclude 31 | WAFv2ProtectRegionalResourceTypes: 32 | Type: String 33 | Default: AWS::ApiGateway::Stage,AWS::ElasticLoadBalancingV2::LoadBalancer 34 | AllowedValues: 35 | - AWS::ApiGateway::Stage,AWS::ElasticLoadBalancingV2::LoadBalancer 36 | - AWS::ElasticLoadBalancingV2::LoadBalancer 37 | - AWS::ApiGateway::Stage 38 | - 39 | WAFv2ProtectCloudFront: 40 | Type: String 41 | Default: 42 | AllowedValues: 43 | - AWS::CloudFront::Distribution 44 | - 45 | WAFv2AutoRemediate: 46 | Type: String 47 | Description: "Should in scope AWS resource types that support AWS WAF automatically be remediated?" 48 | AllowedValues: 49 | - "Yes | Replace existing existing WebACL" 50 | - "Yes | If no current WebACL" 51 | - "No" 52 | ScopeTagName1: 53 | Type: String 54 | Default: 55 | ScopeTagName2: 56 | Type: String 57 | Default: 58 | ScopeTagName3: 59 | Type: String 60 | Default: 61 | ScopeTagValue1: 62 | Type: String 63 | Default: 64 | ScopeTagValue2: 65 | Type: String 66 | Default: 67 | ScopeTagValue3: 68 | Type: String 69 | Default: 70 | RateLimitValue: 71 | Type: String 72 | Description: AWS WAF rate limits use the previous 5 minutes to determine if an IP has exceeded the defined limit. 73 | Default: 10000 74 | RateLimitAction: 75 | Type: String 76 | Default: Block 77 | AllowedValues: 78 | - Block 79 | - Count 80 | AnonymousIPAMRAction: 81 | Type: String 82 | Description: The Anonymous IP list rule group contains rules to block requests from services that permit the obfuscation of viewer identity. These include requests from VPNs, proxies, Tor nodes, and hosting providers. This rule group is useful if you want to filter out viewers that might be trying to hide their identity from your application. 83 | Default: Block 84 | AllowedValues: 85 | - Block 86 | - Count 87 | IPReputationAMRAction: 88 | Type: String 89 | Description: The Amazon IP reputation list rule group contains rules that are based on Amazon internal threat intelligence. This is useful if you would like to block IP addresses typically associated with bots or other threats. Blocking these IP addresses can help mitigate bots and reduce the risk of a malicious actor discovering a vulnerable application. 90 | Default: Block 91 | AllowedValues: 92 | - Block 93 | - Count 94 | CoreRuleSetAMRAction: 95 | Type: String 96 | Description: The Core rule set (CRS) rule group contains rules that are generally applicable to web applications. This provides protection against exploitation of a wide range of vulnerabilities, including some of the high risk and commonly occurring vulnerabilities described in OWASP publications such as OWASP Top 10 97 | Default: Count 98 | AllowedValues: 99 | - Block 100 | - Count 101 | KnownBadInputsAMRAction: 102 | Type: String 103 | Description: The Known bad inputs rule group contains rules to block request patterns that are known to be invalid and are associated with exploitation or discovery of vulnerabilities. This can help reduce the risk of a malicious actor discovering a vulnerable application. 104 | Default: Count 105 | AllowedValues: 106 | - Block 107 | - Count 108 | WAFLogS3BucketName: 109 | Type: String 110 | WAFDeliveryRoleArn: 111 | Type: String 112 | WAFLogKMSKeyArn: 113 | Type: String 114 | Conditions: 115 | ScopeTagName1Flag: !Not [!Equals [!Ref ScopeTagName1, ""]] 116 | ScopeTagName2Flag: !Not [!Equals [!Ref ScopeTagName2, ""]] 117 | ScopeTagName3Flag: !Not [!Equals [!Ref ScopeTagName3, ""]] 118 | ScopeTagValue1Flag: !Not [!Equals [!Ref ScopeTagValue1, ""]] 119 | ScopeTagValue2Flag: !Not [!Equals [!Ref ScopeTagValue2, ""]] 120 | ScopeTagValue3Flag: !Not [!Equals [!Ref ScopeTagValue3, ""]] 121 | AutoRemediateForceFlag: !Equals [!Ref WAFv2AutoRemediate, "Yes | Replace existing existing WebACL"] 122 | AutoRemediateFlag: !Or 123 | - !Equals [!Ref WAFv2AutoRemediate, "Yes | If no current WebACL"] 124 | - !Equals [!Ref WAFv2AutoRemediate, "Yes | Replace existing existing WebACL"] 125 | OUScopeFlag: !Equals [!Ref ScopeType, "OU"] 126 | AccountScopeFlag: !Equals [!Ref ScopeType, "Accounts"] 127 | ExcludeResourceTagFlag: !Equals [!Ref ResourceTagUsage, "Exclude"] 128 | IncludeScopeFlag: !Equals [!Ref IncludeExcludeScope, "Include"] 129 | ExcludeScopeFlag: !Equals [!Ref IncludeExcludeScope, "Exclude"] 130 | CreateRegionalPolicyFlag: !Not [ !Equals [ !Ref WAFv2ProtectRegionalResourceTypes, '' ] ] 131 | CreateCloudFrontPolicyFlag: !And [ !Not [ !Equals [!Ref WAFv2ProtectCloudFront, "" ] ] , !Equals [ !Ref AWS::Region, 'us-east-1' ] ] 132 | RateLimitActionFlag: !Equals [!Ref RateLimitAction,'Block'] 133 | AnonymousIPAMRActionFlag: !Equals [!Ref AnonymousIPAMRAction,'Block'] 134 | IPReputationAMRActionFlag: !Equals [!Ref IPReputationAMRAction,'Block'] 135 | CoreRuleSetAMRActionFlag: !Equals [!Ref CoreRuleSetAMRAction,'Block'] 136 | KnownBadInputsAMRActionFlag: !Equals [!Ref KnownBadInputsAMRAction,'Block'] 137 | 138 | Resources: 139 | RegionalWAFv2SecurityPolicy: 140 | Condition: CreateRegionalPolicyFlag 141 | Type: AWS::FMS::Policy 142 | Properties: 143 | PolicyName: !Sub OneClickShieldRegional-${AWS::StackName} 144 | ResourceType: ResourceTypeList 145 | ResourceTypeList: !Split [",", !Ref WAFv2ProtectRegionalResourceTypes] 146 | ExcludeResourceTags: !If [ExcludeResourceTagFlag, True, False] 147 | IncludeMap: 148 | ORGUNIT: 149 | !If [IncludeScopeFlag, !If [OUScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 150 | ACCOUNT: 151 | !If [IncludeScopeFlag, !If [AccountScopeFlag, !Split [",", !Ref ScopeDetails] , !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 152 | ExcludeMap: 153 | ORGUNIT: 154 | !If [ExcludeScopeFlag, !If [OUScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 155 | ACCOUNT: 156 | !If [ExcludeScopeFlag, !If [AccountScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 157 | RemediationEnabled: !If [AutoRemediateFlag, True, False] 158 | DeleteAllPolicyResources: False 159 | ResourceTags: 160 | !If 161 | - ScopeTagName1Flag 162 | - 163 | - !If 164 | - ScopeTagName1Flag 165 | - Key: !Ref ScopeTagName1 166 | Value: !If [ScopeTagName1Flag, !Ref ScopeTagValue1, ""] 167 | - !Ref "AWS::NoValue" 168 | - !If 169 | - ScopeTagName2Flag 170 | - Key: !Ref ScopeTagName2 171 | Value: !If [ScopeTagName2Flag, !Ref ScopeTagValue2, ""] 172 | - !Ref "AWS::NoValue" 173 | - !If 174 | - ScopeTagName3Flag 175 | - Key: !Ref ScopeTagName3 176 | Value: !If [ScopeTagName3Flag, !Ref ScopeTagValue3, ""] 177 | - !Ref "AWS::NoValue" 178 | - !Ref "AWS::NoValue" 179 | SecurityServicePolicyData: 180 | Type: WAFV2 181 | ManagedServiceData: !Sub 182 | - '{"type": "WAFV2", "preProcessRuleGroups":[{"ruleGroupArn": "${RateLimitRGArn}", "overrideAction": {"type": "NONE"}, "ruleGroupType":"RuleGroup", "excludeRules": [], "sampledRequestsEnabled": true}, {"managedRuleGroupIdentifier": {"vendorName": "AWS", "managedRuleGroupName":"AWSManagedRulesAmazonIpReputationList"}, "overrideAction": {"type": "${IPReputationActionValue}"},"ruleGroupType": "ManagedRuleGroup", "excludeRules":[], "sampledRequestsEnabled": true}, {"managedRuleGroupIdentifier": {"vendorName": "AWS", "managedRuleGroupName": "AWSManagedRulesAnonymousIpList"},"overrideAction": {"type": "${AnonymousIPActionValue}"}, "ruleGroupType": "ManagedRuleGroup", "excludeRules": [], "sampledRequestsEnabled": true},{"managedRuleGroupIdentifier": {"vendorName": "AWS", "managedRuleGroupName": "AWSManagedRulesKnownBadInputsRuleSet"}, "overrideAction": {"type": "${KnownBadInputsActionValue}"}, "ruleGroupType": "ManagedRuleGroup", "excludeRules": [], "sampledRequestsEnabled": true},{"managedRuleGroupIdentifier": {"vendorName": "AWS", "managedRuleGroupName": "AWSManagedRulesCommonRuleSet"}, "overrideAction": {"type": "${CoreRuleSetActionValue}"}, "ruleGroupType": "ManagedRuleGroup", "excludeRules": [], "sampledRequestsEnabled": true}],"postProcessRuleGroups": [{"ruleGroupArn":null,"overrideAction":{"type":"NONE"},"managedRuleGroupIdentifier":{"versionEnabled":null,"version":null,"vendorName":"AWS","managedRuleGroupName":"AWSManagedRulesBotControlRuleSet","managedRuleGroupConfigs":[{"awsmanagedRulesBotControlRuleSet":{"inspectionLevel":"COMMON"}}]},"ruleGroupType":"ManagedRuleGroup","excludeRules":[],"sampledRequestsEnabled":true,"ruleActionOverrides":[{"name":"CategoryAdvertising","actionToUse":{"count":{}}},{"name":"CategoryLinkChecker","actionToUse":{"count":{}}},{"name":"CategorySearchEngine","actionToUse":{"count":{}}}]}], "defaultAction": {"type": "ALLOW"}, "overrideCustomerWebACLAssociation": ${AutoRemediateForceValue}, "sampledRequestsEnabledForDefaultActions": true,"loggingConfiguration": {"redactedFields": [],"logDestinationConfigs": ["${FirehoseArn}"]}}' 183 | - RateLimitRGArn: !GetAtt RegionalRateLimitRuleGroup.Arn 184 | IPReputationActionValue: !If [IPReputationAMRActionFlag, 'NONE','COUNT'] 185 | AnonymousIPActionValue: !If [AnonymousIPAMRActionFlag, 'NONE','COUNT'] 186 | KnownBadInputsActionValue: !If [KnownBadInputsAMRActionFlag, 'NONE','COUNT'] 187 | CoreRuleSetActionValue: !If [CoreRuleSetAMRActionFlag, 'NONE','COUNT'] 188 | FirehoseArn: !GetAtt "WAFDeliverystream.Arn" 189 | AutoRemediateForceValue: !If [AutoRemediateForceFlag,True,False] 190 | CloudFrontWAFv2SecurityPolicy: 191 | Condition: CreateCloudFrontPolicyFlag 192 | Type: AWS::FMS::Policy 193 | Properties: 194 | PolicyName: !Sub OneClickShieldGlobal-${AWS::StackName} 195 | ResourceType: AWS::CloudFront::Distribution 196 | ExcludeResourceTags: !If [ExcludeResourceTagFlag, True, False] 197 | IncludeMap: 198 | ORGUNIT: 199 | !If [IncludeScopeFlag, !If [OUScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 200 | ACCOUNT: 201 | !If [IncludeScopeFlag, !If [AccountScopeFlag, !Split [",", !Ref ScopeDetails] , !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 202 | ExcludeMap: 203 | ORGUNIT: 204 | !If [ExcludeScopeFlag, !If [OUScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 205 | ACCOUNT: 206 | !If [ExcludeScopeFlag, !If [AccountScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 207 | RemediationEnabled: !If [AutoRemediateFlag, true, false] 208 | DeleteAllPolicyResources: false 209 | ResourceTags: 210 | !If 211 | - ScopeTagName1Flag 212 | - 213 | - !If 214 | - ScopeTagName1Flag 215 | - Key: !Ref ScopeTagName1 216 | Value: !If [ScopeTagValue1Flag, !Ref ScopeTagValue1, ""] 217 | - !Ref "AWS::NoValue" 218 | - !If 219 | - ScopeTagName2Flag 220 | - Key: !Ref ScopeTagName2 221 | Value: !If [ScopeTagValue2Flag, !Ref ScopeTagValue2, ""] 222 | - !Ref "AWS::NoValue" 223 | - !If 224 | - ScopeTagName3Flag 225 | - Key: !Ref ScopeTagName3 226 | Value: !If [ScopeTagValue3Flag, !Ref ScopeTagValue3, ""] 227 | - !Ref "AWS::NoValue" 228 | - !Ref "AWS::NoValue" 229 | SecurityServicePolicyData: 230 | Type: WAFV2 231 | ManagedServiceData: !Sub 232 | - '{"type": "WAFV2", "preProcessRuleGroups":[{"ruleGroupArn": "${RateLimitRGArn}", "overrideAction": {"type": "NONE"}, "ruleGroupType":"RuleGroup", "excludeRules": [], "sampledRequestsEnabled": true}, {"managedRuleGroupIdentifier": {"vendorName": "AWS", "managedRuleGroupName":"AWSManagedRulesAmazonIpReputationList"}, "overrideAction": {"type": "${IPReputationActionValue}"},"ruleGroupType": "ManagedRuleGroup", "excludeRules":[], "sampledRequestsEnabled": true}, {"managedRuleGroupIdentifier": {"vendorName": "AWS", "managedRuleGroupName": "AWSManagedRulesAnonymousIpList"},"overrideAction": {"type": "${AnonymousIPActionValue}"}, "ruleGroupType": "ManagedRuleGroup", "excludeRules": [], "sampledRequestsEnabled": true},{"managedRuleGroupIdentifier": {"vendorName": "AWS", "managedRuleGroupName": "AWSManagedRulesKnownBadInputsRuleSet"}, "overrideAction": {"type": "${KnownBadInputsActionValue}"}, "ruleGroupType": "ManagedRuleGroup", "excludeRules": [], "sampledRequestsEnabled": true},{"managedRuleGroupIdentifier": {"vendorName": "AWS", "managedRuleGroupName": "AWSManagedRulesCommonRuleSet"}, "overrideAction": {"type": "${CoreRuleSetActionValue}"}, "ruleGroupType": "ManagedRuleGroup", "excludeRules": [], "sampledRequestsEnabled": true}],"postProcessRuleGroups": [{"ruleGroupArn":null,"overrideAction":{"type":"NONE"},"managedRuleGroupIdentifier":{"versionEnabled":null,"version":null,"vendorName":"AWS","managedRuleGroupName":"AWSManagedRulesBotControlRuleSet","managedRuleGroupConfigs":[{"awsmanagedRulesBotControlRuleSet":{"inspectionLevel":"COMMON"}}]},"ruleGroupType":"ManagedRuleGroup","excludeRules":[],"sampledRequestsEnabled":true,"ruleActionOverrides":[{"name":"CategoryAdvertising","actionToUse":{"count":{}}},{"name":"CategoryLinkChecker","actionToUse":{"count":{}}},{"name":"CategorySearchEngine","actionToUse":{"count":{}}}]}], "defaultAction": {"type": "ALLOW"}, "overrideCustomerWebACLAssociation": ${AutoRemediateForceValue}, "sampledRequestsEnabledForDefaultActions": true,"loggingConfiguration": {"redactedFields": [],"logDestinationConfigs": ["${FirehoseArn}"]}}' 233 | - RateLimitRGArn: !GetAtt CloudFrontRateLimitRuleGroup.Arn 234 | IPReputationActionValue: !If [IPReputationAMRActionFlag, 'NONE','COUNT'] 235 | AnonymousIPActionValue: !If [AnonymousIPAMRActionFlag, 'NONE','COUNT'] 236 | KnownBadInputsActionValue: !If [KnownBadInputsAMRActionFlag, 'NONE','COUNT'] 237 | CoreRuleSetActionValue: !If [CoreRuleSetAMRActionFlag, 'NONE','COUNT'] 238 | FirehoseArn: !GetAtt "WAFDeliverystream.Arn" 239 | AutoRemediateForceValue: !If [AutoRemediateForceFlag,True,False] 240 | RegionalRateLimitRuleGroup: 241 | Condition: CreateRegionalPolicyFlag 242 | Type: 'AWS::WAFv2::RuleGroup' 243 | Properties: 244 | Name: !Sub OneClickDeployRuleGroup-${AWS::StackName} 245 | Scope: REGIONAL 246 | Description: !Sub OneClickDeployRuleGroup-${AWS::StackName} 247 | VisibilityConfig: 248 | SampledRequestsEnabled: true 249 | CloudWatchMetricsEnabled: true 250 | MetricName: !Sub OneClickDeployRuleGroup-${AWS::StackName} 251 | Capacity: 10 252 | Rules: 253 | - Name: RateLimit 254 | Priority: 0 255 | Statement: 256 | RateBasedStatement: 257 | Limit: !Ref RateLimitValue 258 | AggregateKeyType: IP 259 | Action: 260 | Block: !If [RateLimitActionFlag, {},!Ref "AWS::NoValue"] 261 | Count: !If [RateLimitActionFlag, !Ref "AWS::NoValue", {}] 262 | VisibilityConfig: 263 | SampledRequestsEnabled: true 264 | CloudWatchMetricsEnabled: true 265 | MetricName: RateLimit 266 | CloudFrontRateLimitRuleGroup: 267 | Condition: CreateCloudFrontPolicyFlag 268 | Type: 'AWS::WAFv2::RuleGroup' 269 | Properties: 270 | Name: !Sub OneClickDeployRuleGroup-${AWS::StackName} 271 | Scope: CLOUDFRONT 272 | Description: !Sub OneClickDeployRuleGroup-${AWS::StackName} 273 | VisibilityConfig: 274 | SampledRequestsEnabled: true 275 | CloudWatchMetricsEnabled: true 276 | MetricName: !Sub OneClickDeployRuleGroup-${AWS::StackName} 277 | Capacity: 10 278 | Rules: 279 | - Name: RateLimit 280 | Priority: 0 281 | Statement: 282 | RateBasedStatement: 283 | Limit: !Ref RateLimitValue 284 | AggregateKeyType: IP 285 | Action: 286 | Block: !If [RateLimitActionFlag, {},!Ref "AWS::NoValue"] 287 | Count: !If [RateLimitActionFlag, !Ref "AWS::NoValue", {}] 288 | VisibilityConfig: 289 | SampledRequestsEnabled: true 290 | CloudWatchMetricsEnabled: true 291 | MetricName: RateLimit 292 | 293 | WAFDeliveryLogGroup: 294 | Type: AWS::Logs::LogGroup 295 | Properties: 296 | LogGroupName: !Sub "/aws/kinesisfirehose/${AWS::StackName}" 297 | KmsKeyId: !GetAtt KMSKey.Arn 298 | RetentionInDays: 365 299 | WAFDeliveryLogStream: 300 | DependsOn: WAFDeliveryLogGroup 301 | Type: AWS::Logs::LogStream 302 | Properties: 303 | LogGroupName: !Sub "/aws/kinesisfirehose/${AWS::StackName}" 304 | LogStreamName: !Sub "aws-waf-logs-${TopStackName}-${AWS::Region}" 305 | WAFDeliverystream: 306 | Type: AWS::KinesisFirehose::DeliveryStream 307 | Metadata: 308 | cfn_nag: 309 | rules_to_suppress: 310 | - id: W88 311 | reason: "There is no Delivery Stream, these are from FMS, not applicable" 312 | Properties: 313 | DeliveryStreamName: !Sub "aws-waf-logs-${TopStackName}-${AWS::Region}" 314 | DeliveryStreamEncryptionConfigurationInput: 315 | KeyARN: !GetAtt KMSKey.Arn 316 | KeyType: CUSTOMER_MANAGED_CMK 317 | ExtendedS3DestinationConfiguration: 318 | BucketARN: !Sub "arn:aws:s3:::${WAFLogS3BucketName}" 319 | CloudWatchLoggingOptions: 320 | Enabled: true 321 | LogGroupName: !Sub "/aws/kinesisfirehose/${AWS::StackName}" 322 | LogStreamName: !Sub "aws-waf-logs-${TopStackName}-${AWS::Region}" 323 | BufferingHints: 324 | IntervalInSeconds: 60 325 | SizeInMBs: 50 326 | CompressionFormat: UNCOMPRESSED 327 | EncryptionConfiguration: 328 | KMSEncryptionConfig: 329 | AWSKMSKeyARN: !Ref "WAFLogKMSKeyArn" 330 | Prefix: !Sub 'firehose/${AWS::Region}/' 331 | RoleARN: !Ref WAFDeliveryRoleArn 332 | KMSKey: 333 | Type: AWS::KMS::Key 334 | Properties: 335 | Description: CloudWatch Log Encryption Key 336 | EnableKeyRotation: true 337 | PendingWindowInDays: 20 338 | KeyPolicy: 339 | Version: '2012-10-17' 340 | Id: key-default-1 341 | Statement: 342 | - Sid: Enable IAM User Permissions 343 | Effect: Allow 344 | Principal: 345 | AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" 346 | Action: kms:* 347 | Resource: '*' 348 | - Sid: Allow administration of the key 349 | Effect: Allow 350 | Principal: 351 | Service: !Sub "logs.${AWS::Region}.amazonaws.com" 352 | Action: 353 | - kms:Encrypt 354 | - kms:Decrypt 355 | - kms:ReEncrypt* 356 | - kms:GenerateDataKey 357 | - kms:Describe* 358 | Resource: '*' 359 | Condition: 360 | ArnEquals: 361 | "kms:EncryptionContext:aws:logs:arn": !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/kinesisfirehose/${AWS::StackName}" 362 | -------------------------------------------------------------------------------- /cloudformation/templates/fms-wafv2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: 2010-09-09 3 | Parameters: 4 | PrimaryRegion: 5 | Type: String 6 | Description: This region is used to find the S3 Bucket and KMS key for the Kinesis Delivery Stream 7 | TopStackName: 8 | Type: String 9 | ScopeDetails: 10 | Type: String 11 | Default: 12 | IncludeExcludeScope: 13 | Type: String 14 | Default: Include 15 | AllowedValues: 16 | - Include 17 | - Exclude 18 | ScopeType: 19 | Type: String 20 | Description: "Should Firewall Manager Policies be scoped to the entire org (root) or a specific list of OUs (OU)" 21 | Default: Org 22 | AllowedValues: 23 | - Org 24 | - OU 25 | - Accounts 26 | optimizeUnassociatedWebACLValue: 27 | Type: String 28 | Description: "Should Firewall Manager manage unassociated web ACLs? if True Firewall Manager creates web ACLs in the accounts within policy scope only if the web ACLs will be used by at least one resource" 29 | Default: True 30 | AllowedValues: 31 | - True 32 | - False 33 | ResourceTagUsage: 34 | Type: String 35 | Default: Include 36 | Description: Include will scope to only include when ResourceTags match, Exclude will exclude when target resource tags match ResourceTags 37 | AllowedValues: 38 | - Include 39 | - Exclude 40 | ResourcesCleanUpFlag: 41 | Type: String 42 | Description: "Should Firewall Manager automatically remove protections from resources that leave the policy scope? (Not valid for Shield Advanced)" 43 | Default: True 44 | AllowedValues: 45 | - True 46 | - False 47 | WAFv2ProtectRegionalResourceTypes: 48 | Type: String 49 | Default: AWS::ApiGateway::Stage,AWS::ElasticLoadBalancingV2::LoadBalancer 50 | AllowedValues: 51 | - AWS::ApiGateway::Stage,AWS::ElasticLoadBalancingV2::LoadBalancer 52 | - AWS::ElasticLoadBalancingV2::LoadBalancer 53 | - AWS::ApiGateway::Stage 54 | - 55 | WAFv2ProtectCloudFront: 56 | Type: String 57 | Default: 58 | AllowedValues: 59 | - AWS::CloudFront::Distribution 60 | - 61 | WAFv2AutoRemediate: 62 | Type: String 63 | Description: "Should in scope AWS resource types that support AWS WAF automatically be remediated?" 64 | Default: "No" 65 | AllowedValues: 66 | - "Yes | Replace existing existing WebACL" 67 | - "Yes | If no current WebACL" 68 | - "No" 69 | ScopeTagName1: 70 | Type: String 71 | Default: 72 | ScopeTagName2: 73 | Type: String 74 | Default: 75 | ScopeTagName3: 76 | Type: String 77 | Default: 78 | ScopeTagValue1: 79 | Type: String 80 | Default: 81 | ScopeTagValue2: 82 | Type: String 83 | Default: 84 | ScopeTagValue3: 85 | Type: String 86 | Default: 87 | RateLimitValue: 88 | Type: String 89 | Description: AWS WAF rate limits use the previous 5 minutes to determine if an IP has exceeded the defined limit. 90 | Default: 10000 91 | RateLimitAction: 92 | Type: String 93 | Default: Block 94 | AllowedValues: 95 | - Block 96 | - Count 97 | AnonymousIPAMRAction: 98 | Type: String 99 | Description: The Anonymous IP list rule group contains rules to block requests from services that permit the obfuscation of viewer identity. These include requests from VPNs, proxies, Tor nodes, and hosting providers. This rule group is useful if you want to filter out viewers that might be trying to hide their identity from your application. 100 | Default: Block 101 | AllowedValues: 102 | - Block 103 | - Count 104 | IPReputationAMRAction: 105 | Type: String 106 | Description: The Amazon IP reputation list rule group contains rules that are based on Amazon internal threat intelligence. This is useful if you would like to block IP addresses typically associated with bots or other threats. Blocking these IP addresses can help mitigate bots and reduce the risk of a malicious actor discovering a vulnerable application. 107 | Default: Block 108 | AllowedValues: 109 | - Block 110 | - Count 111 | CoreRuleSetAMRAction: 112 | Type: String 113 | Description: The Core rule set (CRS) rule group contains rules that are generally applicable to web applications. This provides protection against exploitation of a wide range of vulnerabilities, including some of the high risk and commonly occurring vulnerabilities described in OWASP publications such as OWASP Top 10 114 | Default: Count 115 | AllowedValues: 116 | - Block 117 | - Count 118 | KnownBadInputsAMRAction: 119 | Type: String 120 | Description: The Known bad inputs rule group contains rules to block request patterns that are known to be invalid and are associated with exploitation or discovery of vulnerabilities. This can help reduce the risk of a malicious actor discovering a vulnerable application. 121 | Default: Count 122 | AllowedValues: 123 | - Block 124 | - Count 125 | S3BucketName: 126 | Type: String 127 | Conditions: 128 | ScopeTagName1Flag: !Not [!Equals [!Ref ScopeTagName1, ""]] 129 | ScopeTagName2Flag: !Not [!Equals [!Ref ScopeTagName2, ""]] 130 | ScopeTagName3Flag: !Not [!Equals [!Ref ScopeTagName3, ""]] 131 | ScopeTagValue1Flag: !Not [!Equals [!Ref ScopeTagValue1, ""]] 132 | ScopeTagValue2Flag: !Not [!Equals [!Ref ScopeTagValue2, ""]] 133 | ScopeTagValue3Flag: !Not [!Equals [!Ref ScopeTagValue3, ""]] 134 | AutoRemediateForceFlag: !Equals [!Ref WAFv2AutoRemediate, "Yes | Replace existing existing WebACL"] 135 | AutoRemediateFlag: !Or 136 | - !Equals [!Ref WAFv2AutoRemediate, "Yes | If no current WebACL"] 137 | - !Equals [!Ref WAFv2AutoRemediate, "Yes | Replace existing existing WebACL"] 138 | OUScopeFlag: !Equals [!Ref ScopeType, "OU"] 139 | AccountScopeFlag: !Equals [!Ref ScopeType, "Accounts"] 140 | ExcludeResourceTagFlag: !Equals [!Ref ResourceTagUsage, "Exclude"] 141 | IncludeScopeFlag: !Equals [!Ref IncludeExcludeScope, "Include"] 142 | ExcludeScopeFlag: !Equals [!Ref IncludeExcludeScope, "Exclude"] 143 | CreateRegionalPolicyFlag: !Not [ !Equals [ !Ref WAFv2ProtectRegionalResourceTypes, '' ] ] 144 | CreateCloudFrontPolicyFlag: !And [ !Not [ !Equals [!Ref WAFv2ProtectCloudFront, "" ] ] , !Equals [ !Ref AWS::Region, 'us-east-1' ] ] 145 | RateLimitActionFlag: !Equals [!Ref RateLimitAction,'Block'] 146 | AnonymousIPAMRActionFlag: !Equals [!Ref AnonymousIPAMRAction,'Block'] 147 | IPReputationAMRActionFlag: !Equals [!Ref IPReputationAMRAction,'Block'] 148 | CoreRuleSetAMRActionFlag: !Equals [!Ref CoreRuleSetAMRAction,'Block'] 149 | KnownBadInputsAMRActionFlag: !Equals [!Ref KnownBadInputsAMRAction,'Block'] 150 | 151 | Resources: 152 | RegionalWAFv2SecurityPolicy: 153 | Condition: CreateRegionalPolicyFlag 154 | Type: AWS::FMS::Policy 155 | Properties: 156 | PolicyName: !Sub OneClickShieldRegional-${AWS::StackName} 157 | ResourceType: ResourceTypeList 158 | ResourceTypeList: !Split [",", !Ref WAFv2ProtectRegionalResourceTypes] 159 | ExcludeResourceTags: !If [ExcludeResourceTagFlag, True, False] 160 | IncludeMap: 161 | ORGUNIT: 162 | !If [IncludeScopeFlag, !If [OUScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 163 | ACCOUNT: 164 | !If [IncludeScopeFlag, !If [AccountScopeFlag, !Split [",", !Ref ScopeDetails] , !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 165 | ExcludeMap: 166 | ORGUNIT: 167 | !If [ExcludeScopeFlag, !If [OUScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 168 | ACCOUNT: 169 | !If [ExcludeScopeFlag, !If [AccountScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 170 | RemediationEnabled: !If [AutoRemediateFlag, True, False] 171 | ResourcesCleanUp: !Ref ResourcesCleanUpFlag 172 | DeleteAllPolicyResources: False 173 | ResourceTags: 174 | !If 175 | - ScopeTagName1Flag 176 | - 177 | - !If 178 | - ScopeTagName1Flag 179 | - Key: !Ref ScopeTagName1 180 | Value: !If [ScopeTagValue1Flag, !Ref ScopeTagValue1, ""] 181 | - !Ref "AWS::NoValue" 182 | - !If 183 | - ScopeTagName2Flag 184 | - Key: !Ref ScopeTagName2 185 | Value: !If [ScopeTagValue2Flag, !Ref ScopeTagValue2, ""] 186 | - !Ref "AWS::NoValue" 187 | - !If 188 | - ScopeTagName3Flag 189 | - Key: !Ref ScopeTagName3 190 | Value: !If [ScopeTagValue3Flag, !Ref ScopeTagValue3, ""] 191 | - !Ref "AWS::NoValue" 192 | - !Ref "AWS::NoValue" 193 | 194 | SecurityServicePolicyData: 195 | Type: WAFV2 196 | ManagedServiceData: !Sub 197 | - '{"type": "WAFV2", "preProcessRuleGroups":[{"ruleGroupArn": "${RateLimitRGArn}", "overrideAction": {"type": "NONE"}, "ruleGroupType":"RuleGroup", "excludeRules": [], "sampledRequestsEnabled": true}, {"managedRuleGroupIdentifier": {"vendorName": "AWS", "managedRuleGroupName":"AWSManagedRulesAmazonIpReputationList"}, "overrideAction": {"type": "${IPReputationActionValue}"},"ruleGroupType": "ManagedRuleGroup", "excludeRules":[], "sampledRequestsEnabled": true}, {"managedRuleGroupIdentifier": {"vendorName": "AWS", "managedRuleGroupName": "AWSManagedRulesAnonymousIpList"},"overrideAction": {"type": "${AnonymousIPActionValue}"}, "ruleGroupType": "ManagedRuleGroup", "excludeRules": [], "sampledRequestsEnabled": true},{"managedRuleGroupIdentifier": {"vendorName": "AWS", "managedRuleGroupName": "AWSManagedRulesKnownBadInputsRuleSet"}, "overrideAction": {"type": "${KnownBadInputsActionValue}"}, "ruleGroupType": "ManagedRuleGroup", "excludeRules": [], "sampledRequestsEnabled": true},{"managedRuleGroupIdentifier": {"vendorName": "AWS", "managedRuleGroupName": "AWSManagedRulesCommonRuleSet"}, "overrideAction": {"type": "${CoreRuleSetActionValue}"}, "ruleGroupType": "ManagedRuleGroup", "excludeRules": [], "sampledRequestsEnabled": true}],"postProcessRuleGroups": [], "defaultAction": {"type": "ALLOW"}, "overrideCustomerWebACLAssociation": ${AutoRemediateForceValue}, "sampledRequestsEnabledForDefaultActions": true,"loggingConfiguration": {"redactedFields": [],"logDestinationConfigs": ["${FirehoseArn}"]}, "optimizeUnassociatedWebACL": ${optimizeUnassociatedWebACLValue}}' 198 | - RateLimitRGArn: !GetAtt RegionalRateLimitRuleGroup.Arn 199 | IPReputationActionValue: !If [IPReputationAMRActionFlag, 'NONE','COUNT'] 200 | AnonymousIPActionValue: !If [AnonymousIPAMRActionFlag, 'NONE','COUNT'] 201 | KnownBadInputsActionValue: !If [KnownBadInputsAMRActionFlag, 'NONE','COUNT'] 202 | CoreRuleSetActionValue: !If [CoreRuleSetAMRActionFlag, 'NONE','COUNT'] 203 | FirehoseArn: !GetAtt "WAFDeliverystream.Arn" 204 | AutoRemediateForceValue: !If [AutoRemediateForceFlag,True,False] 205 | Tags: 206 | - Key: aws-sample-project-name 207 | Value: aws-shield-advanced-one-click-deployment 208 | CloudFrontWAFv2SecurityPolicy: 209 | Condition: CreateCloudFrontPolicyFlag 210 | Type: AWS::FMS::Policy 211 | Properties: 212 | PolicyName: !Sub OneClickShieldGlobal-${AWS::StackName} 213 | ResourceType: AWS::CloudFront::Distribution 214 | ExcludeResourceTags: !If [ExcludeResourceTagFlag, True, False] 215 | IncludeMap: 216 | ORGUNIT: 217 | !If [IncludeScopeFlag, !If [OUScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 218 | ACCOUNT: 219 | !If [IncludeScopeFlag, !If [AccountScopeFlag, !Split [",", !Ref ScopeDetails] , !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 220 | ExcludeMap: 221 | ORGUNIT: 222 | !If [ExcludeScopeFlag, !If [OUScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 223 | ACCOUNT: 224 | !If [ExcludeScopeFlag, !If [AccountScopeFlag, !Split [",", !Ref ScopeDetails], !Ref "AWS::NoValue"], !Ref "AWS::NoValue"] 225 | RemediationEnabled: !If [AutoRemediateFlag, true, false] 226 | ResourcesCleanUp: !Ref ResourcesCleanUpFlag 227 | DeleteAllPolicyResources: false 228 | ResourceTags: 229 | !If 230 | - ScopeTagName1Flag 231 | - 232 | - !If 233 | - ScopeTagName1Flag 234 | - Key: !Ref ScopeTagName1 235 | Value: !If [ScopeTagValue1Flag, !Ref ScopeTagValue1, ""] 236 | - !Ref "AWS::NoValue" 237 | - !If 238 | - ScopeTagName2Flag 239 | - Key: !Ref ScopeTagName2 240 | Value: !If [ScopeTagValue2Flag, !Ref ScopeTagValue2, ""] 241 | - !Ref "AWS::NoValue" 242 | - !If 243 | - ScopeTagName3Flag 244 | - Key: !Ref ScopeTagName3 245 | Value: !If [ScopeTagValue3Flag, !Ref ScopeTagValue3, ""] 246 | - !Ref "AWS::NoValue" 247 | - !Ref "AWS::NoValue" 248 | 249 | SecurityServicePolicyData: 250 | Type: WAFV2 251 | ManagedServiceData: !Sub 252 | - '{"type": "WAFV2", "preProcessRuleGroups":[{"ruleGroupArn": "${RateLimitRGArn}", "overrideAction": {"type": "NONE"}, "ruleGroupType":"RuleGroup", "excludeRules": [], "sampledRequestsEnabled": true}, {"managedRuleGroupIdentifier": {"vendorName": "AWS", "managedRuleGroupName":"AWSManagedRulesAmazonIpReputationList"}, "overrideAction": {"type": "${IPReputationActionValue}"},"ruleGroupType": "ManagedRuleGroup", "excludeRules":[], "sampledRequestsEnabled": true}, {"managedRuleGroupIdentifier": {"vendorName": "AWS", "managedRuleGroupName": "AWSManagedRulesAnonymousIpList"},"overrideAction": {"type": "${AnonymousIPActionValue}"}, "ruleGroupType": "ManagedRuleGroup", "excludeRules": [], "sampledRequestsEnabled": true},{"managedRuleGroupIdentifier": {"vendorName": "AWS", "managedRuleGroupName": "AWSManagedRulesKnownBadInputsRuleSet"}, "overrideAction": {"type": "${KnownBadInputsActionValue}"}, "ruleGroupType": "ManagedRuleGroup", "excludeRules": [], "sampledRequestsEnabled": true},{"managedRuleGroupIdentifier": {"vendorName": "AWS", "managedRuleGroupName": "AWSManagedRulesCommonRuleSet"}, "overrideAction": {"type": "${CoreRuleSetActionValue}"}, "ruleGroupType": "ManagedRuleGroup", "excludeRules": [], "sampledRequestsEnabled": true}],"postProcessRuleGroups": [], "defaultAction": {"type": "ALLOW"}, "overrideCustomerWebACLAssociation": ${AutoRemediateForceValue}, "sampledRequestsEnabledForDefaultActions": true,"loggingConfiguration": {"redactedFields": [],"logDestinationConfigs": ["${FirehoseArn}"]}, "optimizeUnassociatedWebACL": ${optimizeUnassociatedWebACLValue}}' 253 | - RateLimitRGArn: !GetAtt CloudFrontRateLimitRuleGroup.Arn 254 | IPReputationActionValue: !If [IPReputationAMRActionFlag, 'NONE','COUNT'] 255 | AnonymousIPActionValue: !If [AnonymousIPAMRActionFlag, 'NONE','COUNT'] 256 | KnownBadInputsActionValue: !If [KnownBadInputsAMRActionFlag, 'NONE','COUNT'] 257 | CoreRuleSetActionValue: !If [CoreRuleSetAMRActionFlag, 'NONE','COUNT'] 258 | FirehoseArn: !GetAtt "WAFDeliverystream.Arn" 259 | AutoRemediateForceValue: !If [AutoRemediateForceFlag,True,False] 260 | Tags: 261 | - Key: aws-sample-project-name 262 | Value: aws-shield-advanced-one-click-deployment 263 | RegionalRateLimitRuleGroup: 264 | Condition: CreateRegionalPolicyFlag 265 | DeletionPolicy: Retain 266 | Type: 'AWS::WAFv2::RuleGroup' 267 | Properties: 268 | Scope: REGIONAL 269 | Description: !Sub OneClickDeployRuleGroup-${AWS::StackName} 270 | VisibilityConfig: 271 | SampledRequestsEnabled: true 272 | CloudWatchMetricsEnabled: true 273 | MetricName: !Sub OneClickDeployRuleGroup-${AWS::StackName} 274 | Capacity: 10 275 | Rules: 276 | - Name: RateLimit 277 | Priority: 0 278 | Statement: 279 | RateBasedStatement: 280 | Limit: !Ref RateLimitValue 281 | AggregateKeyType: IP 282 | Action: 283 | Block: !If [RateLimitActionFlag, {},!Ref "AWS::NoValue"] 284 | Count: !If [RateLimitActionFlag, !Ref "AWS::NoValue", {}] 285 | VisibilityConfig: 286 | SampledRequestsEnabled: true 287 | CloudWatchMetricsEnabled: true 288 | MetricName: RateLimit 289 | CloudFrontRateLimitRuleGroup: 290 | Condition: CreateCloudFrontPolicyFlag 291 | DeletionPolicy: Retain 292 | Type: 'AWS::WAFv2::RuleGroup' 293 | Properties: 294 | Scope: CLOUDFRONT 295 | Description: !Sub OneClickDeployRuleGroup-${AWS::StackName} 296 | VisibilityConfig: 297 | SampledRequestsEnabled: true 298 | CloudWatchMetricsEnabled: true 299 | MetricName: !Sub OneClickDeployRuleGroup-${AWS::StackName} 300 | Capacity: 10 301 | Rules: 302 | - Name: RateLimit 303 | Priority: 0 304 | Statement: 305 | RateBasedStatement: 306 | Limit: !Ref RateLimitValue 307 | AggregateKeyType: IP 308 | Action: 309 | Block: !If [RateLimitActionFlag, {},!Ref "AWS::NoValue"] 310 | Count: !If [RateLimitActionFlag, !Ref "AWS::NoValue", {}] 311 | VisibilityConfig: 312 | SampledRequestsEnabled: true 313 | CloudWatchMetricsEnabled: true 314 | MetricName: RateLimit 315 | WAFDeliveryLogGroup: 316 | Type: AWS::Logs::LogGroup 317 | Properties: 318 | LogGroupName: !Sub "/aws/kinesisfirehose/${AWS::StackName}" 319 | KmsKeyId: !GetAtt KMSKey.Arn 320 | RetentionInDays: 365 321 | 322 | WAFDeliveryLogStream: 323 | DependsOn: WAFDeliveryLogGroup 324 | Type: AWS::Logs::LogStream 325 | Properties: 326 | LogGroupName: !Sub "/aws/kinesisfirehose/${AWS::StackName}" 327 | LogStreamName: !Sub "aws-waf-logs-${TopStackName}-${AWS::Region}" 328 | WAFDeliverystream: 329 | Type: AWS::KinesisFirehose::DeliveryStream 330 | Metadata: 331 | cfn_nag: 332 | rules_to_suppress: 333 | - id: W88 334 | reason: "There is no Delivery Stream, these are from FMS, not applicable" 335 | Properties: 336 | DeliveryStreamName: !Sub "aws-waf-logs-${TopStackName}-${AWS::Region}" 337 | DeliveryStreamEncryptionConfigurationInput: 338 | KeyARN: !GetAtt KMSKey.Arn 339 | KeyType: CUSTOMER_MANAGED_CMK 340 | ExtendedS3DestinationConfiguration: 341 | BucketARN: !Sub "arn:aws:s3:::${CallRetrieveFirehoseMetadata.S3BucketName}" 342 | CloudWatchLoggingOptions: 343 | Enabled: true 344 | LogGroupName: !Sub "/aws/kinesisfirehose/${AWS::StackName}" 345 | LogStreamName: !Sub "aws-waf-logs-${TopStackName}-${AWS::Region}" 346 | BufferingHints: 347 | IntervalInSeconds: 60 348 | SizeInMBs: 50 349 | CompressionFormat: UNCOMPRESSED 350 | EncryptionConfiguration: 351 | KMSEncryptionConfig: 352 | AWSKMSKeyARN: !GetAtt CallRetrieveFirehoseMetadata.KmsArn 353 | Prefix: !Sub 'firehose/${AWS::Region}/' 354 | RoleARN: !GetAtt CallRetrieveFirehoseMetadata.RoleArn 355 | KMSKey: 356 | Type: AWS::KMS::Key 357 | Properties: 358 | Description: CloudWatch Log Encryption Key 359 | EnableKeyRotation: true 360 | PendingWindowInDays: 20 361 | KeyPolicy: 362 | Version: '2012-10-17' 363 | Id: key-default-1 364 | Statement: 365 | - Sid: Enable IAM User Permissions 366 | Effect: Allow 367 | Principal: 368 | AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" 369 | Action: kms:* 370 | Resource: '*' 371 | - Sid: Allow administration of the key 372 | Effect: Allow 373 | Principal: 374 | Service: !Sub "logs.${AWS::Region}.amazonaws.com" 375 | Action: 376 | - kms:Encrypt 377 | - kms:Decrypt 378 | - kms:ReEncrypt* 379 | - kms:GenerateDataKey 380 | - kms:Describe* 381 | Resource: '*' 382 | Condition: 383 | ArnEquals: 384 | "kms:EncryptionContext:aws:logs:arn": !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/kinesisfirehose/${AWS::StackName}" 385 | 386 | RetrieveFirehoseMetadataLambdaRole: 387 | Type: AWS::IAM::Role 388 | Properties: 389 | AssumeRolePolicyDocument: 390 | Version: 2012-10-17 391 | Statement: 392 | - Effect: Allow 393 | Principal: 394 | Service: 395 | - lambda.amazonaws.com 396 | Action: 397 | - 'sts:AssumeRole' 398 | Path: / 399 | RetrieveFirehoseMetadataLambdaPolicy: 400 | Type: 'AWS::IAM::Policy' 401 | Metadata: 402 | cfn_nag: 403 | rules_to_suppress: 404 | - id: W12 405 | reason: "Wildcard IAM policy required, APIs do not support resource scoping" 406 | - id: W58 407 | reason: "CFN Nag checks for managed policy, permissions granted inline" 408 | Properties: 409 | PolicyName: RetrieveFirehoseMetadataLambdaPolicy 410 | PolicyDocument: 411 | Version: 2012-10-17 412 | Statement: 413 | - Effect: Allow 414 | Action: 415 | - "logs:CreateLogGroup" 416 | - "logs:CreateLogStream" 417 | - "logs:PutLogEvents" 418 | Resource: "arn:aws:logs:*:*:*" 419 | - Effect: Allow 420 | Action: 421 | - ssm:GetParameter 422 | Resource: !Sub "arn:aws:ssm:${PrimaryRegion}:${AWS::AccountId}:parameter/shield-one-click-deployment/waf-log-destination" 423 | Roles: 424 | - !Ref RetrieveFirehoseMetadataLambdaRole 425 | 426 | CallRetrieveFirehoseMetadata: 427 | DependsOn: RetrieveFirehoseMetadataLambdaPolicy 428 | Type: Custom::RetrieveFirehoseMetadata 429 | Properties: 430 | ServiceToken: !GetAtt RetrieveFirehoseMetadataLambda.Arn 431 | S3BucketName: !Ref S3BucketName #This is not used, but if you change the name this resource needs to update so we include it. 432 | 433 | RetrieveFirehoseMetadataLambda: 434 | Type: AWS::Lambda::Function 435 | Metadata: 436 | cfn_nag: 437 | rules_to_suppress: 438 | - id: W58 439 | reason: "Permissions granted, CFN_Nag not parsing correctly?" 440 | - id: W89 441 | reason: "Not applicable for use case" 442 | - id: W92 443 | reason: "Not applicable for use case" 444 | Properties: 445 | TracingConfig: 446 | Mode: Active 447 | Runtime: python3.12 448 | Timeout: 15 449 | Role: !GetAtt RetrieveFirehoseMetadataLambdaRole.Arn 450 | Handler: index.lambda_handler 451 | Environment: 452 | Variables: 453 | PrimaryRegion: !Ref PrimaryRegion 454 | Code: 455 | ZipFile: | 456 | import boto3 457 | import json 458 | import os 459 | import botocore 460 | import cfnresponse 461 | import logging 462 | 463 | logger = logging.getLogger('ssm-cross-region-help') 464 | logger.setLevel('DEBUG') 465 | 466 | primary_region = os.environ.get('PrimaryRegion') 467 | account_id = boto3.client('sts').get_caller_identity().get('Account') 468 | 469 | ssm_client = boto3.client('ssm', region_name=primary_region) 470 | 471 | def lambda_handler(event, context): 472 | print (json.dumps(event)) 473 | responseData = {} 474 | if "RequestType" in event: 475 | if event['RequestType'] in ['Create','Update']: 476 | ssm_arn = f"arn:aws:ssm:{primary_region}:{account_id}:parameter/shield-one-click-deployment/waf-log-destination" 477 | try: 478 | logger.debug(f"Start retrieving SSM Parameter for {ssm_arn}") 479 | response = ssm_client.get_parameter( 480 | Name=ssm_arn, 481 | WithDecryption=True).get('Parameter',{}).get('Value',False) 482 | print (response) 483 | print (type(response)) 484 | responseData.update(json.loads(response)) 485 | #responseData['Data']['LoggingConfig'] = json.loads(response) 486 | responseData['Message'] = "Retrived config successfully Created" 487 | print (f"Sending Response Data: {responseData}") 488 | cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "Retrived Value") 489 | return 490 | except botocore.exceptions.ClientError as error: 491 | logger.error(error.response['Error']) 492 | responseData['Message'] = error.response['Error'] 493 | cfnresponse.send(event, context, cfnresponse.FAILED, responseData, "SubscribeFailed") 494 | return 495 | else: 496 | responseData['Message'] = "CFN Delete, no action taken" 497 | cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "CFNDeleteGracefulContinue") 498 | return -------------------------------------------------------------------------------- /terraform/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | AWS Shield Advanced One Click deployments allows customers getting started with Shield Advanced to get an out of the box recommended baseline configuration. You execute a single Python script and 16 required input parameters with 39 optional parameters to scope and otherwise tune this deployment. This template creates: 3 | 4 | ## Core Deployment 5 | 6 | ### Central Common resources 7 | A core deployment creates a central S3 bucket for all WAFv2 logs as well as an Athena Table with useful named queries and views, and some additional supporting resources. 8 | 9 | ## Organizational Deployment 10 | ### Subscribe and Configure Shield Advanced 11 | Subscribes account to Shield via a custom lambda backed resource. Configures Sheild Response Team (SRT) access and proactive engagement 12 | 13 | ### Firewall Manager Security Policies for Shield Protection 14 | Creates Security policies in each configured region and globally (if desired) to ensure resources are shield protected based on a provided scope 15 | 16 | ### Firewall Manager Security Policies for WAFv2 17 | Creates Security policies in each configured region and globally (if desired) to manage WAFv2 on supported resources based on a provided scope 18 | 19 | 20 | # Prerequisites 21 | 22 | ## AWS Support 23 | Business or Enterprise Support must be enabled on any account where SRT access and/or Proactive Engagment will be enabled 24 | 25 | ## AWS Organizations 26 | This will only apply to accounts that are members of your designated AWS Organization. The Python script queries your Organization Management account to identify all the member Organizational Units and Accounts 27 | Recommended to delegate administration to another account such as your default Firewall Manager administrator account. (see below) 28 | 29 | ## AWS Firewall Manager 30 | Firewall Manager must be enabled. Recommended to delegate administration to another account 31 | https://docs.aws.amazon.com/waf/latest/developerguide/enable-integration.html 32 | 33 | ## AWS Config 34 | AWS Config must be enabled with at least the following resources enabled: 35 | ``` 36 | AWS::ApiGateway::Stage, 37 | AWS::CloudFront::Distribution, 38 | AWS::EC2::EIP, 39 | AWS::ElasticLoadBalancing::LoadBalancer, 40 | AWS::ElasticLoadBalancingV2::LoadBalancer, 41 | AWS::Shield::Protection, 42 | AWS::WAF::WebACL, 43 | AWS::WAFv2::WebACL, 44 | AWS::ShieldRegional::Protection 45 | ``` 46 | 47 | # Deployment configuration 48 | ## Default 49 | If you only provide answers for mandatory fields, the default deployment will do the following: 50 | 51 | 1. Subscribe and configure Shield Advanced for all AWS accounts within the AWS Organization. 52 | 2. Shield protect all supported regional resources as well as CloudFront distributions. 53 | 3. All CloudFront Distributions, Application Load balancers, and API Gateways will have an AWS WAF WebACL attached (if there was not already one in place). 54 | 4. All WebACLs send WAF logs to a central bucket in a central region. _This S3 bucket is created in the location the initial stack was deployed in._ 55 | 5. The WebACl includes the following rules: 56 | a) A [Rate based rule](https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-rate-based.html) with a value of 10,000 action of Count 57 | b) [Anonymous IP](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-ip-rep.html#aws-managed-rule-groups-ip-rep-anonymous) Amazon Managed rule | All rule actions overridden to Count 58 | c) [IP Reputation](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-ip-rep.html#aws-managed-rule-groups-ip-rep-amazon) Amazon Managed rule | All rules with action Block 59 | d) [Core Rule Set](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html#aws-managed-rule-groups-baseline-crs) Amazon Managed rule | All rule actions overridden to Count 60 | e) [Known Bad Inputs](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html#aws-managed-rule-groups-baseline-known-bad-inputs) Amazon Managed rule | All rule actions overridden to Count 61 | 6. An Athena Table and workgroup created with several named queries and views relevant to reviewing WAF logs. 62 | 63 | ## **Customization** 64 | 65 | Terraform variables allow you to change the default behavior as follows: 66 | 67 | ### **Scope** 68 | 1. You can configure which region(s) are in scope. This applies to subscribing and configuring Shield as well as Firewall Manager security policies. 69 | 2. You can configure which AWS accounts are in scope. This applies to subscribing and configuring Shield as well as Firewall Manager security policies. 70 | 3. Firewall Manager security policies can be scoped to only certain resource types and/or the presence of specific tags on resources. 71 | 4. Firewall Manager security policies can be scoped based on the specific tag name/values or existance of specific tag names. Shield and WAFv2 policies can specify different tag scopes. 72 | 73 | ### **Configuration** 74 | **Shield** 75 | 1. SRT access can be enabled/disable. 76 | 2. SRT can be given access to S3 Buckets. 77 | 3. The IAM role used to grant SRT Access (if enabled). 78 | 4. Choose to have CloudFormation create the SRT Access role or specify the name of a role that already exists. 79 | 80 | **AWS WAF** 81 | 1. The rate based rule value and action can be changed between 100 and 2,000,000 and set to "Block". 82 | 2. Each Amazon Managed Rule (AMR) can set the action for that AMR to "Count" or "Block". 83 | 84 | 85 | # How to deploy 86 | 87 | The python script tfstacks.py executes both the core-deployment and organizational-deployment Terraform configurations. 88 | You will need 2 separate tfvars files to be referenced during the script execution. See below for example execution of the python script. (It is advised to run this inside a python virtual environment). The `core-var-file` and the `org-var-file` tfvars are relative to the `core-deployment` and `organizational-deployment` directories respectively. 89 | 90 | ``` 91 | python3 tfstacks.py --core-var-file example.tfvars --org-var-file example.tfvars 92 | ``` 93 | 94 | The following sections require your acknowledgment (Set to _true_) or have a mandatory input that require your attention. 95 | 96 | # Required Terraform Configuration 97 | 98 | In order to properly configure Terraform for your environment the backend configuration *MUST* be customized to your environment. In the `core-deployment` and `organizational-deployment` directories in the `terraform` block within `main.tf` you *MUST* enter the appropriate values for your backend s3 configuration (or leverage another backend). 99 | 100 | *NOTE:* The execution of this relies on workspaces to control the deployments to the individual accounts. 101 | ``` 102 | backend "s3" { 103 | // Backend state MUST be properly confgured and is specific to deployment environment. 104 | bucket = "" 105 | workspace_key_prefix = "" 106 | key = "" 107 | region = "" 108 | } 109 | ``` 110 | 111 | # Variables 112 | Variables are divided into logical groups of parameters between mandatory and optional parameters. Mandatory parameters either require the end user to input/select a value, or explicity verify something. Optional parameters allow end users to customize (e.g. WAF rule actions ) or configure optional features (e.g more than one proactive engagement emergency contact) 113 | 114 | --- 115 | 116 | ## Core Deployment 117 | The core-deployment has only 2 variables that must be set to be available to set up. 118 | 119 | - `scope_regions`: This will determine which regions will be affected. The python script utilizes this variable in addition to Terraform to determine which regions in the Organization Accounts will be targeted. 120 | - `use_lake_formation_permisisons`: This determines if lake formation permissions are used when creating the Athena 121 | ``` 122 | scope_regions = ["", "", ..., "regionN"] 123 | use_lake_formation_permissions = true | false 124 | ``` 125 | - `region`: This is the target region for the primary deployment in which the aws provider is anchored. 126 | 127 | ## AWS Shield Advanced Subscription **[Mandatory]** 128 | 129 | **AcknowledgeServiceTermsPricing** 130 | Acknowledge AWS Shield Advanced has a $3000 / month subscription fee for a consolidated billing family 131 | 132 | Variable: acknowledge_service_terms_pricing 133 | Required: Yes 134 | Type: bool 135 | AllowedValues: true, false 136 | 137 | **AcknowledgeServiceTermsDTO** 138 | Acknowledge AWS Shield Advanced has a Data transfer out usage fees for all protected resources. 139 | 140 | Variable: acknowledge_service_terms_dto 141 | Required: Yes 142 | Type: bool 143 | AllowedValues: true, false 144 | 145 | **AcknowledgeServiceTermsCommitment** 146 | AWS Shield Advanced has a 12 month commitment. 147 | 148 | Variable: acknowledge_service_terms_commitment 149 | Required: Yes 150 | Type: bool 151 | AllowedValues: true, false 152 | 153 | **AcknowledgeServiceTermsAutoRenew** 154 | Acknowledge Shield Advanced subscriptions will auto-renewed after 12 months. However, I can opt out of renewal 30 days prior to the renewal date. 155 | 156 | Variable: acknowledge_service_terms_auto_renew 157 | Required: Yes 158 | Type: bool 159 | AllowedValues: true, false 160 | 161 | **AcknowledgeNoUnsubscribe** 162 | Acknowledge a Shield Advanced subscription comittment will continue even if this Terraform Deployment is deleted. 163 | 164 | Variable: acknowledge_no_unsubscribe 165 | Required: Yes 166 | Type: bool 167 | AllowedValues: true, false 168 | 169 | --- 170 | 171 | ## AWS Shield Advanced Configuration **[Mandatory]** 172 | 173 | **EnableSRTAccess** 174 | AWS Shield Advanced allows you to create and authorize SRT (Shield Response Team) access to update your AWS WAF WebACLs. Specify if this feature should be enabled or disabled. 175 | 176 | Variable: enable_srt_access 177 | Required: Yes 178 | Type: bool 179 | AllowedValues: true, false 180 | 181 | 182 | **EnabledProactiveEngagement** 183 | AWS SRT (Shield Response Team) can proactively reach out when Shield Advanced detects a DDoS event and your application health is impacted (required configuring Route 53 health checks). One or more emergency contacts must also be configured 184 | 185 | Variable: enable_proactive_engagement 186 | Required: Yes 187 | Type: bool 188 | AllowedValues: true, false 189 | 190 | **EmergencyContactEmail1** 191 | During a Proactive Engagement, SRT (Shield Response Team) will reach out to your emergency contacts. You must configure one or more emergency contacts along with enabling Proactive Engagement. Specify a valid e-mail address 192 | 193 | Variable: emergency_contact_email1 194 | Required: Yes 195 | Type: String 196 | AllowedPattern: ^\S+@\S+\.\S+$|\ 197 | 198 | **EmergencyContactPhone1** 199 | During a Proactive Engagement, SRT (Shield Response Team) will reach out to your emergency contacts. You must configure one or more emergency contacts along with enabling Proactive Engagement. Specify a valid phone number + country code. e.g. \+ 15555555555 200 | 201 | Variable: emergency_contact_phone1 202 | Required: Yes 203 | Type: String 204 | AllowedPattern: ^\+[0-9]{11}|\ 205 | 206 | **EmergencyContactNote1** 207 | During a Proactive Engagement, SRT (Shield Response Team) will reach out to your emergency contacts. You must configure one or more emergency contacts along with enabling Proactive Engagement. Provide notes/comments about this contact. E.g. SOC 208 | 209 | Variable: emergency_contact_note1 210 | Required: Yes 211 | Type: String 212 | AllowedPattern: ^[\w\s\.\-,:/()+@]*$|\ 213 | 214 | ## Scope **[Mandatory]** 215 | 216 | **FMSAdministratorAccountId** 217 | The account ID where Firewall Manager security policies be created in. There is no default and the account ID must be provided. 218 | 219 | Variable: fms_admin_account_id 220 | Required: Yes 221 | Type: String 222 | AllowedPatterns: 223 | - \d{12} 224 | 225 | **ScopeType** 226 | CloudFormation Service Managed StackSets can be deployed to an entire AWS Organization, a list of OUs, or a list of accounts. Specify which scope you would like to target for this deployment. This applies to subscribing and configured, Shield protecting resources, and WAFv2 protecting resources. 227 | 228 | Variable: scope_type 229 | Required: Yes 230 | Type: String 231 | AllowedValues: 232 | - Org 233 | - OUs 234 | - Accounts 235 | 236 | **ScopeDetails** 237 | If ScopeType is Org, this parameter requires the root-id for the Organization. E.g. r-1234 238 | If ScopeType is OUs, this parameter is a comma separated list of AWS Organization OUs. 239 | If ScopeType is Accounts, this parameter is a comma separated list of AWS Account IDs 240 | 241 | Variable: scope_details 242 | Required: Yes 243 | Type: CommaDelimitedList 244 | AllowedPattern: 245 | - (^r-[0-9a-z]{4,32}$) 246 | or 247 | - ((ou-[0-9a-z]{4,32}-[a-z0-9]{8,32})((,\s*|,\)ou-[0-9a-z]{4,32}-[a-z0-9]{8,32}$)*) 248 | or 249 | - (\d{12})(,\s*\d{12})*) 250 | 251 | **IncludeExcludeScope** 252 | If ScopeType is OUs or Account IDs, is the value provided as ScopeDetails what is in scope or out of scope? 253 | 254 | Variable: include_exclude_scope 255 | Required: Yes 256 | Type: String 257 | Default: Include 258 | AllowedValues: 259 | - Include 260 | 261 | **ScopeRegions** 262 | List of all AWS regions that are in scope for CloudFormation Stack Sets to deploy Firewall Manager Security policies 263 | 264 | Required: Yes 265 | Type: CommaDelimitedList 266 | AllowedPattern: ((af|ap|ca|eu|me|sa|us|il)-(central|north|(north(?:east|west))|south|south(?:east|west)|east|west)-\d+)(,\s(af|ap|ca|eu|me|sa|us|il)-(central|north|(north(?:east|west))|south|south(?:east|west)|east|west)-\d+)* 267 | 268 | **CallAs** 269 | CloudFormation Service Managed StackSets being run from a delegated CloudFormation StackSet administrator to specify they are calling as such. 270 | 271 | Required: Yes 272 | Type: String 273 | AllowedValues: 274 | - DELEGATED_ADMIN 275 | - SELF 276 | 277 | --- 278 | 279 | ## Stack Set Deployment options **[Optional]** 280 | 281 | **ProtectRegionalResourceTypes** 282 | Firewall Manager can establish Shield Protection on regional Resources. Select one of the following combinations of supported regional resources 283 | 284 | Required: No 285 | Type: String 286 | AllowedValues: 287 | - AWS::ElasticLoadBalancingV2::LoadBalancer,AWS::ElasticLoadBalancing::LoadBalancer,AWS::EC2::EIP 288 | - AWS::ElasticLoadBalancingV2::LoadBalancer,AWS::ElasticLoadBalancing::LoadBalancer 289 | - AWS::ElasticLoadBalancingV2::LoadBalancer,AWS::EC2::EIP 290 | - AWS::ElasticLoadBalancing::LoadBalancer,AWS::EC2::EIP 291 | - AWS::ElasticLoadBalancingV2::LoadBalancer 292 | - AWS::ElasticLoadBalancing::LoadBalancer 293 | - AWS::EC2::EIP 294 | - 295 | 296 | **ProtectCloudFront** 297 | Firewall Manager can establish Shield Protection on Global Resources; today this is only CloudFront. Select CloudFront global resources that should be Shield Protected. 298 | 299 | Required: No 300 | Type: String 301 | AllowedValues: 302 | - AWS::CloudFront::Distribution 303 | - 304 | 305 | **WAFv2ProtectRegionalResourceTypes** 306 | Firewall Manager can create and associate a WebACl on regional Resources. Select one of the following combinations of supported regional resources 307 | 308 | Required: No 309 | Type: String 310 | AllowedValues: 311 | - AWS::ApiGateway::Stage,AWS::ElasticLoadBalancingV2::LoadBalancer 312 | - AWS::ElasticLoadBalancingV2::LoadBalancer 313 | - AWS::ApiGateway::Stage 314 | - 315 | 316 | **WAFv2ProtectCloudFront** 317 | Firewall Manager can create and associate a WebACl on global Resources. Select one of the following combinations of supported global resources 318 | 319 | Required: No 320 | Type: String 321 | AllowedValues: 322 | - AWS::CloudFront::Distribution 323 | - 324 | 325 | **ShieldAutoRemediate** 326 | Firewall Manager can establish Shield Protection for regional and global resources. For resources that are in scope, should in scope AWS resource types have Shield Advanced protection automatically be remediated? 327 | 328 | Required: No 329 | Type: String 330 | AllowedValues: 331 | - Yes 332 | - No 333 | 334 | **WAFv2AutoRemediate** 335 | Firewall Manager can create and associate WAF WebACLs for regional and global resources. Should in scope AWS resource types that support AWS WAF automatically be remediated? For resources that are in scope, if a resource does not have the Firewall Managered WebACL, what action should be taken. Firewall Manager, when remeidating, can only remediate if no WebACL is in place or force associate its WebACL. 336 | 337 | Required: No 338 | Type: String 339 | Default: Yes | If no current WebACL 340 | AllowedValues: 341 | - Yes | Replace existing existing WebACL 342 | - Yes | If no current WebACL 343 | - No 344 | 345 | --- 346 | 347 | ## AWS WAF v2 Configuration **[Optional]** 348 | 349 | **RateLimitValue** 350 | The value for the WebACL deployed by Firewall Manager rate based rule. This value represents how many request by IP can be made in a 5 minute lookback period. IPs that exceed this limit take whatever action is configured by RateLimitAction (Default is block) 351 | 352 | Required: No 353 | Type: Number 354 | Minvalue: 100 355 | MaxValue: 2,000,000 356 | 357 | **RateLimitAction** 358 | The action for the WebACL deployed by Firewall Manager rate based rule. By default this will block 359 | 360 | Required: No 361 | Type: String 362 | Default: Block 363 | AllowedValues: 364 | - Count 365 | - Block 366 | 367 | **IPReputationAMRAction** 368 | The WebACL deployed by Firewall Manager includes the AMR (Amazon Managed Rule) [IPReputationAMRAction](). The default action for this rule is COUNT. 369 | 370 | Required: No 371 | Type: String 372 | Default: Block 373 | AllowedValues: 374 | - Count 375 | - Block 376 | 377 | **AnonymousIPAMRAction** 378 | The WebACL deployed by Firewall Manager includes the AMR (Amazon Managed Rule) [AWSManagedRulesAnonymousIpList](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-ip-rep.html#aws-managed-rule-groups-ip-rep-anonymoushttps://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-ip-rep.html#aws-managed-rule-groups-ip-rep-amazon). The default action for this rule is COUNT. 379 | 380 | Required: No 381 | Type: String 382 | Default: Count 383 | AllowedValues: 384 | - Count 385 | - Block 386 | 387 | **KnownBadInputsAMRAction** 388 | The WebACL deployed by Firewall Manager includes the AMR (Amazon Managed Rule) [KnownBadInputsAMRAction](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html#aws-managed-rule-groups-baseline-known-bad-inputs). The default action for this rule is COUNT. 389 | 390 | Required: No 391 | Type: String 392 | Default: Count 393 | AllowedValues: 394 | - Count 395 | - Block 396 | 397 | **CoreRuleSetAMRAction** 398 | The WebACL deployed by Firewall Manager includes the AMR (Amazon Managed Rule) [CoreRuleSetAMRAction](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html#aws-managed-rule-groups-baseline-crs). The default action for this rule is COUNT. 399 | 400 | Required: No 401 | Type: String 402 | Default: Count 403 | AllowedValues: 404 | - Count 405 | - Block 406 | 407 | --- 408 | 409 | ## AWS Shield Advanced SRT Access **[Optional]** 410 | 411 | **SRTAccessRoleName** 412 | When SRT Access is granted that role will have this name. Depending on the value of SRTAccessRoleAction, this is either the name of the role that will be created, or that already exists and should be used. If none, is specified, CloudFormation will generate the name for the role. 413 | 414 | Required: No 415 | Type: String 416 | 417 | **SRTAccessRoleAction** 418 | When SRT Access is granted, is the role specified in SRTAccessRoleName a role that CloudFormation needs to create or that already exists. 419 | 420 | Required: No 421 | Type: String 422 | Default: CreateRole 423 | AllowedValues: 424 | - UseExisting 425 | - CreateRole 426 | 427 | **SRTBuckets** 428 | When SRT Access is granted, you can optionally grant access to one or more S3 buckets. This is not required for SRT to have access to WAF logs. 429 | 430 | Required: No 431 | Type: CommaDelimitedList 432 | Default: 433 | 434 | --- 435 | 436 | ## AWS Shield Advanced Proactive Engagement **[Optional]** 437 | 438 | **EmergencyContactEmail2** 439 | During a Proactive Engagement, SRT (Shield Response Team) will reach out to your emergency contacts. You must configure one or more emergency contacts along with enabling Proactive Engagement. Specify a valid e-mail address 440 | 441 | Required: Yes 442 | Type: String 443 | AllowedPattern: ^\S+@\S+\.\S+$|\ 444 | 445 | **EmergencyContactPhone2** 446 | During a Proactive Engagement, SRT (Shield Response Team) will reach out to your emergency contacts. You must configure one or more emergency contacts along with enabling Proactive Engagement. Specify a valid phone number + country code. e.g. \+ 15555555555 447 | 448 | Required: Yes 449 | Type: String 450 | AllowedPattern: ^\+[0-9]{11}|\ 451 | 452 | **EmergencyContactNote2** 453 | During a Proactive Engagement, SRT (Shield Response Team) will reach out to your emergency contacts. You must configure one or more emergency contacts along with enabling Proactive Engagement. Provide notes/comments about this contact. E.g. SOC 454 | 455 | Required: Yes 456 | Type: String 457 | AllowedPattern: ^[\w\s\.\-,:/()+@]*$|\ 458 | 459 | **EmergencyContactEmail3** 460 | During a Proactive Engagement, SRT (Shield Response Team) will reach out to your emergency contacts. You must configure one or more emergency contacts along with enabling Proactive Engagement. Specify a valid e-mail address 461 | 462 | Required: No 463 | Type: String 464 | AllowedPattern: ^\S+@\S+\.\S+$|\ 465 | 466 | **EmergencyContactPhone3** 467 | During a Proactive Engagement, SRT (Shield Response Team) will reach out to your emergency contacts. You must configure one or more emergency contacts along with enabling Proactive Engagement. Specify a valid phone number + country code. e.g. \+ 15555555555 468 | 469 | Required: No 470 | Type: String 471 | AllowedPattern: ^\+[0-9]{11}|\ 472 | 473 | **EmergencyContactNote3** 474 | During a Proactive Engagement, SRT (Shield Response Team) will reach out to your emergency contacts. You must configure one or more emergency contacts along with enabling Proactive Engagement. Provide notes/comments about this contact. E.g. SOC 475 | 476 | Required: No 477 | Type: String 478 | AllowedPattern: ^[\w\s\.\-,:/()+@]*$|\ 479 | 480 | **EmergencyContactEmail4** 481 | During a Proactive Engagement, SRT (Shield Response Team) will reach out to your emergency contacts. You must configure one or more emergency contacts along with enabling Proactive Engagement. Specify a valid e-mail address 482 | 483 | Required: No 484 | Type: String 485 | AllowedPattern: ^\S+@\S+\.\S+$|\ 486 | 487 | **EmergencyContactPhone4** 488 | During a Proactive Engagement, SRT (Shield Response Team) will reach out to your emergency contacts. You must configure one or more emergency contacts along with enabling Proactive Engagement. Specify a valid phone number + country code. e.g. \+ 15555555555 489 | 490 | Required: No 491 | Type: String 492 | AllowedPattern: ^\+[0-9]{11}|\ 493 | 494 | **EmergencyContactNote4** 495 | During a Proactive Engagement, SRT (Shield Response Team) will reach out to your emergency contacts. You must configure one or more emergency contacts along with enabling Proactive Engagement. Provide notes/comments about this contact. E.g. SOC 496 | 497 | Required: No 498 | Type: String 499 | AllowedPattern: ^[\w\s\.\-,:/()+@]*$|\ 500 | 501 | **EmergencyContactEmail5** 502 | During a Proactive Engagement, SRT (Shield Response Team) will reach out to your emergency contacts. You must configure one or more emergency contacts along with enabling Proactive Engagement. Specify a valid e-mail address 503 | 504 | Required: No 505 | Type: String 506 | AllowedPattern: ^\S+@\S+\.\S+$|\ 507 | 508 | **EmergencyContactPhone5** 509 | During a Proactive Engagement, SRT (Shield Response Team) will reach out to your emergency contacts. You must configure one or more emergency contacts along with enabling Proactive Engagement. Specify a valid phone number + country code. e.g. \+ 15555555555 510 | 511 | Required: No 512 | Type: String 513 | AllowedPattern: ^\+[0-9]{11}|\ 514 | 515 | **EmergencyContactNote5** 516 | During a Proactive Engagement, SRT (Shield Response Team) will reach out to your emergency contacts. You must configure one or more emergency contacts along with enabling Proactive Engagement. Provide notes/comments about this contact. E.g. SOC 517 | 518 | Required: No 519 | Type: String 520 | AllowedPattern: ^[\w\s\.\-,:/()+@]*$|\ 521 | 522 | --- 523 | 524 | ## Shield Resource Scoping **[Optional]** 525 | 526 | **ResourceTagUsage** 527 | If tags are used to scope with Firewall Manager Security Policies, are these tags what to include (in scope) or what to exclude (out of scope). 528 | 529 | Required: No 530 | Type: String 531 | AllowedValues: 532 | - Include 533 | - Exclude 534 | 535 | **ShieldScopeTagName1** 536 | For Firewall Manager Security policies for Shield protection, one or more tag names can be used to define in scope resources. If you only specify a scope tag name, resources with this tag and any value are in scope. 537 | 538 | Required: No 539 | Type: String 540 | Default: 541 | 542 | **ShieldScopeTagName2** 543 | For Firewall Manager Security policies for Shield protection, one or more tag names can be used to define in scope resources. If you only specify a scope tag name, resources with this tag and any value are in scope. 544 | 545 | Required: No 546 | Type: String 547 | Default: 548 | 549 | **ShieldScopeTagName3** 550 | For Firewall Manager Security policies for Shield protection, one or more tag names can be used to define in scope resources. If you only specify a scope tag name, resources with this tag and any value are in scope. 551 | 552 | Required: No 553 | Type: String 554 | Default: 555 | 556 | **ShieldScopeTagValue1** 557 | For Firewall Manager Security policies for Shield protection, one or more tag name/value can be used to define in scope resources. This defines the exact value for the corresponding tag to be considered in scope. 558 | 559 | Required: No 560 | Type: String 561 | Default: 562 | 563 | **ShieldScopeTagValue2** 564 | For Firewall Manager Security policies for Shield protection, one or more tag name/value can be used to define in scope resources. This defines the exact value for the corresponding tag to be considered in scope. 565 | 566 | Required: No 567 | Type: String 568 | Default: 569 | 570 | **ShieldScopeTagValue3** 571 | For Firewall Manager Security policies for Shield protection, one or more tag name/value can be used to define in scope resources. This defines the exact value for the corresponding tag to be considered in scope. 572 | 573 | Required: No 574 | Type: String 575 | Default: 576 | 577 | ## WAFv2 Resource Scoping **[Optional]** 578 | **WAFv2ScopeTagName1** 579 | For Firewall Manager Security policies for WAFv2 protection, one or more tag names can be used to define in scope resources. If you only specify a scope tag name, resources with this tag and any value are in scope. 580 | 581 | Required: No 582 | Type: String 583 | Default: 584 | 585 | **WAFv2ScopeTagName2** 586 | For Firewall Manager Security policies for WAFv2 protection, one or more tag names can be used to define in scope resources. If you only specify a scope tag name, resources with this tag and any value are in scope. 587 | 588 | Required: No 589 | Type: String 590 | Default: 591 | 592 | **WAFv2ScopeTagName3** 593 | For Firewall Manager Security policies for WAFv2 protection, one or more tag names can be used to define in scope resources. If you only specify a scope tag name, resources with this tag and any value are in scope. 594 | 595 | Required: No 596 | Type: String 597 | Default: 598 | 599 | **WAFv2ScopeTagValue1** 600 | For Firewall Manager Security policies for WAFv2 protection, one or more tag name/value can be used to define in scope resources. This defines the exact value for the corresponding tag to be considered in scope. 601 | 602 | Required: No 603 | Type: String 604 | Default: 605 | 606 | **WAFv2ScopeTagValue2** 607 | For Firewall Manager Security policies for WAFv2 protection, one or more tag name/value can be used to define in scope resources. This defines the exact value for the corresponding tag to be considered in scope. 608 | 609 | Required: No 610 | Type: String 611 | Default: 612 | 613 | **WAFv2ScopeTagValue3** 614 | For Firewall Manager Security policies for WAFv2 protection, one or more tag name/value can be used to define in scope resources. This defines the exact value for the corresponding tag to be considered in scope. 615 | 616 | Required: No 617 | Type: String 618 | Default: 619 | --------------------------------------------------------------------------------