ModSecurity封锁了自己的CDN:当WAF不知道自己在Akamai后面并封禁了边缘节点
发布于 2026年5月2日
问题:CDN报告ERR_READ_ERROR,但WAF健康
Akamai报告ERR_READ_ERROR|no_resp_hdrs——在收到源站响应头之前TCP连接断开。从终端用户的角度来看,网站间歇性加载:有时200,有时无响应超时。WAF正在运行,nginx后端对其他域名正常响应。故障仅针对这个特定域名。
服务器架构在单台OCI主机上叠加了多个层次:
# 网络拓扑
互联网 → Akamai(纯CDN,无WAF/安全功能)
→ WAF容器(network_mode: host,443端口)
ModSecurity 3.0.14 + OWASP CRS 4.14.0-dev + CrowdSec bouncer
→ proxy_pass https://172.28.5.61
→ nginx后端(bridge网络,IP 172.28.5.61)
→ fastcgi → PHP-FPM(network_mode: host)关键细节:WAF运行在network_mode host模式。这意味着它直接看到Akamai边缘节点的IP——没有NAT,没有中间层。网站所有请求的$remote_addr都是Akamai的IP,而不是真实访客的IP。
证据:单个日志文件中有928,219次拦截
第一个具体信号是error.log的大小:839 MB。自2月9日起没有轮转——超过五周的累积。modsec_audit.log达到6.9 GB,access.log达到1.3 GB。
过滤error.log中的拦截记录,规律立即显现:
# 统计error.log中的ModSecurity拦截次数
grep -c 'ModSecurity' /var/log/nginx/error.log
# 928219
# 识别被拦截最多的IP
grep 'ModSecurity.*blocked' /var/log/nginx/error.log | grep -oP 'client: K[0-9.]+' | sort | uniq -c | sort -rn | head -10被拦截的IP总是相同的那几个:192.0.2.12、192.0.2.13、198.51.100.14、198.51.100.15。对照Akamai公布的IP段快速验证,确认了来源:
# 检查IP是否属于Akamai范围(发布于 https://techdocs.akamai.com/...)
# 范围 192.0.2.0/24 覆盖 192.0.2.0 到 192.0.2.255
# 范围 198.51.100.0/24 覆盖 198.51.100.0 到 198.51.100.255
python3 -c "
import ipaddress
akamai_ranges = ['192.0.2.0/24', '198.51.100.0/24']
test_ips = ['192.0.2.12', '192.0.2.13', '198.51.100.14', '198.51.100.15']
for ip in test_ips:
for cidr in akamai_ranges:
if ipaddress.ip_address(ip) in ipaddress.ip_network(cidr):
print(f'{ip} 属于 {cidr}')
"
# 192.0.2.12 属于 198.51.100.0/24
# 192.0.2.13 属于 198.51.100.0/24
# 198.51.100.14 属于 192.0.2.0/24
# 198.51.100.15 属于 192.0.2.0/24WAF正在封锁Akamai自己的边缘节点。每一个通过CDN的请求,其异常分数都是针对边缘节点IP计算的,而不是真实访客的IP。
为什么OWASP CRS会拦截Akamai的请求
OWASP CRS对每个请求计算累积异常分数。当分数超过阈值(默认:5即拦截)时,请求被拒绝。来自Akamai的请求中,分数高达40到60。
最具说明性的案例是wp-cron.php的拦截。Akamai定期发送探测请求以验证源站是否响应。这些探测请求包含一个带XSS模式的referrer,这是边缘节点的标准行为:
# modsec_audit.log片段——Akamai对wp-cron.php的探测
--abc123--A--
[13/Mar/2026:14:23:01 +0000] "GET /wp-cron.php?doing_wp_cron HTTP/1.1"
--abc123--B--
Referer: https://blog.cliente-exemplo.com.br/<script>alert(81)</script>
User-Agent: Mozilla/5.0 (compatible; AkamaiGHost/...)
--abc123--H--
ModSecurity: Warning. XSS Attack Detected via libinjection.
[file "REQUEST-941-APPLICATION-ATTACK-XSS.conf"]
[id "941100"] [rev ""] [msg "XSS Attack Detected via libinjection"]
[data "Matched Data: <script> found within ARGS:doing_wp_cron"] ...
[severity "CRITICAL"] [ver "OWASP_CRS/4.14.0-dev"]
[tag "application-multi"]
[anomaly-score 15]
ModSecurity: Access denied with code 403 (phase 2).
[score: 40] [threshold: 5]带XSS模式的referrer触发了规则941100(通过libinjection检测XSS),得分15。加上其他规则对异常User-Agent和header模式的评分,总分达到40。阈值为5,拦截不可避免。
合法用户的POST请求也受到影响——任何包含特殊字符字段的POST都会累积足够的分数被拦截,因为$remote_addr是Akamai的IP。real_ip配置缺失还影响了WAF的速率限制规则,将来自一个边缘节点的所有流量分组到同一个桶中。
common/akamai.conf文件已经存在——只是没有被引用
检查服务器的配置结构时,Akamai的real_ip文件已经存在:
# 文件 /home/developer/webserver/common/akamai.conf
set_real_ip_from 192.0.2.0/24;
set_real_ip_from 198.51.100.0/24;
# ... 其他Akamai段
real_ip_header X-Forwarded-For;
real_ip_recursive on;问题是WAF的vhost(waf-enabled/blog.cliente-exemplo.com.br)引用了错误的文件:common/gocache.conf而不是common/akamai.conf。服务器最初是以GoCache作为CDN配置的,当Akamai取代GoCache时,只修改了DNS——vhost中的include保持旧配置不变。
# WAF vhost中的内容(错误)
server {
listen 443 ssl;
server_name blog.cliente-exemplo.com.br;
include common/gocache.conf; # <-- 错误:GoCache段,不是Akamai
...
}
# nginx后端vhost中同样的问题(错误)
server {
listen 443;
server_name blog.cliente-exemplo.com.br;
include common/gocache.conf; # <-- 错误:后端也有同样的行
...
}修复方案:两个vhost,两次重新加载
修复需要在WAF和nginx后端两个层面都进行——因为每一层都有自己的real_ip配置:
# 修改前备份vhost
cp /home/developer/webserver/waf-enabled/blog.cliente-exemplo.com.br /home/developer/webserver/blog.cliente-exemplo.com.br.waf-bak.20260319
cp /home/developer/webserver/sites-enabled/blog.cliente-exemplo.com.br /home/developer/webserver/blog.cliente-exemplo.com.br.bak.20260319
# 修复WAF vhost:将gocache.conf替换为akamai.conf
sed 's|include common/gocache.conf;|include common/akamai.conf;|g' /home/developer/webserver/waf-enabled/blog.cliente-exemplo.com.br > /tmp/waf-new
cp /tmp/waf-new /home/developer/webserver/waf-enabled/blog.cliente-exemplo.com.br
# 修复nginx后端vhost:相同调整
sed 's|include common/gocache.conf;|include common/akamai.conf;|g' /home/developer/webserver/sites-enabled/blog.cliente-exemplo.com.br > /tmp/nginx-new
cp /tmp/nginx-new /home/developer/webserver/sites-enabled/blog.cliente-exemplo.com.br
# 先测试并重新加载WAF
docker exec waf nginx -t && docker exec waf nginx -s reload
# 再测试并重新加载nginx后端
docker exec nginx nginx -t && docker exec nginx nginx -s reload结果
WAF重新加载后,拦截立即归零。Akamai边缘节点开始被识别为可信代理——ModSecurity的$remote_addr变成了从X-Forwarded-For计算出的真实访客IP。
modsec_audit.log停止增长。对于累积的10.5 GB日志,进行了手动轮转和压缩:
# 手动轮转累积的日志
cd /var/log/nginx
mv error.log error.log.20260319 # 839 MB
mv modsec_audit.log modsec_audit.log.20260319 # 6.9 GB
mv access.log access.log.20260319 # 1.3 GB
# 重新加载以创建新的日志文件
docker exec nginx nginx -s reload
# 压缩旧日志(约10.5 GB → 约296 MB)
gzip error.log.20260319
gzip modsec_audit.log.20260319
gzip access.log.20260319经验教训:CDN后面的WAF需要在任何规则之前配置real_ip
这是WAF + CDN架构中的经典配置错误。ModSecurity——和任何基于IP的检测系统一样——需要知道哪些地址是可信代理。没有这个配置,每个通过CDN的请求都以边缘节点IP作为源地址。
顺序很重要:set_real_ip_from必须在ModSecurity规则之前处理。在nginx中,这意味着real_ip文件的include必须在server block中的ModSecurity指令之前出现。
其他WAF系统的处理方式不同。云托管WAF解决方案(如集成到负载均衡器的OCI WAF,在另一篇文章中有描述)在CDN已经解包真实IP之后才接收流量——这个问题不存在,因为CDN层是同一平台的一部分。当WAF是自管理的并位于源站时,real_ip配置是运维人员的责任。
928,219次拦截,839 MB的error.log,磁盘占用70%——全部由两个vhost文件中的一行错误配置引起。include common/gocache.conf,本该是include common/akamai.conf。
CDN后面处于network_mode host的WAF配置检查清单
1. 确认使用的CDN: Akamai、Cloudflare、CloudFront、GoCache——每个都有不同的IP段。
2. 验证real_ip_header: Akamai发送X-Forwarded-For和True-Client-IP。Cloudflare使用CF-Connecting-IP。确认CDN填充的是哪个header。
3. 在两个层面都应用: WAF和nginx后端都需要set_real_ip_from。只修复WAF会让后端的速率限制和日志使用错误的IP。
4. 配置logrotate: 误报情况下error.log和modsec_audit.log增长迅速。没有轮转,磁盘会悄然填满。

