CodeKits.shopNew developer offer

Create useful audit logs without recording secrets

Create structured audit events with allowlisted actions and internal identifiers, while excluding tokens, credentials, request bodies and private messages.

By Jonathan · 4 min read

Published

Known action, Allowlisted fields, and Restricted audit sink
In this article

An audit log should answer who attempted an action, what object it concerned and whether it succeeded. It should not become an accidental second database of passwords, private messages and bearer links.

This tutorial provides a dependency-free audit-event serializer tested on Node.js 20.11.1. It is deliberately small: it constructs a known event shape instead of trying to remove every possible secret from an arbitrary request object. Storage durability, access policy and retention remain integration responsibilities.

Start with questions, not payloads

For a private download, useful questions include whether access was allowed, which verified actor requested it and which internal file record was involved. The raw download token and full URL are unnecessary.

Define a small vocabulary such as download.allowed, download.denied, link.revoked and admin.updated. Use explicit outcomes: success, denied or failed. Separate an expected denial from a system failure so alerts do not turn every expired link into an incident.

Use server-generated event and request identifiers. Accept an upstream correlation ID only from a trusted proxy contract and after validation; a random browser header is not automatically trustworthy evidence.

Construct an allowlisted event

The download exports auditRecord and serialiseAudit:

Project file tree
create-useful-audit-logs-without-recording-secrets/
├── README.md
├── audit.mjs
└── test.mjs
download-audit.mjs
import { serialiseAudit } from "./audit.mjs";
 
const line = serialiseAudit({
  action: "download.denied",
  outcome: "denied",
  actorId: verifiedActor?.id,
  objectId: internalFileId,
  requestId: serverRequestId
});
await auditSink.write(line);

The sink is an application integration boundary. The serializer does not know how to persist events. It validates action and outcome names, restricts identifier shapes and discards all unrecognized properties.

Shape validation is not secret detection. A bearer token made of letters and digits could fit the identifier pattern. Pass only verified internal IDs to these fields; never relabel a token as a request ID to bypass redaction.

OWASP's logging guidance recommends excluding sensitive data such as credentials and access tokens, sanitizing event data and protecting access to logs. This example applies those recommendations through a narrow event schema. OWASP logging guidance.

Keep operational errors separate

Do not attach an entire exception, request headers, cookies or JSON body to the audit event. Map failures to a small approved classification, and retain detailed operational diagnostics only in a restricted system with its own redaction rules.

A denied request may refer to a nonexistent object. Avoid copying an attacker-supplied filename into the trusted object field. You can record an internally assigned request ID without recording the unsafe value.

Structured JSON escapes newline characters, but it is still worth rejecting malformed identifiers and bounding all free text. A log viewer must render values as text, not HTML. Log storage is an input surface too.

Design the sink and retention policy

Decide what happens when audit storage is unavailable. For high-impact administrative changes, you may require a durable audit/outbox record in the same database transaction before acknowledging success. For lower-risk reads, a bounded asynchronous queue may be acceptable, provided dropped events are measured.

Never let an unbounded logging queue exhaust application memory. Alert on sink failures through a separate path, without recursively logging the same failure forever.

Restrict log access, use encryption in transit and at rest where appropriate, and set a retention period. Internal actor IDs can still be personal data when they can be linked to a person. Hashing an identifier does not automatically make a dataset anonymous.

Audit records are not automatically tamper-evident. If that is a requirement, add a separately controlled append-only sink, integrity checks and operational review. Do not call ordinary application logs an immutable ledger.

Test exclusions explicitly

Run node --test test.mjs. The suite passes a synthetic token, authorization header, private message, URL and newline-bearing request ID. It verifies that those values do not appear in serialized output and rejects unknown action names.

Then test the integrated pipeline: reverse-proxy access logs, tracing agents, error reporters and database slow-query logs may still capture secrets even when this serializer does not. Send a synthetic sentinel token through the full flow and search only the authorized test log stores for it.

If a secret appears, fix the capture point, not just the final viewer. Keep test credentials harmless and rotate any real credential accidentally logged. Pair this with server-side authorization, or discuss a Next.js application with auditable workflows.