众所周知,中国是全球制造业的巨大力量,许多中国企业通过 2B 电商平台网站进行商品销售和采购。在这些电商平台上,Web 应用防火墙(WAF)成为不可或缺的安全工具。然而,WAF 也可能导致误杀问题。一旦误杀发生,网站管理员需要尽快解决,以免企业客户无法正常下单,造成巨大的损失。而要解决误杀问题,首先需要有能够定位相关日志的线索。
本文将介绍如何利用 Amazon Lambda@Edge,在 [Amazon CloudFront 自定义错误页面](https://docs.aws.amazon.com/zh_cn/AmazonCloudFront/latest/DeveloperGuide/GeneratingCustomErrorResponses.html?trk=cndc-detail)上展示每个由 Amazon WAF 返回的“403 Forbidden”错误的 Request ID。通过这个唯一的 WAF Request ID,网站运维工程师能够快速查询相应的 WAF 日志,找到误杀的原因。随后,可以配置 [Scope-down](https://docs.aws.amazon.com/zh_cn/waf/latest/developerguide/waf-rule-scope-down-statements.html?trk=cndc-detail) 来修复误杀问题。
### **01 工作原理**
CloudFront 的[请求事件](https://docs.aws.amazon.com/zh_cn/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html#lambda-event-structure-request?trk=cndc-detail)包含有 `requestId` 字段,每一个请求都有一个唯一的 [Request ID](https://repost.aws/zh-Hans/knowledge-center/cloudfront-latency-diagnosis-data?trk=cndc-detail) 作为标识。以下是 Lambda@Edge 请求事件的 `requestId` 数据结构。
```js
{
"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 的工作流程
### **02 配置步骤**
#### 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](https://docs.aws.amazon.com/zh_cn/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesCacheBehavior?trk=cndc-detail),单独关联 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 文件,插入您想要的图片。
```js
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 动作
### **03 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 复制到操作系统剪切板
### **04 通过 Request ID 查询 WAF 日志的方法**
企业客户通常习惯于使用在线通讯工具直接与网站运营方联系。在遇到 WAF 误杀时,他们通常使用通讯工具向网站提供错误页面的截图。我们的错误页面提供一键复制 Request ID 的按钮,企业客户也可以很方便地使用通讯工具发送 Request ID 的文本。网站运维工程师在收到 Request ID 之后,即可在 WAF 日志监控系统上查询到对应的 WAF 拦截日志。具体的查询方法取决于监控 WAF 日志的方式。
#### 在 Centralized Logging with OpenSearch 监控平台上查询 Request ID
如果网站使用了 [Centralized Logging with OpenSearch 解决方案](https://aws.amazon.com/cn/solutions/implementations/centralized-logging-with-opensearch/?trk=cndc-detail)监控 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:
```js
fields @message, httpRequest.requestId as requestId
| filter requestId = "tMzyyrTJhk5XiBbowY2v-WY5m-PGluVYKggI6KIJhlTHBlqpDEGQOQ==" # 替换成需要检索的Request ID.
| display @message
```
也可以使用 `like` 方法进行模糊查询:
```js
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](https://aws.amazon.com/cn/s3/?trk=cndc-detail) 桶,并且没有使用 OpenSearch 等工具创建仪表盘,则先创建 [WAF 日志表](https://docs.aws.amazon.com/zh_cn/athena/latest/ug/waf-logs.html?trk=cndc-detail),再使用下面的 SQL 语句检索 Request ID,并使用 like 方法进行模糊查询:
```js
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 价格计算器](https://calculator.aws/#/addService/Lambda?trk=cndc-detail)评估的每月 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 美化自定义页面,成本可以更低。
### **05 避免 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 函数。
### **06 方案总结**
按照本文所介绍的方法,仅需支付少量的 Lambda@Edge 和 CloudFront DTO 费用,完成简单的配置操作,即可实现 WAF unique request ID 解决方案。通过唯一的 WAF request ID,网站管理员可以快速排查和解决误杀问题,缩短 WAF 规则的评估时间,改善用户体验。这个解决方案也适用于 Amazon ALB,[Amazon API Gateway](https://aws.amazon.com/cn/api-gateway/?trk=cndc-detail),以及其他所有能够作为 CloudFront 源站的 Web 应用或服务。只需要部署 CloudFront distribution 加速 ALB、API Gateway 或其他 Web 应用,并将 WAF WebACL 关联到 CloudFront distribution,即可支持 403 错误页面显示 WAF unique request ID。同时,CloudFront 还提供边缘加速,并降低 Amazon 源站的 DTO 成本。
