123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445 |
- # 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"
- # }
- # }
- }
|