0

I'm setting up a WAF for an API. I want to have different rate limits depending on the source of the traffic. Some traffic comes from Vercel and want to have a higher threshold compared to the traffic that comes directly. What I see is that when a request arrives, it's checked against the Vercel rate limiter rule, but it then falls back to the default rule.

At high level, what I want to implement is:

  • A rate limit if a request has a header with a given value.
  • A default rate limit if the one above didn't match (the header is missing).

Here's the terraform config of the ACL (it's slighly simplified from the original one)

resource "aws_wafv2_web_acl" "api" {
  name        = "waf"
  description = "WAF for ALB"
  scope       = "REGIONAL"

default_action { allow {} }

rule { name = "Vercel" priority = 1 action { block {} }

statement {
  rate_based_statement {
    limit = 5000
    evaluation_window_sec = 300
    aggregate_key_type    = "FORWARDED_IP"
    forwarded_ip_config {
      header_name       = "x-forwarded-for"
      fallback_behavior = "NO_MATCH"
    }
    scope_down_statement {
      byte_match_statement {
        field_to_match {
          single_header {
            name = "user-agent"
          }
        }
        positional_constraint = "EXACTLY"
        search_string         = "Vercel Edge Functions"
        text_transformation {
          priority = 0
          type     = "NONE"
        }
      }

    }
  }
}

visibility_config {
  cloudwatch_metrics_enabled = true
  metric_name                = "vercel"
  sampled_requests_enabled   = true
}

}

Default limiting rule for traffic we cannot match

rule { name = "default-rate-limit" priority = 2

action {
  block {}
}

statement {
  rate_based_statement {
    limit                 = 60
    evaluation_window_sec = 60
    aggregate_key_type    = "IP"
  }
}

visibility_config {
  cloudwatch_metrics_enabled = true
  metric_name                = "default-rate-limit"
  sampled_requests_enabled   = true
}

}

visibility_config { cloudwatch_metrics_enabled = true metric_name = "waf" sampled_requests_enabled = true } }

To test this, I wrote a script that sends a bunch of requests with no headers until it's blocked. Then, I run a single request with the user-agent and x-forwarded-for headers and it generated this log snippet. The Vercel rule matched, but the processing didn't stop there and continue to the default rate limit rule.

    "terminatingRuleId": "default-rate-limit",
    "terminatingRuleType": "RATE_BASED",
    "action": "BLOCK",
    "terminatingRuleMatchDetails": [],
    "httpSourceName": "ALB",
    "httpSourceId": "xxxxxxxxx-app/api-lb/xxxxxxxxxxxx",
    "ruleGroupList": [],
    "rateBasedRuleList": [
        {
            "rateBasedRuleId": "arn:aws:wafv2:us-west-2:xxxxxxxxxx",
            "rateBasedRuleName": "Vercel",
            "limitKey": "FORWARDEDIP",
            "maxRateAllowed": 50000,
            "evaluationWindowSec": 300,
            "limitValue": "yyy.yyy.yyy.yyy".  // IP from x-forwarded-for header
        },
        {
            "rateBasedRuleId": "arn:aws:wafv2:xxxxxxxxxx",
            "rateBasedRuleName": "default-rate-limit",
            "limitKey": "IP",
            "maxRateAllowed": 60,
            "evaluationWindowSec": 60,
            "limitValue": "xxx.xxx.xxx.xxx" // IP from where the request originated
        }
    ],

I'm wondering if something is misconfigured above or if I need to use labels for this use case.

And a note that I'm using the user-agent while I test this. The real solution will use a different header with a value that is more difficult to guess.

edit

I found a potential (ugly) solution. To have a rate limiter and immediately after a rule that allows traffic with the same conditions as in the scope down statement of the rate limiter.

I tried using labels, but the WAF only applies the label in a rate limit rule when it's actively blocking traffic. So it's not possible to know if a rule didn't match the scope down statement or matched but was not rate limited.

Augusto
  • 225

0 Answers0