亚马逊AWS官方博客

亚马逊云科技 WAF 部署小指南(六)追踪 Amazon WAF Request ID,排查误杀原因

众所周知,中国是全球制造业的巨大力量,许多中国企业通过 2B 电商平台网站进行商品销售和采购。在这些电商平台上,Web 应用防火墙(WAF)成为不可或缺的安全工具。然而,WAF 也可能导致误杀问题。一旦误杀发生,网站管理员需要尽快解决,以免企业客户无法正常下单,造成巨大的损失。而要解决误杀问题,首先需要有能够定位相关日志的线索。

本文将介绍如何利用 Amazon Lambda@Edge,在 Amazon CloudFront 自定义错误页面上展示每个由 Amazon WAF 返回的“403 Forbidden”错误的 Request ID。通过这个唯一的 WAF Request ID,网站运维工程师能够快速查询相应的 WAF 日志,找到误杀的原因。随后,可以配置 Scope-down 来修复误杀问题。

工作原理

CloudFront 的请求事件包含有 requestId 字段,每一个请求都有一个唯一的 Request ID 作为标识。以下是 Lambda@Edge 请求事件的 requestId 数据结构。

{
  "Records": [
    {
      "cf": {
        "config": {
          "eventType": "viewer-request",
          "requestId": "4TyzHTaYWb1GX1qTfsHhEqV6HUDd_BzoBZnwfnvQc_1oF26ClkoUSEQ=="
        },
      }
    }
  ]
}

如图 1 所示,CloudFront 自定义错误页面是由 CloudFront(而不是 Client)发起的,它的 Request ID 与 Client 原始请求的 Request ID 相同。因此,我们使用 Lambda@Edge 捕获自定义错误页面的请求,从请求事件中读取 Request ID,插入到预先定义好的 HTML 代码中,直接将这个 HTML 作为 Response body 返回给 CloudFront。

图 1:CloudFront 自定义错误页面展示 WAF Request ID 的工作流程

配置步骤

1. 为 HTTP 状态码 403 创建 CloudFront 自定义错误页面

图 2:为 HTTP 状态码 403 创建 CloudFront 自定义错误页面

按照图 2 所示的方法,为 HTTP 状态码 403 创建 CloudFront 自定义错误页面,并配置错误页面的缓存时间(TTL)和错误页面的 URI path。这个步骤需要注意:

  • Client 看到的 403 错误页面的 Request ID 都应该是唯一的,所以需要设置“Error caching minimum TTL”为“0”,即不缓存。
  • 为了避免错误页面的 URI 受到 DDoS 攻击,产生不必要的 Lambda@Edge 费用,需要将这个 URI path 设置的尽量长一些,复杂一些。我们建议随机生成一个 Universally Unique ID(UUID)作为错误页面的 URI path,并且不要泄露这个 UUID。这个 URI path 所对应的网页并不需要真正存储在源站,因为 Lambda@Edge 会提前终结 CloudFront 的请求。

2. 为 CloudFront 自定义错误页面的 URI path 单独创建一个 Cache Behavior

我们目的是只允许 CloudFront 向自定义错误页面发起的请求才能触发 Lambda@Edge,因此,需要为自定义错误页面的 URI path 单独创建一个 Cache behavior,单独关联 Lambda@Edge 函数。

图 3:为 CloudFront 自定义错误页面的 URI path 单独创建一个 Cache Behavior

如图 3 所示,这个 Behavior 所配置的缓存策略必须是“Managed-CachingDisabled”。任何一个 Maximum TTL>0 的缓存策略都会使得 CloudFront 向自定义错误页面发起的请求无法触发 Viewer request Lambda@Edge 函数,而只能触发 Origin request Lambda@Edge 函数。

3. 创建 Lambda@Edge 函数,并添加 CloudFront viewer request trigger

复制以下 Python 代码,创建一个 Runtime 为 Python3.X,Architecture 为 X86_64 的 Lambda 函数。您可以编辑 CONTENT 变量的值——它实际上就是一个 HTML 网页的代码。您可以根据需求调用合适的 CSS 文件,插入您想要的图片。

