From 0b6e7ff6b424b21da7650adaffe34308c8912723 Mon Sep 17 00:00:00 2001 From: Sean Rankine Date: Wed, 22 Jan 2025 16:49:55 +0000 Subject: [PATCH] Add backend WAFs --- .../variables.tf | 26 ++ .../govuk-publishing-infrastructure/wafs.tf | 229 ++++++++++++++++++ 2 files changed, 255 insertions(+) diff --git a/terraform/deployments/govuk-publishing-infrastructure/variables.tf b/terraform/deployments/govuk-publishing-infrastructure/variables.tf index 6d07efcdc..82c0264cd 100644 --- a/terraform/deployments/govuk-publishing-infrastructure/variables.tf +++ b/terraform/deployments/govuk-publishing-infrastructure/variables.tf @@ -103,3 +103,29 @@ variable "amazonmq_govuk_chat_retry_message_ttl" { default = 300000 description = "Time in miliseconds before messages in the govuk_chat_retry queue expires and are sent back to the govuk_chat_published_ducoments queue through the dead letter mechanism" } + +variable "allow_high_request_rate_from_cidrs" { + type = list(string) + description = "List of CIDRs from which we allow a higher ratelimit." +} + +variable "backend_public_base_rate_warning" { + type = number + description = "A warning rate limit threshold for the backend public web ACL" +} + +variable "backend_public_base_rate_limit" { + type = number + description = "An enforced rate limit threshold for the backend public web ACL" +} + +variable "backend_public_ja3_denylist" { + type = list(string) + description = "For the backend ALB. List of JA3 signatures for which we should block all requests." +} + +variable "waf_log_retention_days" { + type = string + description = "The number of days CloudWatch will retain WAF logs for." + default = "30" +} diff --git a/terraform/deployments/govuk-publishing-infrastructure/wafs.tf b/terraform/deployments/govuk-publishing-infrastructure/wafs.tf index 8000a4ba8..29c1650ec 100644 --- a/terraform/deployments/govuk-publishing-infrastructure/wafs.tf +++ b/terraform/deployments/govuk-publishing-infrastructure/wafs.tf @@ -94,3 +94,232 @@ resource "aws_wafv2_rule_group" "x_always_block" { sampled_requests_enabled = false } } + +resource "aws_wafv2_ip_set" "govuk_requesting_ips" { + name = "govuk_requesting_ips" + description = "The IP addresses used by our infra to make requests that hit the cache LB." + scope = "REGIONAL" + ip_address_version = "IPV4" + addresses = formatlist("%s/32", data.tfe_outputs.cluster_infrastructure.nonsensitive_values.public_nat_gateway_ips) +} + +resource "aws_wafv2_ip_set" "high_request_rate" { + name = "high_request_rate" + description = "Source addresses from which we allow a higher ratelimit." + scope = "REGIONAL" + ip_address_version = "IPV4" + addresses = var.allow_high_request_rate_from_cidrs +} + +resource "aws_wafv2_web_acl" "backend_public" { + name = "backend_public_web_acl" + scope = "REGIONAL" + + default_action { + allow {} + } + + # this rule matches any request that contains the header X-Always-Block: true + # we use it as a simple sanity check / acceptance test from smokey to ensure that + # the waf is enabled and processing requests + rule { + name = "x-always-block_web_acl_rule" + priority = 10 + + override_action { + none {} + } + + statement { + rule_group_reference_statement { + arn = aws_wafv2_rule_group.x_always_block.arn + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "x-always-block-rule-group" + sampled_requests_enabled = true + } + } + + rule { + name = "rate-limit-exemptions" + priority = 20 + + action { + allow {} + } + + statement { + or_statement { + statement { + ip_set_reference_statement { + arn = aws_wafv2_ip_set.govuk_requesting_ips.arn + } + } + statement { + ip_set_reference_statement { + arn = aws_wafv2_ip_set.high_request_rate.arn + } + } + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "govuk-infra-backend-requests" + sampled_requests_enabled = true + } + } + + # This rule is intended for monitoring only + # set a base rate limit per IP looking back over the last 5 minutes + # this is checked every 30s + rule { + name = "backend-public-base-rate-warning" + priority = 30 + + action { + count {} + } + + statement { + rate_based_statement { + limit = var.backend_public_base_rate_warning + aggregate_key_type = "IP" + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "backend-public-base-rate-warning" + sampled_requests_enabled = true + } + } + + # set a base rate limit per IP looking back over the last 5 minutes + # this is checked every 30s + rule { + name = "backend-public-base-rate-limit" + priority = 40 + + action { + block { + custom_response { + response_code = 429 + + response_header { + name = "Retry-After" + value = 30 + } + + response_header { + name = "Cache-Control" + value = "max-age=0, private" + } + + custom_response_body_key = "backend-public-rule-429" + } + } + } + + statement { + rate_based_statement { + limit = var.backend_public_base_rate_limit + aggregate_key_type = "IP" + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "backend-public-base-rate-limit" + sampled_requests_enabled = true + } + } + + dynamic "rule" { + for_each = var.backend_public_ja3_denylist + iterator = signature + + content { + name = "deny-ja3-${signature.value}" + + # All rules require a unique priority, and the size of the JA3 denylist is potentially unbounded, + # so we add these rules to the end of the list to avoid collisions. + priority = 50 + signature.key + + action { + block {} + } + + statement { + byte_match_statement { + positional_constraint = "EXACTLY" + search_string = signature.value + + field_to_match { + ja3_fingerprint { + fallback_behavior = "NO_MATCH" + } + } + + text_transformation { + type = "NONE" + priority = 0 + } + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "deny-ja3-${signature.value}" + sampled_requests_enabled = true + } + } + } + + custom_response_body { + key = "backend-public-rule-429" + content = < + + + Welcome to GOV.UK + + + +

GOV.UK

+

Sorry, there have been too many attempts to access this page.

+

Try again in a few minutes.

+ + + HTML + + content_type = "TEXT_HTML" + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "backend-public-web-acl" + sampled_requests_enabled = true + } +} + +resource "aws_cloudwatch_log_group" "public_backend_waf" { + # the name must start with aws-waf-logs + # https://docs.aws.amazon.com/waf/latest/developerguide/logging-cw-logs.html#logging-cw-logs-naming + name = "aws-waf-logs-backend-public-${var.aws_environment}" + retention_in_days = var.waf_log_retention_days + +} + +resource "aws_wafv2_web_acl_logging_configuration" "public_backend_waf" { + log_destination_configs = [aws_cloudwatch_log_group.public_backend_waf.arn] + resource_arn = aws_wafv2_web_acl.backend_public.arn +}