I tried Dynamic Label Interpolation with AWS WAF to test dynamic forwarding of bot information and custom block pages
This page has been translated by machine translation. View original
Introduction
On May 11, 2026, AWS WAF Dynamic Label Interpolation became available in all regions where AWS WAF is supported. Additionally, on July 21, detailed use cases were published on the Security Blog.
According to the official blog, AWS WAF Bot Control identifies and assigns labels to more than 650 bots and agents. However, traditionally it was necessary to write individual rules for each label to perform header insertion or response control. With Dynamic Label Interpolation, label values can be embedded within rules using the ${namespace:} syntax, allowing a single rule to cover an entire namespace. In this article, we verified the following two scenarios using a minimal configuration of ALB + Regional WAF.
- Scenario 1 — Dynamically forward labels assigned by Bot Control to request headers using
${namespace:}, reducing rule management costs - Scenario 2 — Embed Synthetic Labels (IP and request ID) into custom block responses to facilitate tracking during support inquiries
Verification Details
Verification Environment
| Item | Details |
|---|---|
| Client | curl 8.18.0 (with various User-Agents specified) |
| ALB | Fixed response 200 (no target group required) |
| AWS WAF | Regional WebACL, Bot Control Common (InspectionLevel: COMMON) |
| Bot Control OverrideAction | Count |
| Region | ap-northeast-1 |
| Deployment Method | CloudFormation |
WAF rule evaluation order follows ascending priority. In this verification, rules are evaluated in the following order.
bot-control-common(priority: 0) — Bot Control managed rule group. Assigns labels but does not block (OverrideAction: Count)forward-bot-signals(priority: 100) — Custom rule. References labels assigned by Bot Control and inserts headersrate-limit-with-debug-page(priority: 200) — Rate-based rule. Responds with a custom block page when the threshold is exceeded
Bot Control is set to OverrideAction: Count so that subsequent rules can reference its labels. See "Prerequisites Discovered During Initial Verification" for details.
Scenario 1: Dynamic Forwarding of Bot Control Labels
Rule Configuration
In the forward-bot-signals rule, LabelMatchStatement with Scope: NAMESPACE matches the entire namespace, and the ${namespace:} syntax dynamically expands label values into headers.
forward-bot-signals rule definition (JSON)
{
"Name": "forward-bot-signals",
"Priority": 100,
"Statement": {
"LabelMatchStatement": {
"Scope": "NAMESPACE",
"Key": "awswaf:managed:aws:bot-control:bot:category:"
}
},
"Action": {
"Count": {
"CustomRequestHandling": {
"InsertHeaders": [
{
"Name": "x-waf-bot-category",
"Value": "${awswaf:managed:aws:bot-control:bot:category:}"
},
{
"Name": "x-waf-bot-name",
"Value": "${awswaf:managed:aws:bot-control:bot:name:}"
},
{
"Name": "x-waf-bot-signals",
"Value": "${awswaf:managed:aws:bot-control:signal:}"
},
{
"Name": "x-waf-client-ip",
"Value": "${awswaf:ip:}"
}
]
}
}
},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "forward-bot-signals"
}
}
Test Results
Requests were sent with three User-Agent patterns to verify Bot Control label assignment and the behavior of forward-bot-signals.
| Test | User-Agent | HTTP Status | Bot Control Judgment | forward-bot-signals |
|---|---|---|---|---|
| Test 1: curl default UA | curl/8.18.0 | 200 | bot:category:http_library + signal:non_browser_user_agent |
Triggered (header inserted) |
| Test 2: Googlebot spoofing | Googlebot/2.1 | 200 | signal:non_browser_user_agent |
Not triggered |
| Test 3: Chrome UA | Chrome/126.0 | 200 | — (token:absent only) |
Not triggered |
In Test 1, Bot Control assigned the bot:category:http_library label, which matched the forward-bot-signals condition and triggered header insertion. The expanded values are recorded in the requestHeadersInserted field of the WAF log.
"requestHeadersInserted": [
{"name": "x-amzn-waf-x-waf-bot-category", "value": "http_library"},
{"name": "x-amzn-waf-x-waf-bot-name", "value": "curl"},
{"name": "x-amzn-waf-x-waf-bot-signals", "value": "non_browser_user_agent"},
{"name": "x-amzn-waf-x-waf-client-ip", "value": "203.0.113.1"}
]
The correspondence between ${namespace:} syntax and expansion results is as follows.
| Variable (namespace interpolation) | Expansion Result |
|---|---|
${awswaf:managed:aws:bot-control:bot:category:} |
http_library |
${awswaf:managed:aws:bot-control:bot:name:} |
curl |
${awswaf:managed:aws:bot-control:signal:} |
non_browser_user_agent |
${awswaf:ip:} |
203.0.113.1 |
In Test 2 (Googlebot spoofing), Bot Control assigned the signal:non_browser_user_agent label, but did not assign a bot:category: label. Since Bot Control uses not only the User-Agent string but also request characteristics such as the source IP to identify bots, this verification could not confirm classification as bot:category:search_engine through UA change alone. Since forward-bot-signals uses the bot:category: namespace as its condition, the rule did not trigger.
In Test 3 (Chrome UA), Bot Control signals labels were not assigned and only token:absent (indicating the AWS WAF token is absent from the request) was assigned, so similarly there was no match with the bot:category: namespace and the rule did not trigger.
According to the official documentation, if multiple labels match in a namespace, they are concatenated with commas. However, this situation did not occur during this verification and was not confirmed.
Prerequisites Discovered During Initial Verification
In the initial verification, OverrideAction was set to None (applying managed rule actions as-is). As a result, the CategoryHttpLibrary rule within Bot Control BLOCKED the request, and it never reached the subsequent forward-bot-signals. The WAF log recorded only the BLOCK by Bot Control, and the requestHeadersInserted field did not appear. Changing OverrideAction to Count causes Bot Control to only assign labels and pass the request through, allowing subsequent rules to reference the labels.
WAF Log: BLOCK with OverrideAction: None (equivalent to Test 1)
{
"terminatingRuleId": "bot-control-common",
"terminatingRuleType": "MANAGED_RULE_GROUP",
"action": "BLOCK",
"ruleGroupList": [{
"ruleGroupId": "AWS#AWSManagedRulesBotControlRuleSet",
"terminatingRule": {
"ruleId": "CategoryHttpLibrary",
"action": "BLOCK"
}
}],
"labels": [
{"name": "awswaf:managed:aws:bot-control:bot:category:http_library"},
{"name": "awswaf:managed:aws:bot-control:bot:name:curl"},
{"name": "awswaf:managed:aws:bot-control:signal:non_browser_user_agent"}
]
}
Scenario 2: Embedding Synthetic Labels into Custom Block Pages
The rate-based rule rate-limit-with-debug-page BLOCKs IPs that exceed 100 requests in 5 minutes and responds with a custom response body. Synthetic Labels (${awswaf:ip:} and ${awswaf:request_id:}) are embedded in the response body and headers to dynamically display information needed for support inquiries when blocked.
{
"CustomResponseBodies": {
"block-page-with-debug": {
"Content": "<!DOCTYPE html>\n<html>\n<head><title>Access Denied</title></head>\n<body>\n<h1>Access Denied</h1>\n<p>Your IP: ${awswaf:ip:}</p>\n<p>Request ID: ${awswaf:request_id:}</p>\n<p>If you believe this is an error, contact support with the Request ID above.</p>\n</body>\n</html>",
"ContentType": "TEXT_HTML"
}
}
}
Test Results
110 consecutive requests were sent with a Chrome UA to trigger the rate limit and verify the block response. The rate-based rule continuously evaluates the request rate using a 5-minute window, and BLOCKs requests while the threshold is exceeded. The block is lifted once the rate is determined to have fallen below the threshold.
The HTML response body received by the client when blocked is shown below.
<!DOCTYPE html>
<html>
<head><title>Access Denied</title></head>
<body>
<h1>Access Denied</h1>
<p>Your IP: 203.0.113.1</p>
<p>Request ID: 1467396511:1-6a5fdb1c-7efa072b7f5ecd8c3bb60a6c</p>
<p>If you believe this is an error, contact support with the Request ID above.</p>
</body>
</html>
The same Request ID is also inserted in the response header x-blocked-request-id.
x-blocked-request-id: 1467396511:1-6a5fdb1c-7efa072b7f5ecd8c3bb60a6c
In this verification, the Request ID displayed in the response matched the requestId field in the WAF log. This makes it possible to identify the corresponding WAF log entry using the ID reported by the user.
WAF Log: Rate Limit BLOCK (Test 4)
{
"terminatingRuleId": "rate-limit-with-debug-page",
"terminatingRuleType": "RATE_BASED",
"action": "BLOCK",
"responseCodeSent": 403,
"rateBasedRuleList": [{
"rateBasedRuleName": "rate-limit-with-debug-page",
"limitKey": "IP",
"maxRateAllowed": 100,
"evaluationWindowSec": 300,
"limitValue": "203.0.113.1"
}]
}
Summary
As you add control for each bot type using AWS WAF Bot Control, judgment rules and exception settings per category tend to increase, making WebACL management more complex. Dynamic Label Interpolation enables the dynamic forwarding of categories, names, and signals assigned by Bot Control to request headers on a per-namespace basis. This provides a simple means of passing information to the origin without enumerating per-category values in WAF rules.
This allows you to consider a configuration where, instead of writing detailed per-category processing in WAF, the judgment results are passed to the origin and the application side selects the necessary processing or response. In this verification, we confirmed that Bot Control label values are dynamically expanded into headers.
Additionally, WAF rules including Bot Control may sometimes result in unintended blocks or support inquiries. By embedding Synthetic Labels into custom block pages and response headers, you can receive a Request ID from blocked users and use it as a clue to trace the corresponding WAF log.
If you are experiencing challenges with the number of Bot Control rules, exception management, or post-block investigation, please try Dynamic Label Interpolation in a verification environment first.