CONTENT = '''
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>WAF custom error page</title>
    <link rel="stylesheet" href="/css/style.css"/>
  </head>
  <body>
    <h1>403 Forbidden!</h1>
    <p>Your request was blocked.</p>
    <p>Request ID: <span id="requestId">{CF_RID}</span></p>
    <button onclick="copyToClipboard()">Copy Request ID</button>
    <div id="copyMessage"></div>
    <script>
      function copyToClipboard() {
        var requestId = document.getElementById("requestId").innerText;
        var tempTextArea = document.createElement("textarea");
        tempTextArea.value = requestId;
        document.body.appendChild(tempTextArea);
        tempTextArea.select();
        document.execCommand("copy");
        document.body.removeChild(tempTextArea);
        document.getElementById("copyMessage").textContent = "Copied!";
      }
    </script>
  </body>
</html>
'''

def lambda_handler(event, context):
    record = event['Records'][0]['cf']
    request_id = record['config']['requestId']
    response = CONTENT.replace('{CF_RID}', request_id)
    return {
        'status': 403,
        'statusDescription': 'Forbidden',
        'headers': {
                'content-type': [{
                    'key': 'Content-Type',
                    'value': 'text/html'
                }]
            },
        'body': response
    }

如图 4 所示,为这个 Lambda 函数添加一个 Trigger。Resource 类型为“CloudFront”,Event type 类型为“viewer-request”,Path pattern 为 CloudFront 自定义错误页面的 URI path。

图 4:为 Lambda 函数添加 Trigger

4. 创建 WAF WebACL 并关联 CloudFront distribution

最后,我们创建一个 WAF WebACL,配置一个 WAF 规则,匹配 URI path /waf-id 来产生 Block 的动作。另外,我们还创建了一个限速规则匹配 URI path /rate-based-rule 并配置了 WAF 自定义响应。我们会在下文介绍这样做的目的。

图 5:配置 WAF WebACL 用于测试 Block 动作

CloudFront 自定义错误页面展示 WAF Request ID 的效果

浏览器访问 https://d123.cloudfront.net/waf-id,触发了 WAF block 动作,成功显示如图 6 所示的自定义错误页面(截图中的几个 JS 脚本是 Chrome 浏览器的插件所产生的,与本次测试无关)。

图 6:自定义错误页面和 WAF unique request ID 测试结果

测试结果如下:

  • 自定义错误页面可以显示 CloudFront request ID,并且和 X-Amz-Cf-Id response header 的值相同
  • 每一次请求都可以得到不同的 Request ID
  • 浏览器无法观察到 CloudFront 自定义错误页面的真实 URI path
  • 点击“Copy Request ID”按钮即可将 Request ID 复制到操作系统剪切板

通过 Request ID 查询 WAF 日志的方法

企业客户通常习惯于使用在线通讯工具直接与网站运营方联系。在遇到 WAF 误杀时,他们通常使用通讯工具向网站提供错误页面的截图。我们的错误页面提供一键复制 Request ID 的按钮,企业客户也可以很方便地使用通讯工具发送 Request ID 的文本。网站运维工程师在收到 Request ID 之后,即可在 WAF 日志监控系统上查询到对应的 WAF 拦截日志。具体的查询方法取决于监控 WAF 日志的方式。

在 Centralized Logging with OpenSearch 监控平台上查询 Request ID

如果网站使用了 Centralized Logging with OpenSearch 解决方案监控 WAF 日志,可以在仪表盘上添加“Request ID”作为过滤条件。步骤如下:

1)点击仪表盘右上角的“Edit”按钮

2)点击“Filters”面板右上角的齿轮图标,再点击“Edit visualization”菜单

3)“Add”一个 Options list,输入 Control Label 名称,选择 Index Pattern,Field 选择 httpRequest.requestId.keyword

4)点击网页右下角蓝色的“Update”按钮,更新 Visualization

5)点击网页右上角“Save”按钮,保存对仪表板所做的更改

Centralized Logging with OpenSearch 的仪表盘支持模糊查询。虽然 WAF Request ID 比较长,但只需要输入几个字符就可以找到完整的 Request ID,进而找到对应的 WAF 日志。

图 7:使用 Centralized Logging with OpenSearch 检索 Request ID

使用 Amazon CloudWatch Log Insight 查询 Request ID

如果 WAF 日志保存在 CloudWatch log group,可以使用下面的 CloudWatch log insight 查询语句检索 Request ID:

fields @message, httpRequest.requestId as requestId
| filter requestId = "tMzyyrTJhk5XiBbowY2v-WY5m-PGluVYKggI6KIJhlTHBlqpDEGQOQ=="    # 替换成需要检索的Request ID.
| display @message

也可以使用 like 方法进行模糊查询:

