Skip to content

Commit 25b0017

Browse files
OlamideOl1claude
andcommitted
Add custom block response to host_uri_rate_limit_rules
Optional per-rule block_response_code (e.g. 429), block_retry_after_seconds (Retry-After header), and block_response_body_json (APPLICATION_JSON body via a keyed WebACL custom_response_body). Defaults keep the plain 403 block, so existing rules are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 3476cbd commit 25b0017

2 files changed

Lines changed: 41 additions & 1 deletion

File tree

aws/waf/main.tf

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ resource "aws_wafv2_web_acl" "main" {
1313
metric_name = "${var.name}-cloudfront-web-acl"
1414
}
1515

16+
# Custom JSON bodies for host_uri_rate_limit_rules whose block action returns one.
17+
# Keyed by the rule's map key and referenced from the rule's block custom_response.
18+
dynamic "custom_response_body" {
19+
for_each = { for k, r in var.host_uri_rate_limit_rules : k => r if r.block_response_body_json != null }
20+
content {
21+
key = custom_response_body.key
22+
content = custom_response_body.value.block_response_body_json
23+
content_type = "APPLICATION_JSON"
24+
}
25+
}
26+
1627
dynamic "rule" {
1728
for_each = var.header_match_rules == null ? {} : var.header_match_rules
1829
content {
@@ -366,7 +377,23 @@ resource "aws_wafv2_web_acl" "main" {
366377
dynamic "action" {
367378
for_each = rule.value["count_override"] == false ? [1] : []
368379
content {
369-
block {}
380+
block {
381+
dynamic "custom_response" {
382+
for_each = rule.value["block_response_code"] != null ? [1] : []
383+
content {
384+
response_code = rule.value["block_response_code"]
385+
custom_response_body_key = rule.value["block_response_body_json"] != null ? rule.key : null
386+
387+
dynamic "response_header" {
388+
for_each = rule.value["block_retry_after_seconds"] != null ? [1] : []
389+
content {
390+
name = "Retry-After"
391+
value = tostring(rule.value["block_retry_after_seconds"])
392+
}
393+
}
394+
}
395+
}
396+
}
370397
}
371398
}
372399

aws/waf/variables.tf

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ variable "host_uri_rate_limit_rules" {
9595
uri_paths = optional(list(string), []) # URI path(s) to scope the rate limit to. With REGEX these are regex patterns. Empty (default) rate-limits the whole host.
9696
uri_match_type = optional(string, "STARTS_WITH") # How to match uri_paths: EXACTLY (pin one endpoint), STARTS_WITH (prefix), or REGEX (for variable segments, e.g. "^/api/v1/users/[^/]+/validation$"). REGEX is case-sensitive (URL-decoded, not lowercased); use an inline (?i) flag for case-insensitivity.
9797
count_override = optional(bool, false) # If true, override the action to `count` (dry run). If false (default), the action is `block` when the limit is exceeded.
98+
99+
# Optional custom block response (only applied when the action is `block`, i.e. count_override = false).
100+
block_response_code = optional(number) # HTTP status returned to blocked clients, e.g. 429. Null (default) => WAF's default 403. Setting this is what enables the custom response.
101+
block_retry_after_seconds = optional(number) # If set (requires block_response_code), adds a `Retry-After: <n>` response header.
102+
block_response_body_json = optional(string) # If set (requires block_response_code), returns this string as an APPLICATION_JSON body (<= 4096 bytes).
98103
}))
99104
default = {}
100105

@@ -106,4 +111,12 @@ variable "host_uri_rate_limit_rules" {
106111
condition = alltrue([for r in values(var.host_uri_rate_limit_rules) : contains([60, 120, 300, 600], r.evaluation_window_sec)])
107112
error_message = "evaluation_window_sec must be one of 60, 120, 300, 600."
108113
}
114+
validation {
115+
condition = alltrue([for r in values(var.host_uri_rate_limit_rules) : r.block_response_code == null ? true : (r.block_response_code >= 200 && r.block_response_code <= 599)])
116+
error_message = "block_response_code must be a valid HTTP status (200-599) supported by AWS WAF, e.g. 429."
117+
}
118+
validation {
119+
condition = alltrue([for r in values(var.host_uri_rate_limit_rules) : r.block_response_body_json == null ? true : length(r.block_response_body_json) <= 4096])
120+
error_message = "block_response_body_json must be 4096 bytes or fewer (AWS WAF custom response body limit)."
121+
}
109122
}

0 commit comments

Comments
 (0)