Headers as Bags
HTTP logging frameworks treat request headers as a bag of key-value pairs. The logging API exposes the full bag. Operators enable header logging for debugging: when a request fails, the headers tell the story. No built-in denylist. No credential filtering in the documentation. Full headers to disk.
The credential headers in a typical request:
- Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... (JWT or OAuth token)
- Cookie: session=abc123; auth=xyz789
- X-API-Key: sk-live-abc123...
- X-Auth-Token: ghp_abc123... (GitHub personal access token pattern)
These values authenticate the request. Written to a log file, they authenticate any request.
The Credential Pipeline
A credential written to a log file does not stay in one place. It travels:
1. Web server writes to /var/log/nginx/access.log
2. Log rotation agent (logrotate) copies to /var/log/nginx/access.log.1
3. Log shipper (Fluentd, Filebeat, Logstash) reads & ships to aggregator
4. Log aggregator (Elasticsearch, Splunk, Datadog) indexes & stores
5. Retained for 30-90 days under default policy
The credential exists in all five locations simultaneously. Revoking the session token does not remove the credential from the log aggregator. It remains searchable, exportable, & accessible to anyone with log access for the full retention window.
The Exposure Window
Exposure window for a credential in memory: max(session duration, process lifetime). Session: hours to days. Process: hours to weeks.
Exposure window for a credential in a log: max(session duration, log retention). Session: hours to days. Retention: 30-90 days.
A credential stolen from memory required the attacker to be present during the session window. A credential stolen from a log requires only access to the log aggregator, available retroactively, for the full retention period.
MOAD-0003 vs MOAD-0004
MOAD-0003 (Leaked Context): a credential in memory leaks to the wrong request handler. Accessible only during the process window, through the thread pool. Ephemeral.
MOAD-0004 (Logged Secret): a credential on disk persists through log rotation, log shipping, & log aggregation. Accessible retroactively, to anyone with log access, for 30-90 days. Persistent.
The structural difference: ephemeral vs persistent. The fix operates at a different layer.
Ephemeral vs Persistent
The ephemeral/persistent distinction determines the risk surface, the fix layer, & the incident response requirements.
Credential Denylist at the Serialization Layer
The fix: a credential denylist at the serialization layer. Before any header value reaches the log output, check the header name against a denylist. Replace the value with [REDACTED].
CREDENTIAL_HEADERS = {
'authorization',
'cookie',
'x-api-key',
'x-auth-token',
'x-csrf-token',
'proxy-authorization',
}
def sanitize_headers(headers: dict) -> dict:
return {
k: '[REDACTED]' if k.lower() in CREDENTIAL_HEADERS else v
for k, v in headers.items()
}
The denylist belongs at the serialization layer, not at the log query layer. Log query redaction: applies after the credential reached disk; the raw value still exists, just hidden from display. Serialization layer redaction: the credential never reaches disk. The raw value never enters the log file, the log shipper, or the log aggregator.
Testing the Denylist
Three test patterns:
- Positive: a request with Authorization: Bearer token123 produces a log entry with Authorization: [REDACTED]
- Negative: a request with Content-Type: application/json produces a log entry with the value intact
- Case-insensitive: AUTHORIZATION: Bearer token123 also produces [REDACTED] (HTTP header names case-insensitive)
The denylist requires maintenance: new credential header patterns (e.g., custom X-Service-Auth headers) need explicit addition. The fix is structural but not self-maintaining.
Apply the Denylist
A team configures their Nginx access log format to include all request headers for debugging a production incident. The configuration:
log_format debug_format '$remote_addr - $request - $http_authorization - $http_cookie';
access_log /var/log/nginx/debug.log debug_format;
They resolve the incident & intend to remove the debug configuration, but the change does not reach production before the next deployment cycle (7 days later).