Skip to content

Commit fe674ae

Browse files
OlamideOl1claude
andcommitted
Add optional uri_paths scoping to WAF host IP restriction
Lets a host restriction apply to specific URL path prefixes instead of the whole host, e.g. lock /admin to an allowed IP while the rest of the host stays public. Paths are matched with STARTS_WITH after URL_DECODE and LOWERCASE; multiple paths are OR'd together. Defaults to an empty list, which renders no URI condition and leaves the whole host restricted, so existing consumers are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f65a3de commit fe674ae

2 files changed

Lines changed: 65 additions & 6 deletions

File tree

aws/waf/main.tf

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,64 @@ resource "aws_wafv2_web_acl" "main" {
273273
}
274274
}
275275
}
276+
277+
# Optional: scope the restriction to one or more URL path prefixes.
278+
# Renders nothing when uri_paths is empty, leaving the whole host restricted.
279+
dynamic "statement" {
280+
for_each = length(rule.value["uri_paths"]) > 0 ? [1] : []
281+
content {
282+
dynamic "byte_match_statement" {
283+
for_each = length(rule.value["uri_paths"]) == 1 ? [1] : []
284+
content {
285+
field_to_match {
286+
uri_path {}
287+
}
288+
289+
positional_constraint = "STARTS_WITH"
290+
291+
search_string = lower(rule.value["uri_paths"][0])
292+
293+
text_transformation {
294+
priority = 0
295+
type = "URL_DECODE"
296+
}
297+
text_transformation {
298+
priority = 1
299+
type = "LOWERCASE"
300+
}
301+
}
302+
}
303+
dynamic "or_statement" {
304+
for_each = length(rule.value["uri_paths"]) > 1 ? [1] : []
305+
content {
306+
dynamic "statement" {
307+
for_each = rule.value["uri_paths"]
308+
content {
309+
byte_match_statement {
310+
field_to_match {
311+
uri_path {}
312+
}
313+
314+
positional_constraint = "STARTS_WITH"
315+
316+
search_string = lower(statement.value)
317+
318+
text_transformation {
319+
priority = 0
320+
type = "URL_DECODE"
321+
}
322+
text_transformation {
323+
priority = 1
324+
type = "LOWERCASE"
325+
}
326+
}
327+
}
328+
}
329+
}
330+
}
331+
}
332+
}
333+
276334
statement {
277335
not_statement {
278336
statement {

aws/waf/variables.tf

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,14 @@ variable "block_ip_list" {
7272
}
7373

7474
variable "host_ip_restriction_rules" {
75-
description = "Restrict specific Hosts to an allowed IP set. For each entry, blocks requests where the Host header matches exactly AND the source IP is not in that entry's allowed set. Globally allowed IPs (allowed_ip_list) still pass via the priority-0 allow rule."
75+
description = "Restrict specific Hosts (optionally scoped to URL path prefixes) to an allowed IP set. For each entry, blocks requests where the Host header matches exactly, the URI matches one of the given path prefixes (if any), AND the source IP is not in that entry's allowed set. Globally allowed IPs (allowed_ip_list) still pass via the priority-0 allow rule."
7676
type = map(object({
77-
name = string # Friendly name, used in the rule name and CloudWatch metric.
78-
priority = number # Unique WAF rule priority within the ACL. All rules are processed from lowest to highest priority.
79-
host = string # Exact Host header to restrict, e.g. "core-data.albaikcloud.com".
80-
allowed_ip_list = list(string) # IPv4 CIDRs permitted to reach the host. All other (non globally-allowed) IPs are blocked for this host.
81-
count_override = optional(bool, false) # If true, override the action to `count` (dry run). If false (default), the action is `block`.
77+
name = string # Friendly name, used in the rule name and CloudWatch metric.
78+
priority = number # Unique WAF rule priority within the ACL. All rules are processed from lowest to highest priority.
79+
host = string # Exact Host header to restrict, e.g. "core-data.albaikcloud.com".
80+
allowed_ip_list = list(string) # IPv4 CIDRs permitted to reach the host. All other (non globally-allowed) IPs are blocked for this host.
81+
uri_paths = optional(list(string), []) # Path prefixes to scope the restriction to, e.g. ["/admin"]. Matched with STARTS_WITH, so "/admin" also covers "/admin/x". Empty (default) restricts the whole host.
82+
count_override = optional(bool, false) # If true, override the action to `count` (dry run). If false (default), the action is `block`.
8283
}))
8384
default = {}
8485
}

0 commit comments

Comments
 (0)