How to log without leaking secrets.

•

•

Your Logs Are a Database of Passwords

I still remember the cold sweat I felt back at the logistics startup, staring at a terminal screen at 3:15 AM because our centralized logging cluster had just ingested a raw payload containing three hundred plaintext customer credit card numbers. We weren’t using some fancy, enterprise-grade security suite; we were just using a standard library that did exactly what it was told to do. It turns out that figuring out how to log without leaking secrets is rarely about buying a more expensive observability tool and almost always about the unsexy, manual work of intercepting data before it ever hits the disk.

I’m not here to sell you on a specific middleware or a complex regex-heavy framework that will break the moment a developer changes a field name. Instead, I want to walk through the practical, slightly boring patterns I’ve used to build redaction layers that actually work in production. We’ll look at structural ways to handle sensitive data, the trade-offs of scrubbing at the application level versus the collector level, and how to ensure your next production incident doesn’t turn into a compliance nightmare.

Table of Contents

The Cost of Failing at Preventing Credential Exposure in Logs

The Cost of Failing at Preventing Credential Exposure in Logs

The cost isn’t just a line item in a security audit; it’s the sheer, exhausting friction that follows a leak. When I was at the insurance firm, a single misconfigured middleware that dumped entire request objects into our ELK stack turned a routine patch into a week-long forensic nightmare. We weren’t just looking for bugs anymore; we were playing whack-a-mole with PII redaction techniques to ensure we hadn’t inadvertently archived thousands of customer social security numbers. You end up spending more time cleaning up the mess and explaining the “incident” to compliance than you ever spent actually building the feature.

Then there is the blast radius. If you aren’t rigorous about preventing credential exposure in logs, you aren’t just leaking a password; you’re handing over the keys to your entire observability stack. I’ve seen teams lose entire production environments because a developer logged a raw response from a third-party payment provider, and that response contained a bearer token. Once that token is in your telemetry, it’s effectively public. You can’t just “delete” a log entry easily once it’s been indexed and replicated across a distributed system. You’re stuck in a cycle of rotating every single secret that might have touched that stream, which is a massive, expensive distraction from actual engineering.

Why Generic Observability Security Compliance Isnt Enough

Why Generic Observability Security Compliance Isnt Enough

The problem with relying solely on observability security compliance is that it’s usually designed to satisfy an auditor, not a developer in the middle of a debugging session. Compliance checklists are great for checking boxes during a SOC2 audit, but they are notoriously bad at catching the nuance of a developer accidentally logging a `User` object that happens to contain a plaintext session token. You can have all the high-level monitoring in the world, but if your security posture is just a set of periodic scans, you’re still vulnerable to the moment of human error that happens between those scans.

I’ve seen teams implement heavy-handed automated secret detection in telemetry, thinking they’ve solved the problem. The reality is often a game of cat and mouse. If your detection pattern is too rigid, it misses the subtle stuff; if it’s too loose, it creates a mountain of false positives that developers eventually start ignoring. True prevention requires more than just a scanner at the edge of your pipeline; it requires actual sensitive data masking in application logs at the source. If you aren’t handling this at the code level, you aren’t actually securing your data—you’re just hoping your tools are faster than your mistakes.

A few things I’ve learned while cleaning up messes

  • Stop treating your logs like a debugging playground. I used to think dumping the entire request object was the fastest way to find a bug, but that’s just how you accidentally invite a credential leak. If you don’t need the full payload to understand the error, don’t log it. Pick specific fields and leave the rest out.
  • Use structural logging with a strict schema. If you’re just throwing strings into a file, you’re going to miss something. By using structured logs (JSON), you can actually implement filters at the library level that look for keys like `password`, `token`, or `secret` and automatically redact them before they ever hit the disk.
  • Implement “Scrubbing at the Edge.” I’ve found it’s much safer to have a centralized middleware or a logging interceptor that acts as a gatekeeper. It’s better to have a slightly more complex setup where every log entry passes through a regex-based scrubber than to rely on every single developer on the team remembering to manually mask sensitive strings.
  • Sanitize your error messages before they leave the application. It sounds obvious, but I’ve seen database drivers throw exceptions that include the full connection string—including the password—directly into the stack trace. Wrap your external calls in try-catch blocks that catch the “real” error but log a sanitized version for the observability stack.
  • Audit your log retention and access. Even if you manage to keep secrets out of the logs, the logs themselves are a goldmine for attackers if they aren’t protected. I treat my log access like I treat production database access: it should be restricted, audited, and definitely not something a junior dev can browse through without a specific reason.

