ModSecurity blocked its own CDN: when the WAF doesn't know it's behind Akamai and bans the edge nodes
Published on May 2, 2026
The problem: ERR_READ_ERROR on CDN with healthy WAF
Akamai was reporting ERR_READ_ERROR|no_resp_hdrs — TCP connection broken before receiving origin headers. From the end user's perspective, the site loaded intermittently: sometimes 200, sometimes timeout without response. The WAF was working, the nginx backend was responding for other domains. The failure was specific to this domain.
The server architecture stacks multiple layers on a single OCI host:
# Network topology
Internet → Akamai (CDN only, no WAF/security)
→ WAF container (network_mode: host, port 443)
ModSecurity 3.0.14 + OWASP CRS 4.14.0-dev + CrowdSec bouncer
→ proxy_pass https://172.28.5.61
→ nginx backend (bridge network, IP 172.28.5.61)
→ fastcgi → PHP-FPM (network_mode: host)The critical detail: the WAF is in network_mode host. This means it sees Akamai edge node IPs directly — no NAT, no intermediate layer. The $remote_addr for all requests to the site is an Akamai IP, not the real visitor's IP.
The evidence: 928,219 blocks in a single log
The first concrete signal was the error.log size: 839 MB. No rotation since February 9 — over five weeks accumulating. The modsec_audit.log was at 6.9 GB. The access.log at 1.3 GB.
Filtering the blocks in the error.log, the pattern was immediate:
# Count ModSecurity blocks in error.log
grep -c 'ModSecurity' /var/log/nginx/error.log
# 928219
# Identify the most blocked IPs
grep 'ModSecurity.*blocked' /var/log/nginx/error.log | grep -oP 'client: K[0-9.]+' | sort | uniq -c | sort -rn | head -10The blocked IPs were always the same: 192.0.2.12, 192.0.2.13, 198.51.100.14, 198.51.100.15. A quick check against Akamai's published ranges confirmed the origin:
# Check if IPs belong to Akamai ranges (published at https://techdocs.akamai.com/...)
# Range 192.0.2.0/24 covers 192.0.2.0 to 192.0.2.255
# Range 198.51.100.0/24 covers 198.51.100.0 to 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} belongs to {cidr}')
"
# 192.0.2.12 belongs to 198.51.100.0/24
# 192.0.2.13 belongs to 198.51.100.0/24
# 198.51.100.14 belongs to 192.0.2.0/24
# 198.51.100.15 belongs to 192.0.2.0/24The WAF was blocking Akamai's own edge nodes. Every request passing through the CDN had its anomaly score calculated against the edge node IP, not the real visitor's IP.
Why OWASP CRS was blocking Akamai
OWASP CRS calculates a cumulative anomaly score per request. When the score exceeds a threshold (default: 5 to block), the request is denied. On requests coming from Akamai, the score was reaching 40-60.
The most revealing case was the wp-cron.php block. Akamai sends periodic probes to validate the origin is responding. These probes include a referrer with an XSS pattern that is part of standard edge node behavior:
# Excerpt from modsec_audit.log — Akamai probe to 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]The referrer with XSS pattern triggered rule 941100 (XSS Detection via libinjection) with score 15. Combined with other rules that scored for the unusual User-Agent and header patterns, the total score reached 40. With a threshold of 5, the block was inevitable.
Legitimate user POSTs were also affected — any POST including fields with special characters accumulated enough score to block, because the $remote_addr was Akamai's and the IP reputation didn't help. The absence of correct real_ip also affected the WAF's rate limiting rules, which grouped all traffic from an edge node into a single bucket.
The common/akamai.conf file already existed — it just wasn't included
Inspecting the server's configuration structure, the real_ip file for Akamai was already present:
# File /home/developer/webserver/common/akamai.conf
set_real_ip_from 192.0.2.0/24;
set_real_ip_from 198.51.100.0/24;
# ... other Akamai ranges
real_ip_header X-Forwarded-For;
real_ip_recursive on;The problem was that the WAF vhost (waf-enabled/blog.cliente-exemplo.com.br) included the wrong file: common/gocache.conf instead of common/akamai.conf. The server had originally been configured with GoCache as CDN, and when Akamai replaced GoCache, only the DNS was changed — the vhosts kept the old includes.
# What was in the WAF vhost (incorrect)
server {
listen 443 ssl;
server_name blog.cliente-exemplo.com.br;
include common/gocache.conf; # <-- WRONG: GoCache ranges, not Akamai
...
}
# What the nginx backend vhost also had (incorrect)
server {
listen 443;
server_name blog.cliente-exemplo.com.br;
include common/gocache.conf; # <-- WRONG: same line in the backend
...
}The fix: two vhosts, two reloads
The fix needed to be applied to both layers — WAF and nginx backend — because each has its own real_ip configuration:
# Backup vhosts before changing
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
# Fix WAF vhost: replace gocache.conf with 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
# Fix nginx backend vhost: same adjustment
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
# Test and reload WAF first
docker exec waf nginx -t && docker exec waf nginx -s reload
# Test and reload nginx backend
docker exec nginx nginx -t && docker exec nginx nginx -s reloadThe results
After the WAF reload, blocks dropped to zero immediately. Akamai edge nodes started being recognized as trusted proxies — ModSecurity's $remote_addr became the real visitor IP, calculated from X-Forwarded-For.
The modsec_audit.log stopped growing. For the 10.5 GB of accumulated logs, manual rotation and compression were performed:
# Manual rotation of accumulated logs
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
# Reload to create new log files
docker exec nginx nginx -s reload
# Compress old logs (~10.5 GB → ~296 MB)
gzip error.log.20260319
gzip modsec_audit.log.20260319
gzip access.log.20260319The lesson: WAF behind CDN requires real_ip before any rule
This is a classic misconfiguration in WAF + CDN architectures. ModSecurity — like any IP-based detection system — needs to know which addresses are trusted proxies. Without this, every request passing through the CDN has the edge node IP as the source address.
Order matters: set_real_ip_from must be processed before ModSecurity rules. In nginx, this means the real_ip file include must come before ModSecurity directives in the server block.
Other WAF systems handle this differently. Cloud-managed WAF solutions (such as OCI's WAF integrated with the Load Balancer, described in another post) receive traffic after the CDN has already unwrapped the real IP — the problem doesn't exist because the CDN layer is part of the same platform. When the WAF is self-managed and sits at the origin, real_ip configuration is the operator's responsibility.
928,219 blocks, 839 MB error.log, disk at 70% — all caused by a single incorrect line in two vhost files. include common/gocache.conf where it should have been include common/akamai.conf.
Checklist for WAF in network_mode host behind CDN
1. Identify the CDN in use: Akamai, Cloudflare, CloudFront, GoCache — each has distinct IP ranges.
2. Verify real_ip_header: Akamai sends X-Forwarded-For and True-Client-IP. Cloudflare uses CF-Connecting-IP. Confirm which header the CDN populates.
3. Apply to BOTH layers: WAF and nginx backend both need set_real_ip_from. Fixing only the WAF leaves rate limiting and backend logs with the wrong IP.
4. Configure logrotate: error.log and modsec_audit.log grow rapidly in case of false positives. Without rotation, the disk fills silently.

