WAF audit logs without leaking credentials, the secure way
The WAF blocked a login attack and wrote a perfect record of it: the user's password, their session cookie and a bearer token, now in a log index that forty people can search. Your WAF just became the best place to steal credentials from.
The short answer
Log audit parts A, F, H and Z only: drop B (request headers, which hold Authorization and Cookie) and C or I (request bodies, which hold passwords). Use SecAuditEngine RelevantOnly. The rule match line still quotes the matched value, so redact sensitive parameter names in the log shipper before the log leaves the node.
On this page
What goes wrong
A WAF audit log records a whole transaction, split into parts named by
letters. Part B is the request headers. Part C is the request body. They
are the parts that make an audit log useful for an investigation, and they
are exactly where credentials live: Authorization: Bearer ...,
Cookie: session=..., and password=... in a login form.
Coraza's default parts are ABCFHZ. The recommended config that ships with
coraza-proxy-wasm sets ABIJDEFHZ. Both include request headers and body.
The first time an attack hits your login endpoint, the log stores every
credential in the request, in plain text, and ships it to wherever your logs
go.
There is a second path. When a CRS rule matches, the error log line quotes
the value it matched, in a [data "Matched Data: ... found within ARGS_POST:password: ..."] field. That line is written no matter which audit
parts you choose.
What the docs say
B: Request headers. C: Request body (present only if the request body exists and Coraza is configured to intercept it. This would require SecRequestBodyAccess to be set to on).
Source: Coraza docs, SecAuditLogParts
Default: ABCFHZ
Source: Coraza docs, SecAuditLogParts
Coraza's list of actions has no sanitiseArg or sanitiseMatched, the
ModSecurity v2 actions that masked values in logs; see the
Coraza actions reference. The docs
do not warn that parts B and C contain credentials, and nothing mentions the
matched-data field at all.
The secure configuration
Coraza directives (in coraza-proxy-wasm, entries in directives_map):
Include @recommended-conf
SecRuleEngine On
# Write an audit record only for relevant transactions (rule matches and
# the relevant status codes, which @recommended-conf sets to 40x/41x and
# 50x/51x), not for every request.
SecAuditEngine RelevantOnly
# A = header (time, client, transaction id), F = response headers,
# H = trailer (engine state), Z = end. No B (request headers: Authorization,
# Cookie), no C/I (request body: passwords, tokens), no E (response body).
SecAuditLogParts AFHZ
SecAuditLogFormat JSON
Include @crs-setup-conf
Include @owasp_crs/*.confThe transaction id in part A matches the unique_id in the rule match
lines, so you can still join an audit record to the rules that fired.
Redaction for the rule match lines, in the log shipper, before anything
leaves the node. As a sed expression (translate it to your shipper's
replace stage: the replace function in Vector, stage.replace in Grafana
Alloy, or a Lua filter in Fluent Bit):
# Replace the value after "found within <COLLECTION>:<name>: " when the
# name looks like a credential. Case-insensitive.
sed -E 's/(found within [A-Z_]+:[^:"]*(pass|pwd|secret|token|key|auth|session|cookie)[^:"]*: )[^"]*/\1[REDACTED]/I'In the Envoy access log, never log %REQ(AUTHORIZATION)%,
%REQ(COOKIE)% or the full query string of login and token endpoints.
Prove it
The test in secure-tests/waf-audit-logs-without-credentials/run.sh sends one
login attempt that trips CRS rule 942100: a bearer token, a session cookie,
and a password with SQL in it.
curl -X POST https://app.example.com/login \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.c2VjcmV0.sig" \
-H "Cookie: session=s3cr3t-session-id" \
--data "user=alice&password=hunter2%27+OR+%271%27%3D%271"The fields found in the JSON audit record, with Coraza's default parts and
with AFHZ:
== SecAuditLogParts ABCFHZ
POST /login -> 403
audit log fields:
"authorization":["Bearer eyJhbGciOiJIUzI1NiJ9.c2VjcmV0.sig"]
"cookie":["session=s3cr3t-session-id"]
"body":"user=alice&password=hunter2%27+OR+%271%27%3D%271"
== SecAuditLogParts AFHZ
POST /login -> 403
audit log fields:
"body":""
"headers":nullThe rule match line, which does not depend on the audit parts, and the same line after the redaction filter:
== The error log line for rule 942100 (any audit parts)
[data "Matched Data: s&sos found within ARGS_POST:password: hunter2' OR '1'='1"]
== The same line after the shipper's redaction filter
[data "Matched Data: s&sos found within ARGS_POST:password: [REDACTED]"]Mistakes people make
Keeping the recommended parts because they came with the filter
ABIJDEFHZ is built for debugging a rule set, not for production logs that
many people can read. Choose parts on purpose.
Relying on the log backend's access control
Logs get copied: to a SIEM, to a support ticket, to a laptop during an incident. A credential that was never written cannot leak from any of them.
Fixing the audit log and forgetting the match lines
The [data "Matched Data: ..."] field quotes the matched variable. It shows
up in the proxy log on every rule match, with or without an audit log.
Removing inspection of the password field instead
Excluding ARGS:password from the rules removes the leak and the protection
at the same time. Redact the log, keep the inspection.
Logging everything to catch everything
SecAuditEngine On writes a record for every request. With parts B and C that
is a full copy of every login. Use RelevantOnly.
Checklist
- Set
SecAuditLogParts AFHZ(no B, C, I, E). - Set
SecAuditEngine RelevantOnly. - Add a redaction stage for matched-data values of credential-like names.
- Keep Authorization, Cookie and login query strings out of the access log.
- Send one test login attack and grep the logs for its password and token.
- Set a retention period for WAF logs and restrict who can search them.
A good WAF log tells you who attacked, when and with what. It should never be able to tell you the password.
H2-CPQE
Learn it on a live range
WAF, rate limiting and DDoS, in Edge and Post-Quantum Networking: a real host in your browser, and every objective checked on the machine.
Start freeThe Dome
Want it run for you?
The Dome puts post-quantum TLS, a WAF that blocks, signed DNS and a zero-trust mesh in front of your application. Tell us what you run.
See the Dome