What I’ve learned from the wreckage

Compliance is a checkbox; actual security is a grep command. You can pass an audit and still have a junior dev accidentally log an entire `User` object—including the hashed password—because they thought it would make debugging easier.

Stop trying to build a perfect “security layer” and start building better defaults. If your logging library doesn’t automatically redact common patterns like `Authorization` headers or `client_secret` fields, you’re just relying on every single engineer to remember the rules every single time they write a `log.info()` call.

Treat your logs like a liability, not just an asset. We spend so much time making sure we have enough telemetry to find a bug, but we rarely spend time asking how much sensitive data we are actually hoarding in our ELK stack. If you don’t have a plan to scrub it, you’re just building a treasure map for whoever eventually gets access to your observability tools.

Moving from Panic to Prevention

Moving from Panic to Prevention in security.

At the end of the day, preventing secret leakage isn’t about finding a magic library that solves everything; it’s about building a culture of defensive observability. We’ve talked about why compliance checklists fail, why generic security scans miss the context of a specific log line, and why you need to treat your log pipelines with the same scrutiny you give your production database. You need automated scrubbing, strict developer linting, and—most importantly—the realization that every log statement is a potential liability until proven otherwise. If you wait until you’re staring at a compromised credential in a plaintext JSON blob to implement these checks, you’ve already lost the battle.

I’ve spent enough time in the trenches to know that perfection is a moving target, but we can at least aim for predictable stability. My goal isn’t to build a system that is mathematically impossible to breach, but to build one where a single developer’s mistake at 2:00 AM doesn’t turn into a company-wide catastrophe. Write your code, write your tests, and for heaven’s sake, write your logs with the assumption that someone is watching. It makes the sleep much better, and it makes the post-mortems a lot less frequent.

Frequently Asked Questions

How do I actually implement a scrubber without adding so much latency that I end up breaking the very performance I'm trying to monitor?

The temptation is to run every log line through a heavy regex engine before it hits the disk, but that’s a great way to turn a minor latency spike into a full-blown outage. I’ve learned that you have to move the heavy lifting out of the critical path. Use a lightweight, non-blocking buffer to ship raw logs to a sidecar or a separate process. Let the sidecar do the expensive scrubbing asynchronously. It’s a trade-off: you risk a tiny window of exposure if the sidecar lags, but you won’t kill your application’s throughput.

If I start masking fields at the application level, how do I handle the edge case where a secret is nested deep inside a generic map or a complex JSON object?

This is where the “clean code” dream usually hits a wall. If you try to write a recursive function to scrub every possible key, you’ll eventually tank your CPU or miss something because someone decided to nest a token inside a `metadata` map. I’ve found it’s better to stop trying to be surgical. Instead, I use a structural approach: define a set of “danger keys” and use a middleware that flattens or intercepts the object before it ever hits the logger. It’s less elegant, but it’s predictable.

Is it better to try and catch these leaks at the source in the code, or should I be relying on a centralized log aggregator to scrub everything as it comes in?

I’ve seen both approaches fail, usually in different ways. If you rely solely on a central scrubber, you’re playing a game of whack-a-mole with regex patterns that will eventually miss something. If you only do it in code, you’re betting on every developer on your team being perfect—and I’ve worked with enough people to know that’s a losing bet. The only way to sleep at night is to do both: catch it at the source whenever possible, but keep the centralized scrubber as your safety net for when someone inevitably forgets.

How do I deal with the "false positive" problem—where the scrubber hides legitimate, non-sensitive data that looks like a token, making debugging a nightmare?

The “scrubber-induced blindness” is a real pain. I once spent three hours chasing a phantom bug because our regex decided a legitimate UUID was a session token and nuked it from the logs. To fix this, stop relying on pure pattern matching. Use context-aware scrubbing. Instead of just hunting for strings that “look like” keys, hook into your logger so it knows which fields are sensitive by design. It’s more work upfront, but it beats debugging a black hole.

Tomasz Wierzbicki

About Tomasz Wierzbicki

I am not interested in arguing about frameworks. I am interested in why the thing broke at two in the morning and what would have prevented it. Most of what I know came from being wrong in production and writing it down afterwards.