# trussworks/wafv2/aws has a basic WAF with the AWS Managed Ruleset # See https://registry.terraform.io/modules/trussworks/wafv2/aws/latest # # Attempted to add some sane defaults so we can customize as needed # # IMPORTANT NOTES: # An 'Allow' action stops processing immediately. Avoid them! # Goals: # - US IPs only - https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-geo-match.html locals { waf_name = replace(var.fqdns[0], ".", "_") # A complicated building of managed rules. Each one builds on the previous. managed_rules_0 = [] managed_rules_1 = var.excluded_set_AWSManagedRulesCommonRuleSet ? local.managed_rules_0 : concat(local.managed_rules_0, [{ "excluded_rules" : var.excluded_rules_AWSManagedRulesCommonRuleSet, "name" : "AWSManagedRulesCommonRuleSet", "override_action" : var.block_settings["AWSManagedRulesCommonRuleSet"] ? "none" : "count", "priority" : 510 }]) managed_rules_2 = var.excluded_set_AWSManagedRulesAmazonIpReputationList ? local.managed_rules_1 : concat(local.managed_rules_1, [{ "excluded_rules" : var.excluded_rules_AWSManagedRulesAmazonIpReputationList, "name" : "AWSManagedRulesAmazonIpReputationList", "override_action" : var.block_settings["AWSManagedRulesAmazonIpReputationList"] ? "none" : "count", "priority" : 520 }]) managed_rules_3 = var.excluded_set_AWSManagedRulesKnownBadInputsRuleSet ? local.managed_rules_2 : concat(local.managed_rules_2, [{ "excluded_rules" : var.excluded_rules_AWSManagedRulesKnownBadInputsRuleSet, "name" : "AWSManagedRulesKnownBadInputsRuleSet", "override_action" : var.block_settings["AWSManagedRulesKnownBadInputsRuleSet"] ? "none" : "count", "priority" : 530 }]) managed_rules_4 = var.excluded_set_AWSManagedRulesSQLiRuleSet ? local.managed_rules_3 : concat(local.managed_rules_3, [{ "excluded_rules" : var.excluded_rules_AWSManagedRulesSQLiRuleSet, "name" : "AWSManagedRulesSQLiRuleSet", "override_action" : var.block_settings["AWSManagedRulesSQLiRuleSet"] ? "none" : "count", "priority" : 540 }]) managed_rules_5 = var.excluded_set_AWSManagedRulesLinuxRuleSet ? local.managed_rules_4 : concat(local.managed_rules_4, [{ "excluded_rules" : var.excluded_rules_AWSManagedRulesLinuxRuleSet, "name" : "AWSManagedRulesLinuxRuleSet", "override_action" : var.block_settings["AWSManagedRulesLinuxRuleSet"] ? "none" : "count", "priority" : 550 }]) managed_rules_6 = var.excluded_set_AWSManagedRulesUnixRuleSet ? local.managed_rules_5 : concat(local.managed_rules_5, [{ "excluded_rules" : var.excluded_rules_AWSManagedRulesUnixRuleSet, "name" : "AWSManagedRulesUnixRuleSet", "override_action" : var.block_settings["AWSManagedRulesUnixRuleSet"] ? "none" : "count", "priority" : 560 }]) managed_rules = local.managed_rules_6 } resource "aws_wafv2_ip_set" "blocked" { name = "${local.waf_name}_blocked_ips" scope = "REGIONAL" ip_address_version = "IPV4" addresses = toset(concat(var.additional_blocked_ips, local.blocked_ips)) lifecycle { create_before_destroy = true } } resource "aws_wafv2_ip_set" "allowed" { name = "${local.waf_name}_allowed_ips" scope = "REGIONAL" ip_address_version = "IPV4" addresses = var.allowed_ips lifecycle { create_before_destroy = true } } resource "aws_wafv2_ip_set" "admin" { name = "${local.waf_name}_admin_ips" scope = "REGIONAL" ip_address_version = "IPV4" addresses = var.admin_ips lifecycle { create_before_destroy = true } } resource "aws_wafv2_rule_group" "xdr_custom_rules" { name = "${local.waf_name}_xdr_custom_rules_rev4" # update name when updating scope = "REGIONAL" capacity = 60 # Note, there is visibilty config for the group and for the rule visibility_config { cloudwatch_metrics_enabled = true metric_name = "${local.waf_name}_xdr_custom_rules" #metric_name = "xdr_custom_rules" sampled_requests_enabled = true } rule { name = "Block_Nonpermitted_Countries" priority = 100 action { # WAF rule's strange data format makes this a little complex, # but the end result is that if block_settings["custom"] is # set to true, it will block. Otherwise, it will count. dynamic "block" { for_each = var.block_settings["custom"] ? ["block"] : [] content {} } dynamic "count" { for_each = var.block_settings["custom"] ? [] : ["count"] content {} } } statement { not_statement { statement { geo_match_statement { country_codes = [ "US", "DE", ] } } } } visibility_config { cloudwatch_metrics_enabled = true metric_name = "Block_Nonpermitted_Countries" sampled_requests_enabled = true } # rule_label { # name = "xdr_custom:nonpermittedcountry" # } } rule { name = "Block_Admin_Unless_Permitted" priority = 110 action { # WAF rule's strange data format makes this a little complex, # but the end result is that if block_settings["custom"] is # set to true, it will block. Otherwise, it will count. dynamic "block" { for_each = var.block_settings["admin"] ? ["block"] : [] content {} } dynamic "count" { for_each = var.block_settings["admin"] ? [] : ["count"] content {} } } statement { and_statement { statement { not_statement { statement { ip_set_reference_statement { arn = aws_wafv2_ip_set.admin.arn } } } } statement { byte_match_statement { field_to_match { uri_path {} } positional_constraint = "STARTS_WITH" search_string = "/admin" text_transformation { priority = 1 type = "URL_DECODE" } text_transformation { priority = 2 type = "LOWERCASE" } } } } } visibility_config { cloudwatch_metrics_enabled = true metric_name = "Block_Unallowed_Admin_Access" sampled_requests_enabled = true } # rule_label { # name = "xdr_custom:nonpermittedcountry" # } } # rule { # name = "Block_log4j_Exploit_20211210" # action { # block {} # } # priority = 110 # # #rule_label { # # name = "xdr_custom:log4j" # #} # # visibility_config { # cloudwatch_metrics_enabled = true # metric_name = "Block_Log4j_exploit_20211210" # sampled_requests_enabled = true # } # # statement { # or_statement { # statement { # byte_match_statement { # field_to_match { # single_header { # name = "user-agent" # } # } # positional_constraint = "STARTS_WITH" # search_string = "$${jndi:" # ldap://" # # text_transformation { # priority = 1 # type = "BASE64_DECODE" # } # # #text_transformation { # # priority = 3 # # type = "HEX_DECODE" # #} # # text_transformation { # priority = 5 # type = "LOWERCASE" # } # } # } # ## statement { ## byte_match_statement { ## field_to_match { ## method {} ## } ## positional_constraint = "STARTS_WITH" ## search_string = "$${jndi:" # ldap://" ## ## text_transformation { ## priority = 1 ## type = "BASE64_DECODE" ## } ## ## text_transformation { ## priority = 3 ## type = "HEX_DECODE" ## } ## ## text_transformation { ## priority = 5 ## type = "LOWERCASE" ## } ## } ## } ## ## statement { ## byte_match_statement { ## field_to_match { ## query_string {} ## } ## positional_constraint = "CONTAINS" ## search_string = "$${jndi:" # ldap://" ## ## text_transformation { ## priority = 1 ## type = "BASE64_DECODE" ## } ## ## #text_transformation { ## # priority = 3 ## # type = "HEX_DECODE" ## #} ## ## text_transformation { ## priority = 5 ## type = "LOWERCASE" ## } ## } ## } # # statement { # byte_match_statement { # field_to_match { # uri_path {} # } # positional_constraint = "CONTAINS" # search_string = "$${jndi:" # ldap://" # # text_transformation { # priority = 1 # type = "BASE64_DECODE" # } # # #text_transformation { # # priority = 3 # # type = "HEX_DECODE" # #} # # text_transformation { # priority = 5 # type = "LOWERCASE" # } # } # } # } # } # } # Add additional custom rules here lifecycle { create_before_destroy = true } } module "wafv2" { source = "trussworks/wafv2/aws" version = "= 2.4.0" name = local.waf_name scope = "REGIONAL" alb_arn = var.resource_arn associate_alb = true default_action = var.block_settings["default"] ? "block" : "allow" # Note: Even in block, the final action is actually to 'allow' if the host header is correct filtered_header_rule = { header_types = var.fqdns header_value = "host" priority = 900 action = "allow" } # IP based rules are processed first. # If an IP is blocked, it is blocked no matter what. # If an IP is allowed, it is allowed only if it's not blocked, but no other WAF rules are processed. ip_sets_rule = [ { name = "blocked_ips" action = "block" priority = 10 ip_set_arn = aws_wafv2_ip_set.blocked.arn }, { name = "allowed_ips" action = "allow" priority = 20 ip_set_arn = aws_wafv2_ip_set.allowed.arn } ] # Custom Rules are defined above group_rules = [ { name = aws_wafv2_rule_group.xdr_custom_rules.name arn = aws_wafv2_rule_group.xdr_custom_rules.arn priority = 100 override_action = var.block_settings["custom"] ? "none" : "count" excluded_rules = [] } ] # A rate-based rule tracks the rate of requests for each originating IP address, and triggers the rule action when the rate exceeds a limit that you specify on the number of requests in any 5-minute time span # Because of zscalar, this is completely ineffective for us. #ip_rate_based_rule = { # name = "Rate_Limit" # priority = 200 # limit = 3000 # 6000 requests per 5 minutes= 10 requests/second (sustained for 5 minutes) # action = "block" #} # AWS managed rulesets # Baseline was from trussworks/wafv2/aws, but copied here to be customized for our use and renumbered. managed_rules = local.managed_rules depends_on = [aws_wafv2_rule_group.xdr_custom_rules] tags = var.tags } resource "aws_wafv2_web_acl_logging_configuration" "waf_logs" { log_destination_configs = ["arn:${var.aws_partition}:firehose:${var.aws_region}:${var.aws_account_id}:deliverystream/aws-waf-logs-splunk"] resource_arn = module.wafv2.web_acl_id # logging_filter { # default_behavior = "KEEP" # # filter { # behavior = "DROP" # # condition { # action_condition { # action = "COUNT" # } # } # # condition { # label_name_condition { # label_name = "awswaf:111122223333:rulegroup:testRules:LabelNameZ" # } # } # # requirement = "MEETS_ALL" # } # # filter { # behavior = "KEEP" # # condition { # action_condition { # action = "ALLOW" # } # } # # requirement = "MEETS_ANY" # } # } }