Security logs are supposed to help when something goes wrong. They become a new security problem when they copy the very data you are trying to protect.
A login handler that records a submitted password, an API gateway that stores bearer tokens, or an error logger that captures an entire request body can move sensitive values into systems with different readers, retention periods, backups, and exports. A compromise of the logging path may then expose credentials or personal data even when the primary application database remains protected.
The defensive goal is not to log less blindly. It is to preserve the evidence needed to investigate security events while excluding values that are unnecessary for that purpose. This article develops that mental model, shows how to decide what belongs in an event, and explains where redaction helps and where it is too late.
Treat logging as another data flow
It is easy to think of a log call as harmless output:
request -> application -> response
|
+--> log messageFor security design, the log branch deserves the same attention as any other storage path. A value written there may flow onward to collectors, search indexes, dashboards, alerting systems, archives, backups, and support tools.
That creates a separate trust boundary. People or services that legitimately need to inspect operational events do not automatically need access to authentication secrets, session tokens, private message bodies, or complete payment details.
A useful rule is:
Record the fact needed for investigation, not the secret or sensitive content that happened to be nearby.
For example, an authentication event may need to say that a password login failed for a particular account identifier. It does not need the attempted password.
Start from the question the event must answer
Suppose a developer wants enough evidence to investigate repeated failed logins. A tempting event is a dump of the request:
login_failed
username=alex@example.test
password=<submitted value>
source_ip=203.0.113.20The password adds serious exposure but little investigative value. The useful questions are more likely to be:
- Which account was targeted?
- When did the attempt happen?
- Which source or client was involved?
- Which authentication mechanism was used?
- What broad outcome occurred?
A smaller event can answer those questions without storing the credential:
login_failed
account_id=usr_4821
source_ip=203.0.113.20
auth_method=password
reason=invalid_credentialThis is a simplified teaching example. Production events may need more context, and even fields such as source IP addresses can be sensitive and should have an explicit purpose and retention policy. The important change is that the schema is designed around an investigative question instead of around whatever data the application currently has in memory.
Prefer allowlisted event schemas over cleanup
There are two broad ways to keep sensitive values out of logs.
The first is to log a large object and remove fields that look dangerous:
request object
-> serialize everything
-> redact password, token, secret, ...
-> logThe second is to construct the event from fields that are intentionally permitted:
application state
-> select event_type, account_id, outcome, source
-> logThe second model is usually easier to reason about. A denylist must anticipate every sensitive field name, nested location, alias, and new field introduced later. An allowlisted schema starts with nothing and adds only values that have a defined logging purpose.
This does not make redaction useless. Redaction is valuable as defense in depth, especially around framework logs, exception handlers, HTTP tracing, and third-party components that may emit data outside your application event schema. But a redaction filter should not be the primary reason an application is allowed to hand complete secret-bearing objects to the logging system.
Classify values by what the log needs to prove
A practical design separates values into three groups.
Some values are useful as recorded facts. Event type, timestamp, stable internal actor identifier, action, resource identifier, and outcome often belong here when they support investigation.
Some values are useful only for correlation. You may need to know whether two events involved the same token or client without retaining the original value. In those cases, a purpose-built non-secret identifier is preferable when one exists. If the system must derive a correlation value from secret material, use a keyed construction designed for that purpose rather than a plain hash of a low-entropy secret. A plain hash can still permit guessing when the original value comes from a small or predictable space.
Other values normally do not belong in security logs at all. Passwords, recovery codes, private keys, session identifiers, bearer tokens, API secrets, and full authentication headers are examples. Possession of many of these values can grant authority, so copying them into logs expands the places from which that authority can leak.
Sensitive business data deserves the same reasoning even when it is not a credential. Ask whether the event needs the full value, a stable identifier, a coarse classification, a count, or merely the fact that the field was present.
Remove sensitive data before the logging boundary
Where filtering happens matters.
If an application sends a raw request to a central collector and the collector redacts it later, the sensitive value has already crossed a boundary. It may appear in network buffers, ingestion queues, rejected-event storage, collector diagnostics, or temporary processing state before the filter runs.
Prefer to construct the safe event as close as practical to the application code that understands the data:
secret-bearing request
|
v
application understands field meaning
|
+--> perform operation
|
+--> construct safe event
|
v
logging pipelineCentral filters still provide useful backup protection, but they should catch mistakes rather than define the first safe boundary.
This is especially important for exception handling. Generic error capture can accidentally serialize local variables, request bodies, headers, or objects whose string representations contain secrets. Configure error reporting deliberately and test what an actual captured event contains instead of assuming the library’s defaults match your threat model.
Preserve evidence without preserving credentials
Removing secrets does not mean making events vague.
Imagine an API request authenticated by a bearer token. An investigation may need to determine which credential record was used, but it does not need the bearer value itself. If your credential store assigns each credential a stable internal ID, log that ID:
api_request_denied
credential_id=cred_7319
principal_id=svc_204
resource_id=report_18
reason=insufficient_scopeThe credential ID lets responders correlate events and inspect the credential’s metadata through an authorized system. It cannot by itself be presented as the bearer credential.
This distinction is reusable: identity is often useful evidence; authenticating material is usually not. Design systems so those two concepts have separate identifiers.
The same approach works for objects containing personal data. A security event can often reference an internal record ID rather than copy the record’s sensitive fields. Investigators who are authorized to see the underlying data can retrieve it through the normal protected path when necessary.
Make accidental logging difficult
Good intentions are fragile when logging APIs accept arbitrary strings and objects. Put safer behavior into the interface.
Structured event types can define the fields an event is allowed to contain. Shared logging helpers can reject known secret-bearing types. HTTP middleware can exclude authentication headers and request bodies by default, with narrow opt-in rules for fields that have a documented operational need.
Code review also becomes easier when a log statement looks like this:
record_security_event(
event="credential_revoked",
credential_id=credential.id,
actor_id=current_user.id
)rather than this:
log("credential revoked", credential, request)The first form exposes the intended data contract. The second asks reviewers to know how two complex objects will be serialized now and after future changes.
For high-risk applications, add automated tests that send distinctive synthetic sensitive values through important flows and assert that those values do not appear in captured logs. This verifies behavior at the output boundary rather than only checking configuration.
Plan for the cases that still escape
No logging design can guarantee that sensitive data will never be emitted. A new dependency may log unexpected details, a debugging flag may change behavior, or an operator may add temporary instrumentation during an incident.
Defense in depth limits the damage of those failures. Restrict access to log stores, separate administrative roles where practical, encrypt transport and storage according to the environment’s threat model, define retention periods, and include logging systems in incident response and deletion procedures.
Shorter retention reduces the time during which an accidentally logged value remains exposed, but retention is not a substitute for exclusion. A bearer token that is valid for ten minutes can still be dangerous if it is copied to a broadly readable log for five minutes.
Similarly, access control on the log platform does not justify recording passwords. The application should avoid creating the unnecessary copy in the first place.
Know what this control does not solve
Keeping sensitive data out of logs reduces exposure through logging infrastructure. It does not protect a secret while the application legitimately processes it, stop an attacker who already controls the application process, or replace secure credential storage and rotation.
It also does not guarantee privacy merely because obvious credentials are absent. Combinations of identifiers, network addresses, resource names, timestamps, and other metadata can still be sensitive. The appropriate fields and retention period depend on the application’s threat model, investigative needs, and data obligations.
There is a real trade-off with observability. Removing too much context can make incidents harder to investigate. The answer is not indiscriminate collection. Define the questions responders need to answer, record the minimum evidence that answers them, and provide controlled paths to deeper data when an investigation genuinely requires it.
Verify the control with realistic tests
Reviewing source code is useful, but the final log output is what matters. Test representative flows such as successful and failed authentication, token rejection, password reset, file upload errors, malformed requests, and unhandled exceptions.
Use synthetic marker values that resemble the kinds of data you want to exclude, then inspect every relevant destination: local application output, centralized events, error reporting, traces, alert payloads, and archived copies created by the test environment.
A useful verification question is simple:
If this synthetic secret entered the application,
can I find its original value anywhere in the observability pipeline?If the answer is yes, identify which component emitted it and move the exclusion earlier where possible. Then keep downstream filtering as a second layer.
Conclusion
Security logs should preserve evidence, not duplicate authority or sensitive content. Start each event from the investigative question it must answer, construct structured events from allowlisted fields, use non-secret identifiers for correlation, and remove sensitive values before they cross into the logging pipeline.
Then assume mistakes can still happen. Restrict log access, control retention, test real output paths, and treat redaction as defense in depth. The result is a logging system that remains useful during an incident without quietly becoming another high-value store of credentials and private data.