diff --git a/src/azul/__init__.py b/src/azul/__init__.py index 9c8cfaf8b..c91710879 100644 --- a/src/azul/__init__.py +++ b/src/azul/__init__.py @@ -1503,6 +1503,8 @@ def docker_images(self) -> dict[str, str]: waf_rate_rule_name = 'RateRule' + waf_expensive_rate_rule_name = 'ExpensiveRateRule' + config: Config = Config() # yes, the type hint does help PyCharm diff --git a/terraform/api_gateway.tf.json.template.py b/terraform/api_gateway.tf.json.template.py index c631452b8..57a9cb912 100644 --- a/terraform/api_gateway.tf.json.template.py +++ b/terraform/api_gateway.tf.json.template.py @@ -204,6 +204,34 @@ def for_domain(cls, domain): 'rule': [ { 'priority': 0, + 'name': 'LabelPostRequests', + 'action': { + 'count': {} + }, + 'rule_label': { + 'name': 'azul:expensive' + }, + 'statement': { + 'byte_match_statement': { + 'field_to_match': { + 'method': {} + }, + 'positional_constraint': 'EXACTLY', + 'search_string': 'POST', + 'text_transformation': { + 'priority': 0, + 'type': 'NONE' + } + } + }, + 'visibility_config': { + 'metric_name': 'LabelPostRequests', + 'sampled_requests_enabled': True, + 'cloudwatch_metrics_enabled': True + } + }, + { + 'priority': 1, 'name': 'BlockedIPs', 'action': { 'block': {} @@ -220,7 +248,31 @@ def for_domain(cls, domain): } }, { - 'priority': 1, + 'priority': 2, + 'name': config.waf_expensive_rate_rule_name, + 'action': { + 'block': {} + }, + 'statement': { + 'rate_based_statement': { + 'limit': 100, # limit must be between 100 and 20,000,000 + 'aggregate_key_type': 'IP', + 'scope_down_statement': { + 'label_match_statement': { + 'scope': 'LABEL', + 'key': 'azul:expensive' + } + } + } + }, + 'visibility_config': { + 'metric_name': config.waf_expensive_rate_rule_name, + 'sampled_requests_enabled': True, + 'cloudwatch_metrics_enabled': True + } + }, + { + 'priority': 3, 'name': config.waf_rate_rule_name, 'action': { 'block': {} @@ -228,7 +280,7 @@ def for_domain(cls, domain): 'statement': { 'rate_based_statement': { 'limit': 1000, # limit must be between 100 and 20,000,000 - 'aggregate_key_type': 'IP' + 'aggregate_key_type': 'IP', } }, 'visibility_config': { @@ -238,7 +290,7 @@ def for_domain(cls, domain): } }, { - 'priority': 2, + 'priority': 4, 'name': 'AWS-CommonRuleSet', 'override_action': { 'none': {} @@ -274,7 +326,7 @@ def for_domain(cls, domain): } }, { - 'priority': 3, + 'priority': 5, 'name': 'AWS-AmazonIpReputationList', 'override_action': { 'none': {} @@ -292,7 +344,7 @@ def for_domain(cls, domain): } }, { - 'priority': 4, + 'priority': 6, 'name': 'AWS-UnixRuleSet', 'override_action': { 'none': {} diff --git a/terraform/cloudwatch.tf.json.template.py b/terraform/cloudwatch.tf.json.template.py index 9c9356cc8..d72fb3be1 100644 --- a/terraform/cloudwatch.tf.json.template.py +++ b/terraform/cloudwatch.tf.json.template.py @@ -367,6 +367,25 @@ def prod_qualified_resource_name(name: str) -> str: }, 'alarm_actions': ['${data.aws_sns_topic.monitoring.arn}'], 'ok_actions': ['${data.aws_sns_topic.monitoring.arn}'], + }, + 'waf_expensive_rate_blocked': { + 'alarm_name': config.qualified_resource_name('waf_expensive_rate_blocked'), + 'comparison_operator': 'GreaterThanThreshold', + 'threshold': 0, + 'datapoints_to_alarm': 1, + 'evaluation_periods': 1, + 'period': 5 * 60, + 'metric_name': 'BlockedExpensiveRequests', + 'namespace': 'AWS/WAFV2', + 'statistic': 'Sum', + 'treat_missing_data': 'notBreaching', + 'dimensions': { + 'WebACL': '${aws_wafv2_web_acl.api_gateway.name}', + 'Region': config.region, + 'Rule': config.waf_expensive_rate_rule_name + }, + 'alarm_actions': ['${data.aws_sns_topic.monitoring.arn}'], + 'ok_actions': ['${data.aws_sns_topic.monitoring.arn}'], } } }