The comment shows up in code reviews all the time: “We moved it to env vars, we’re good.” A database password that had been sitting in source code is now in an environment variable. Everyone approves. The PRpull requesta developer's proposal to merge code changes into the main codebase, typically reviewed by teammates before it lands merges. Case closed.

Except nothing actually changed. The secret still exists, readable, in plain text. It just lives somewhere else now.

Imagine someone tells you “don’t leave your house key under the doormat.” Smart. So instead, you write “key is taped behind the mailbox” on a sticky note. Then you leave that sticky note on your desk. You mention it in your team chat. It shows up in the build logs. You haven’t solved the problem. You’ve just moved the sticky note.

That’s what a .env file is. A sticky note with your secret’s hiding spot written on it. Out of the code, into a file, or a build pipeline setting, or a value handed to the application when it starts. Job done. Move on.

The trouble is that a sticky note is still a sticky note wherever you put it.

Why environment variables feel safe

The appeal of environment variables is that they separate configuration from code. Your source code gets shared and deployed everywhere. The secret stays local, on the machine, never checked in. The sticky note stays in your house instead of going to the office.

Secrets in source code end up in version control history permanently. Even if you delete the line, it’s in a previous commit. Anyone with repository access can find it. Bots actively scan public repositories for accidentally committed credentials, and they find them within seconds.

Environment variables solve that problem. But that’s where the protection ends.

The .env file problem

In practice, environment variables on a developer’s machine usually live in a .env file in the project root. Every framework has a library that reads this file and injects the values into the runtime environment.

The .env file is supposed to be in your .gitignore (a file that tells version control to skip certain files when saving code history). And it usually is. But “usually” isn’t “always.”

Search any public code hosting platform for .env files and the results keep going. Database URLsUniform Resource Locatorthe address that points to a specific page, file, or service on the internet with passwords. Payment processor APIApplication Programming Interfacethe way two software systems talk to each otherLearn more in The Bouncer That Confuses Everyone → keys. Cloud provider credentials. All sitting in public repositories, search engines indexing them, available to anyone. That’s the sticky note blowing off your desk and landing on the sidewalk.

Even when the .env file is properly ignored, it’s still a plain-text file on every developer’s laptop. A compromised laptop means compromised credentials. The developer copies it to set up a new machine. Pastes it into team chat. Now those values live on that chat provider’s servers. Build a container image in a directory that contains the .env file, and the credentials get baked into that image, the packaged snapshot your application gets copied from every time it runs.

But there’s a leak nobody saw coming. Developers using AI coding assistants grant these tools access to their project directories. If the assistant can read your codebase, it can read your .env file. That sticky note on your desk? Your AI assistant just read it too. Whether those values end up in logs, suggestions, or training data depends entirely on the tool’s data handling practices. Most developers never think to ask.

The .env file isn’t a security mechanism. It’s a convenience that happens to keep credentials out of one specific place. It does nothing to protect them everywhere else they end up.

So the secret escapes your laptop. The build system handles this better. Right?

Build pipelines

The leaks that keep coming up aren’t in the source code. They’re in the system that builds it, and the story is always mundane. These pipelines need to push to production, access databases, call external APIsApplication Programming Interfacethe way two software systems talk to each otherLearn more in The Bouncer That Confuses Everyone →. So teams store credentials in the pipeline’s settings, the same way you might save a password in your browser.

That’s a step up from hardcoding them. But build systems leak credentials constantly, and almost always by accident.

Build logs are the biggest culprit. Environment variables show up in debugging output when a pipeline is misconfigured. A failing test dumps the full environment. Someone echoes a variable to confirm it was set. And just like that, the value is in the build log, which might be accessible to everyone on the team, or in some cases, publicly visible. The sticky note just fell off your desk and landed in the hallway. Anyone walking by can read it.

Most build platforms try to mask values in logs by replacing them with asterisks. But masking is best effort. If the value is Base64 encoded (reformatted into text that looks like gibberish but converts right back to the original), the masking might not catch it. The same goes for URLUniform Resource Locatorthe address that points to a specific page, file, or service on the internet-encoded values, values split across multiple lines, or values embedded in a JSONJavaScript Object Notationa standard format for structuring data that both humans and machines can read object.

And then there are the credentials that live in build pipelines for years. The cloud provider key that was added by an engineer who left the company three years ago. Nobody remembers what it’s for. Nobody dares to delete it because something might break. It’s never been rotated. It still works.

If the build system can’t keep the sticky note hidden, production must be where it finally gets locked away. That’s the promise, anyway.

The illusion of encryption

Production is where the sticky note gets its most convincing disguise. Applications ship as containers now, and the credential gets handed to the container as an environment variable the moment it starts, written into the deployment file that describes how the application should run.

So the note moves into a locked room. That sounds like progress. But every process running inside that room can read it. The value sits in the container’s memory, visible to anyone who knows where to look. It shows up when you inspect the container’s configuration. And if you’re running any other software alongside your application in that same room, it reads every environment variable too.

The platforms that manage all these containers offer their own “secret” storage, and the name is doing a lot of work. The values are Base64 encoded, which, as we saw earlier, is a formatting trick, not a security measure. Anyone with read access to the configuration can decode them instantly. The label says “secret.” The reality says “slightly obscured plain text.”

When you stop writing things down

So where does the sticky note analogy break down? When you stop writing things down at all.

A dedicated secrets management system doesn’t leave credentials sitting in files, environment variables, or deployment specs. Your application reaches into a locked vault at the moment it needs the value, uses it, and the vault closes behind it. No note on a desk. No note in a hallway. Nothing written down anywhere.

The difference between that vault and a folder with a password on it comes down to four things. It scrambles the values so they’re meaningless without the right key, even to someone who walks off with the storage itself. It hands out access one person and one service at a time, instead of to everyone who can reach the system. It rotates credentials on its own, so anything that leaks has a short shelf life. And it records every read, so “who opened this, and when?” becomes a question with an answer.

That last one is the tell. If you can’t answer it, you don’t have secrets management. You have storage with extra steps.

Building this takes real investment. Access rules, operational knowledge, and the discipline to maintain it long after the launch. Not every team is ready for that overnight. Encrypting your .env files is a start. Your cloud provider’s built-in secret storage goes further, with values scrambled and access controlled at the platform level. Automated checks in your build pipeline can catch an exposed credential before it reaches production.

None of these are the vault. All of them beat a sticky note taped to the underside of a desk.

The uncomfortable question

The rule shouldn’t be “don’t put secrets in your code.” That’s a start, but it’s not enough. The rule should be “know where every secret lives, who can access it, when it was last rotated, and what happens if it’s compromised.”

Most teams can answer the first part. Very few can answer the rest.

How many credentials does your team have right now that nobody can account for? Keys set by people who left. Values that haven’t been rotated in years. .env files copied across laptops, pasted into chat threads, cached in tools nobody’s audited. Every one of those is a sticky note somebody left somewhere.

If one of them leaked tomorrow, would you even know which one it was?

Previously on Off White Paper: the number dispenser looked at what keeps attackers from taking unlimited tries at your system. This post looks at what they’re trying to reach with those tries.