DEV Community

Riley Lin
Riley Lin

Posted on

Stop the Send When a Log Field Is Still Hot

You should treat every log line as a packed envelope, because a model receives each field you forgot to strip before the question. A stack trace looks like a harmless debugging aid, yet it often carries hostnames, query fragments, and headers never written for a stranger. If one field inside that line is still hot, you should stop the entire send, even when the surrounding text looks dull and operational. That standard is stricter than a vague caution, and it is the only standard that still holds on a tired incident afternoon.

Picture the trust boundary as a doorway rather than as the chat box where you type the question. The doorway is the moment those bytes are copied into a request, a ticket, or a file uploaded for an outside explanation. Before that copy happens, the log still sits in a collector you administer, and you can still delete a field without negotiating with anyone. After the copy, you are asking another process to remember a slice of your system, and a later edit cannot pull that field back.

Hot fields hide inside ordinary lines, which is why a quick visual scan so often misses them during an incident. A debug logger may print an Authorization header because someone left verbose middleware enabled while chasing a timeout. An exception message may embed a database URL because the driver helpfully printed the target it could not reach. A user email may sit in a template variable that an error formatter treated as harmless context rather than as personal data.

Identifiers deserve the same suspicion you already give to passwords, because they join records that were never meant to meet. A request identifier can connect a public paste to an internal trace when both sides preserve the same value. An internal address beside a service name sketches how your network is wired, even when no password appears on the line. A file path can reveal a home directory, a tenant name, or a deploy layout you would not publish in a status note.

The practical move is a local gate that classifies each field before any model is allowed to see the file. You run that gate on the machine holding the log, then you forward only the redacted copy after reading the blocked key report. The gate works as a speed bump rather than a compliance certificate, making the dangerous send harder than the safe one.

The script below is a proposed local check, and it is not a record of a run inside your environment or a certified control. It reads JSON lines, refuses keys you name, and masks values that look like tokens, mail addresses, or connection strings. You should extend the patterns for your own stack, and you should keep the raw file off every request path.

#!/usr/bin/env python3
'''Proposed local gate. Not a certified control. Synthetic fixtures only.'''
import json
import sys
from pathlib import Path

HOT_KEYS = {
    'authorization', 'cookie', 'set-cookie', 'password',
    'secret', 'api_key', 'access_token', 'refresh_token',
    'connection_string', 'private_key',
}

def has_email(text):
    cleaned = text.replace(',', ' ').replace(';', ' ')
    for token in cleaned.split():
        if token.count('@') != 1:
            continue
        local, domain = token.split('@')
        if local and '.' in domain and ':' not in local:
            return True
    return False

def has_bearer(text):
    return 'Bearer ' in text

def has_url_password(text):
    start = text.find('://')
    if start < 0:
        return False
    at = text.find('@', start + 3)
    if at < 0:
        return False
    return ':' in text[start + 3:at]

def has_private_key(text):
    return '-----BEGIN ' in text and 'PRIVATE KEY-----' in text

def classify(key, value):
    reasons = []
    if str(key).lower() in HOT_KEYS:
        reasons.append('hot_key')
    text = value if isinstance(value, str) else json.dumps(value)
    if has_email(text):
        reasons.append('email')
    if has_bearer(text):
        reasons.append('bearer')
    if has_url_password(text):
        reasons.append('url_secret')
    if has_private_key(text):
        reasons.append('private_key')
    return reasons

def redact(record):
    blocked = []
    clean = {}
    for key, value in record.items():
        reasons = classify(key, value)
        if reasons:
            blocked.append({'key': key, 'reasons': reasons})
            clean[key] = '[REDACTED]'
        else:
            clean[key] = value
    return clean, blocked

def main():
    src, dst, report_path = map(Path, sys.argv[1:4])
    findings = []
    with src.open() as incoming, dst.open('w') as outgoing:
        for index, line in enumerate(incoming, 1):
            clean, blocked = redact(json.loads(line))
            if blocked:
                findings.append({'line': index, 'blocked': blocked})
            outgoing.write(json.dumps(clean) + '\n')
    report_path.write_text(json.dumps(findings, indent=2))
    # Exit 2 means the send stays stopped until a human reviews the report.
    sys.exit(2 if findings else 0)

if __name__ == '__main__':
    main()
Enter fullscreen mode Exit fullscreen mode

A matching fixture keeps the lesson concrete without placing a live secret into an article or a shared channel. Save the two lines below as sample.jsonl; a correct run of this proposed gate should exit nonzero, since both lines still carry heat. The first line would survive a key-only denylist, because the hot value sits in msg rather than in a field named password. The second line hides a user email and a database URL that includes a password in the userinfo section.

{"level":"error","msg":"upstream rejected Bearer EXAMPLE.not.a.real.token","service":"billing"}
{"level":"error","user":"ada@example.com","msg":"connect failed postgres://app:example-pass@db.internal:5432/app"}
Enter fullscreen mode Exit fullscreen mode

You run the gate from the shell, you print the status, and you inspect the report before any paste leaves the machine. A nonzero status is the intended signal rather than a broken tool, because it means at least one field was still hot. The redacted file is then the only candidate for a later question, and even that candidate deserves a human glance before it moves. If the report names a key you need, swap that value for a fake label like tenant T1 instead of restoring it.

python3 redact_gate.py sample.jsonl redacted.jsonl report.json
echo gate_status=$?
python3 -m json.tool report.json
Enter fullscreen mode Exit fullscreen mode

What you withhold is broader than passwords, because the model may carry a labeled box but not the contents of your desk. You should not send raw authorization headers, session cookies, recovery codes, connection strings, or private key blocks, even when they sit inside an exception. You should not send production request bodies, because a body can hold a card number or a note that no pattern list anticipated. You should not send an unredacted trace, because one hot field can repeat across services and make the leak wider than a single line.

Free capacity does not shrink that list, yet a clean report can justify a narrower question about the redacted file alone. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option you can try for that redacted file, provided you never upload the raw log. Those are availability claims only, with no quota, hardware, duration, or permanence stated here, and a free server stays outside your boundary.

You should skip this approach when the data is regulated and must not leave your network even after a redaction pass. You should not treat the script as a substitute for a reviewed data-loss product, a signed vendor agreement, or a secrets manager. You should not point the script at a live production stream and treat a clean exit as proof that nothing sensitive remains. Regular expressions miss new formats and custom names, so a green status is a hint rather than a warranty for an auditor.

If the incident truly requires the raw secret in order to proceed, you rotate that secret instead of teaching it to any model. The habit is small enough for a busy week: classify the line, keep the raw file local, and ask only about a clean copy. When the report is empty, a remote explanation of that copy is optional, and when the report is not empty, the send stays stopped.

Top comments (0)