From 034196a33f5a1550523ee01cebd112cee02528d0 Mon Sep 17 00:00:00 2001 From: "Michael B. Klein" Date: Thu, 4 Jan 2024 20:56:56 +0000 Subject: [PATCH] Add challenge for browser-based requests --- firewall/README.md | 5 +- firewall/ip_address_sets.tf | 13 +--- firewall/ip_firewall.tf | 24 +----- firewall/main.tf | 5 +- firewall/nul_ips.tf | 20 +++++ firewall/security_firewall.tf | 136 ++++++++++++++++++---------------- firewall/variables.tf | 16 +--- monitoring/README.md | 2 - 8 files changed, 107 insertions(+), 114 deletions(-) create mode 100644 firewall/nul_ips.tf diff --git a/firewall/README.md b/firewall/README.md index a102334..cfe28a4 100644 --- a/firewall/README.md +++ b/firewall/README.md @@ -4,9 +4,10 @@ This terraform project includes the resources required to set up the WAF ACL(s) ## Secrets +* `allowed_user_agents` – The list of user agents to allow through without bot control * `firewall_type` – The type of firewall to create (`IP` for NUL IPs, `SECURITY` for managed security rulesets) -* `nul_ips` – A list of IP ranges representing the NUL staff offices and VPN -* `rdc_home_ips` – A list of IP addresses representing home offices of NUL RDC staffers for convenience +* `global_rate_limit` – Rate limit (# requests per 5 minutes) for clients not caught by any other rule +* `high_traffic_ips` – Known high-traffic IPs to block * `resources` – A map of indicating the resources to be protected * Example: `{ name = "my-app", arn = "arn:aws:elasticloadbalancing:..." }` diff --git a/firewall/ip_address_sets.tf b/firewall/ip_address_sets.tf index e470722..d10775a 100644 --- a/firewall/ip_address_sets.tf +++ b/firewall/ip_address_sets.tf @@ -3,7 +3,7 @@ resource "aws_wafv2_ip_set" "nul_ip_set" { description = "NU Library IPv4 Addresses" scope = "REGIONAL" ip_address_version = "IPV4" - addresses = var.nul_ips + addresses = local.nul_ips tags = local.tags } @@ -12,16 +12,7 @@ resource "aws_wafv2_ip_set" "nul_ipv6_set" { description = "NU Library IPv6 Addresses" scope = "REGIONAL" ip_address_version = "IPV6" - addresses = var.nul_ips_v6 - tags = local.tags -} - -resource "aws_wafv2_ip_set" "rdc_home_ip_set" { - name = "rdc-home-ips" - description = "Home IP Addresses of RDC Users" - scope = "REGIONAL" - ip_address_version = "IPV4" - addresses = var.rdc_home_ips + addresses = local.nul_ips_v6 tags = local.tags } diff --git a/firewall/ip_firewall.tf b/firewall/ip_firewall.tf index d01b170..70e5bca 100644 --- a/firewall/ip_firewall.tf +++ b/firewall/ip_firewall.tf @@ -1,5 +1,5 @@ resource "aws_wafv2_web_acl" "ip_firewall" { - count = var.firewall_type == "IP" ? 1 : 0 + count = local.ip_firewall ? 1 : 0 name = "staging-ip-acl" description = "Protect staging resources using IP restrictions" scope = "REGIONAL" @@ -50,26 +50,6 @@ resource "aws_wafv2_web_acl" "ip_firewall" { sampled_requests_enabled = true } } - rule { - name = "allow-rdc-home-ips" - priority = 3 - - action { - allow {} - } - - statement { - ip_set_reference_statement { - arn = aws_wafv2_ip_set.rdc_home_ip_set.arn - } - } - - visibility_config { - cloudwatch_metrics_enabled = false - metric_name = "Allow_RDC_Home_IPs" - sampled_requests_enabled = true - } - } visibility_config { cloudwatch_metrics_enabled = false @@ -79,7 +59,7 @@ resource "aws_wafv2_web_acl" "ip_firewall" { } resource "aws_wafv2_web_acl_association" "ip_firewall" { - for_each = var.firewall_type == "IP" ? var.resources : {} + for_each = local.ip_firewall ? var.resources : {} resource_arn = each.value web_acl_arn = aws_wafv2_web_acl.ip_firewall[0].arn } \ No newline at end of file diff --git a/firewall/main.tf b/firewall/main.tf index ac23137..23d0a70 100644 --- a/firewall/main.tf +++ b/firewall/main.tf @@ -17,7 +17,10 @@ module "core" { } locals { - namespace = module.core.outputs.stack.namespace + namespace = module.core.outputs.stack.namespace + ip_firewall = var.firewall_type == "IP" + security_firewall = var.firewall_type == "SECURITY" + tags = merge( module.core.outputs.stack.tags, { diff --git a/firewall/nul_ips.tf b/firewall/nul_ips.tf new file mode 100644 index 0000000..5e2dc83 --- /dev/null +++ b/firewall/nul_ips.tf @@ -0,0 +1,20 @@ +locals { + nul_ips = [ + "129.105.184.0/24", + "129.105.19.0/24", + "129.105.203.0/24", + "129.105.29.0/24", + "165.124.126.38/32", + "165.124.144.0/23", + "165.124.160.0/21", + "165.124.199.32/29", + "165.124.200.24/29", + "165.124.201.96/28", + "165.124.202.0/24" + ] + nul_ips_v6 = [ + "2620:10d:2000:3000:0:0224::/96", + "2620:10d:2000:3000:0:0078::/96", + "2620:10d:2000:3000:0:0077::/96" + ] +} \ No newline at end of file diff --git a/firewall/security_firewall.tf b/firewall/security_firewall.tf index cf8459e..10e9433 100644 --- a/firewall/security_firewall.tf +++ b/firewall/security_firewall.tf @@ -1,5 +1,4 @@ locals { - count_only = var.firewall_type != "SECURITY" excluded_rules = { AWSManagedRulesCommonRuleSet = ["CrossSiteScripting_BODY", "GenericRFI_BODY", "SizeRestrictions_BODY"] AWSManagedRulesKnownBadInputsRuleSet = [] @@ -14,6 +13,7 @@ resource "aws_cloudwatch_log_group" "security_firewall_log" { } resource "aws_wafv2_web_acl" "security_firewall" { + count = local.security_firewall ? 1 : 0 name = "${local.namespace}-load-balancer-firewall" scope = "REGIONAL" tags = local.tags @@ -32,10 +32,15 @@ resource "aws_wafv2_web_acl" "security_firewall" { name = "${local.namespace}-allow-nul-ips" priority = 0 + rule_label { + name = "nul:internal-ip:v4" + } + action { - allow {} + count {} } + statement { ip_set_reference_statement { arn = aws_wafv2_ip_set.nul_ip_set.arn @@ -43,7 +48,7 @@ resource "aws_wafv2_web_acl" "security_firewall" { } visibility_config { - cloudwatch_metrics_enabled = false + cloudwatch_metrics_enabled = true metric_name = "${local.namespace}-allow-nul-ips" sampled_requests_enabled = true } @@ -53,8 +58,12 @@ resource "aws_wafv2_web_acl" "security_firewall" { name = "${local.namespace}-${local.namespace}-allow-nul-ips-v6" priority = 1 + rule_label { + name = "nul:internal-ip:v6" + } + action { - allow {} + count {} } statement { @@ -64,7 +73,7 @@ resource "aws_wafv2_web_acl" "security_firewall" { } visibility_config { - cloudwatch_metrics_enabled = false + cloudwatch_metrics_enabled = true metric_name = "${local.namespace}-allow-nul-ips" sampled_requests_enabled = true } @@ -143,7 +152,6 @@ resource "aws_wafv2_web_acl" "security_firewall" { search_string = "/api/" field_to_match { - uri_path {} } @@ -164,19 +172,11 @@ resource "aws_wafv2_web_acl" "security_firewall" { } rule { - name = "AmazonIPReputationList" + name = "${local.namespace}-aws-managed-ip-reputation-list" priority = 4 override_action { - dynamic "none" { - for_each = toset(local.count_only ? [] : [1]) - content {} - } - - dynamic "count" { - for_each = toset(local.count_only ? [1] : []) - content {} - } + none {} } statement { @@ -194,19 +194,11 @@ resource "aws_wafv2_web_acl" "security_firewall" { } rule { - name = "AWSManagedRulesBotControlRuleSet" + name = "${local.namespace}-aws-managed-bot-control" priority = 5 override_action { - dynamic "none" { - for_each = toset(local.count_only ? [] : [1]) - content {} - } - - dynamic "count" { - for_each = toset(local.count_only ? [1] : []) - content {} - } + none {} } statement { @@ -240,15 +232,7 @@ resource "aws_wafv2_web_acl" "security_firewall" { priority = 6 action { - dynamic "block" { - for_each = toset(local.count_only ? [] : [1]) - content {} - } - - dynamic "count" { - for_each = toset(local.count_only ? [1] : []) - content {} - } + block {} } statement { @@ -264,11 +248,45 @@ resource "aws_wafv2_web_acl" "security_firewall" { } } - # Block requests from a single IP exceeding 750 requests per 5 minute period + # Challenge browsers that exceed the rate limit rule { - name = "${local.namespace}-rate-limiter" + name = "${local.namespace}-browser-rate-limiter" priority = 7 + action { + challenge {} + } + + statement { + rate_based_statement { + aggregate_key_type = "IP" + limit = var.global_rate_limit + + scope_down_statement { + not_statement { + statement { + label_match_statement { + scope = "LABEL" + key = "awswaf:managed:aws:bot-control:bot:category:http_library" + } + } + } + } + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "${local.namespace}-rate-limiter" + sampled_requests_enabled = true + } + } + + # Rate limit (HTTP status 429) HTTP client libraries that exceed the rate limit + rule { + name = "${local.namespace}-http-client-rate-limiter" + priority = 8 + action { block { custom_response { @@ -281,7 +299,14 @@ resource "aws_wafv2_web_acl" "security_firewall" { statement { rate_based_statement { aggregate_key_type = "IP" - limit = 300 + limit = var.global_rate_limit + + scope_down_statement { + label_match_statement { + scope = "LABEL" + key = "awswaf:managed:aws:bot-control:bot:category:http_library" + } + } } } @@ -293,19 +318,11 @@ resource "aws_wafv2_web_acl" "security_firewall" { } rule { - name = "AWSManagedRulesCommonRuleSet" - priority = 8 + name = "${local.namespace}-aws-managed-managed-common" + priority = 9 override_action { - dynamic "none" { - for_each = toset(local.count_only ? [] : [1]) - content {} - } - - dynamic "count" { - for_each = toset(local.count_only ? [1] : []) - content {} - } + none {} } statement { @@ -335,19 +352,11 @@ resource "aws_wafv2_web_acl" "security_firewall" { } rule { - name = "AWSManagedRulesKnownBadInputsRuleSet" - priority = 9 + name = "${local.namespace}-aws-managed-known-bad-inputs" + priority = 10 override_action { - dynamic "none" { - for_each = toset(local.count_only ? [] : [1]) - content {} - } - - dynamic "count" { - for_each = toset(local.count_only ? [1] : []) - content {} - } + none {} } statement { @@ -380,8 +389,9 @@ resource "aws_wafv2_web_acl" "security_firewall" { } resource "aws_wafv2_web_acl_logging_configuration" "security_firewall" { + count = local.security_firewall ? 1 : 0 log_destination_configs = [aws_cloudwatch_log_group.security_firewall_log.arn] - resource_arn = aws_wafv2_web_acl.security_firewall.arn + resource_arn = aws_wafv2_web_acl.security_firewall[0].arn logging_filter { default_behavior = "KEEP" @@ -400,7 +410,7 @@ resource "aws_wafv2_web_acl_logging_configuration" "security_firewall" { } resource "aws_wafv2_web_acl_association" "security_firewall" { - for_each = var.firewall_type == "SECURITY" ? var.resources : {} + for_each = local.security_firewall ? var.resources : {} resource_arn = each.value - web_acl_arn = aws_wafv2_web_acl.security_firewall.arn + web_acl_arn = aws_wafv2_web_acl.security_firewall[0].arn } diff --git a/firewall/variables.tf b/firewall/variables.tf index 1c07c16..8319904 100644 --- a/firewall/variables.tf +++ b/firewall/variables.tf @@ -13,19 +13,9 @@ variable "high_traffic_ips" { default = [] } -variable "nul_ips" { - type = list - default = [] -} - -variable "nul_ips_v6" { - type = list - default = [] -} - -variable "rdc_home_ips" { - type = list - default = [] +variable "global_rate_limit" { + type = number + default = 1000 } variable "resources" { diff --git a/monitoring/README.md b/monitoring/README.md index 566fd75..9f3e682 100644 --- a/monitoring/README.md +++ b/monitoring/README.md @@ -7,8 +7,6 @@ This terraform project creates CloudWatch alarms to monitor stack resources and * `actions_enabled` – Boolean value indicating whether to enable actions for alarms * `alarm_actions` – The list of ARNS to be notified in an alarm state * `load_balancers` – The names of the application load balancers to monitor -* `nul_ips` – A list of IP ranges representing the NUL staff offices and VPN -* `rdc_home_ips` – A list of IP addresses representing home offices of NUL RDC staffers for convenience * `services` – A map of indicating which services to monitor * Example: `{ "meadow" = ["meadow"], "arch" = ["arch-webapp", "arch-worker"] }`