fields @message, httpRequest.requestId as requestId
| filter requestId like "tMzyy"    # 替换成需要检索的 Request ID
| display @message

下面的图 8 是 CloudWatch log insight- 检索结果的部分截图。点击左边的黑色三角形符号,即可展开完整的日志。

图 8:CloudWatch log insight 检索 Request ID 的部分截图

使用 Amazon Athena 查询 Request ID

如果 WAF 日志保存在 Amazon S3 桶,并且没有使用 OpenSearch 等工具创建仪表盘,则先创建 WAF 日志表,再使用下面的 SQL 语句检索 Request ID,并使用 like 方法进行模糊查询:

select from_unixtime(timestamp/1000) as datetime, * from "waf_log_db"."waf_request_id"
where httprequest.requestid like '%mLNIV%'

您需要将“waf_request_id”替换成您自己的数据表的名字,并将“mLNIV”替换成您希望检索的 Request ID(需要保留前后两个“%”符号)。下面的图 9 是 Athena 检索结果的部分截图,向右滚动页面即可显示所有日志字段。

图 9:Athena 检索 Request ID 的部分截图

成本估算

假设每月 10 Billion WAF 请求,其中 0.1% 为 Blocked 请求,即 10 Million。Lambda@Edge 内存 128GB,保守估计平均每个请求 Duration 为 5ms。使用 Amazon 价格计算器评估的每月 Lambda@Edge 含免费套餐的成本为 1.80 USD,不含免费套餐的成本为 2.10 USD。

另外,CloudFront 自定义页面所返回的 HTML 和 CSS 也会增加少量的数据流出(DTO)费用。本文所使用的 HTML 和 CSS,加上它们的 HTTP response headers,一共不到 2KB。10 Million 请求一共产生 20GB 的 DTO,没有超出免费套餐。如果不考虑免费套餐,每月产生大约 2 USD 的 CloudFront DTO 成本。如果不使用 CSS 美化自定义页面,成本可以更低。

避免 DDoS 事件消耗 Lambda@Edge 成本

上文“配置步骤 1”介绍了使用 UUID 作为 CloudFront 自定义错误页面的 URI path,避免错误页面的 URI 受到 DDoS 攻击,产生不必要的 Lambda@Edge 费用。另外,HTTP Flood 等 DDoS 攻击会触发 WAF 限速规则产生大量的 403 error。我们通常不需要关注限速规则的误杀,所以不需要使用 Lambda@Edge 函数为限速规则的 Block 动作展示 WAF Request ID。解决办法是使用 WAF 自定义响应覆盖 CloudFront 自定义错误页面。

按照图 10 的方法,为 WAF 规则的 Block 动作配置自定义响应。WAF 自定义响应的优先级高于 CloudFront 自定义错误页面,所以限速规则返回的 403 error 并不会触发 CloudFront 请求自定义错误页面,因此也不会触发 Lambda@Edge 函数。

图 10:为 WAF 规则配置自定义响应

WAF 自定义响应的测试结果

我们在之前的“配置步骤 4”已经创建了一个限速规则,匹配 URI path /rate-based-rule 并配置了 WAF 自定义响应。测试效果如图 11 所示:

图 11:WAF自定义响应测试结果

对于其他(几乎)不会产生误杀的规则,在错误页面展示 Request ID 并不必要,都可以按此方法为它们配置 WAF 自定义响应,避免执行 Lambda@Edge 函数。

方案总结

按照本文所介绍的方法,仅需支付少量的 Lambda@Edge 和 CloudFront DTO 费用,完成简单的配置操作,即可实现 WAF unique request ID 解决方案。通过唯一的 WAF request ID,网站管理员可以快速排查和解决误杀问题,缩短 WAF 规则的评估时间,改善用户体验。这个解决方案也适用于 Amazon ALB,Amazon API Gateway,以及其他所有能够作为 CloudFront 源站的 Web 应用或服务。只需要部署 CloudFront distribution 加速 ALB、API Gateway 或其他 Web 应用,并将 WAF WebACL 关联到 CloudFront distribution,即可支持 403 错误页面显示 WAF unique request ID。同时,CloudFront 还提供边缘加速,并降低 Amazon 源站的 DTO 成本

本篇作者

陈程

亚马逊云科技边缘产品架构师,有多年 OTT 行业相关的工作经验,熟悉云网络、CDN 和 WAF。非工作时间忙于抚养一娃俩猫。