ModSecurity audit logging: ~37 lines / ~5 KB written per blocked request, SecAuditLogType not configurable
Affected area
core / all-in-one — ModSecurity audit logging.
What happened?
With MODSECURITY_SEC_AUDIT_ENGINE=RelevantOnly (the default), every blocked request writes a full audit record. Measured on bunkerity/bunkerweb-all-in-one:1.6.9, that is 37.3 lines and 5,109 bytes of audit log per blocked request.
Under sustained blocking this is a large amount of I/O generated directly by attacker-controlled traffic, and there is currently no way to tune it short of turning audit logging off entirely. Two things compound the default:
SecAuditLogType Serialis hardcoded insrc/common/core/modsecurity/confs/server-http/modsecurity-rules.conf.modsec(line 126 ondev). Serial means all worker threads serialise on a single writer.- In the container images the audit log is the container's stderr.
src/bw/Dockerfilesymlinks/var/log/bunkerweb/modsec_audit.logto/proc/1/fd/2, and in the all-in-one imagelogstream.shpipes it throughtail -F | tr -d '\000-\010\013-\037\177' | sed "s/^/[MODSEC] /"— two extra processes touching every one of those lines.
I want to note up front that MODSECURITY_SEC_AUDIT_LOG was added on dev since 1.6.9, so the log path is now configurable — that already helps, and this issue is not asking for it again. What remains unconfigurable is the writer type and the volume.
How to reproduce?
Origin: nginx:alpine with location / { return 200 "ok"; }. BunkerWeb as a reverse proxy in front of it, rate/ban features disabled so a single-source generator isn't simply IP-banned:
docker run -d --name bw-bench -p 18092:8080 \
-e SERVICE_UI=no -e SERVER_NAME=localhost \
-e USE_REVERSE_PROXY=yes -e REVERSE_PROXY_HOST=http://$OIP:80 -e REVERSE_PROXY_URL=/ \
-e USE_MODSECURITY=yes -e USE_MODSECURITY_CRS=yes -e SECURITY_MODE=block \
-e USE_BAD_BEHAVIOR=no -e USE_LIMIT_REQ=no -e USE_LIMIT_CONN=no -e USE_BLACKLIST=no \
-e USE_DNSBL=no -e USE_BUNKERNET=no -e USE_WHITELIST=no -e USE_GREYLIST=no \
bunkerity/bunkerweb-all-in-one:1.6.9Attack targets must carry Host: localhost, otherwise BunkerWeb serves its default landing page with neither the reverse proxy nor CRS engaged, which silently looks like "not blocked":
GET http://127.0.0.1:18092/?id=1%27%20OR%20%271%27=%271
Host: localhostThen, per rate:
docker exec bw-bench sh -c ': > /var/log/bunkerweb/modsec_audit.log'
vegeta attack -duration=15s -rate=300 -timeout=30s -connections=200 -targets=attack.txt | vegeta report
docker exec bw-bench sh -c 'wc -lc < /var/log/bunkerweb/modsec_audit.log'Measurements
15 s attack runs, 3-target mix (SQLi, XSS, path traversal), all returning 403:
| Attack rate | Blocks | Audit lines written | Audit bytes written |
|---|---|---|---|
| 300 req/s | 4,500 | 168,000 | 23.0 MB |
| 350 req/s | 5,250 | 196,000 | 27.1 MB |
| 1000 req/s | 15,000 | 560,000 | 76.6 MB |
Latency at 300 req/s, audit logging on vs off, identical config otherwise:
SecAuditEngine |
p50 | p99 |
|---|---|---|
RelevantOnly (default) |
42 ms | 459 ms |
Off |
27 ms | 84 ms |
So audit logging costs roughly 5x p99 at that rate.
What this is not
I want to be precise here, because I initially assumed the opposite and was wrong.
Audit logging is not a throughput-collapse or availability problem. I ran a 2x2 varying the audit engine and the deny-response size independently. With audit logging fully enabled and writing 76.6 MB during the run, an instance whose deny response was small handled 1,000 attack req/s at p50 5.5 ms with zero drops. Disabling audit logging on an otherwise-default instance did not prevent collapse. The dominant factor was elsewhere.
This issue is therefore about cost and tunability only, not availability. I did not want to file a performance claim that a controlled test doesn't support.
Suggested improvements
- Make
SecAuditLogTypeconfigurable (Serial/Concurrent), so high-volume deployments can escape the single serial writer. This is the main ask. - Consider documenting the per-blocked-request cost next to
MODSECURITY_SEC_AUDIT_ENGINE, so operators can size disk and choose deliberately. Roughly 5 KB and 37 lines per blocked request at the defaultSecAuditLogPartsis useful to know before an attack rather than during one. - Optionally, a way to sample or cap audit records under sustained blocking — though I appreciate ModSecurity offers no native mechanism for this, so it may not be practical.
Happy to send a PR for (1) and (2) if the direction is welcome — I'd branch from dev.
BunkerWeb version
1.6.9 (measured). Claims re-verified against dev at the time of filing.
What integration are you using?
Docker (all-in-one image).
Notes
I work on WAF engineering commercially, so I'll state that plainly. The numbers above are BunkerWeb measured against itself — there is no comparative or competitive data in this report.
Source: bunkerity/bunkerweb