Security
Environment Variables Are Not a Secrets Manager
Moving a password out of a config file into an environment variable is a real improvement over committing plaintext, but a process environment is readable by anyone with host access — it's a step up, not a vault.
- Security
- Secrets
- Configuration
- Operations
I wanted to get a service-account password out of a config file. The file was checked into version control and shipped inside the deployment artifact, which meant a credential was sitting in git history and riding along to every box the artifact landed on. The plan was the standard one: pull the secret into an environment variable and reference it from the config. That plan is correct, and it’s a real improvement — but along the way I hit two things worth saying out loud, because they’re where people quietly get this wrong. Your config probably doesn’t expand variables the way you assume, and an environment variable is not a vault.
Getting the secret out of the file is the real win
Start with what actually matters: the win here is that the credential stops living in version control and stops being baked into the artifact. That removes it from git history, from code review, from every clone and every build cache. Whatever else is true about environment variables, achieving that is worth doing. Plaintext secrets in a repo are one of the most common and most damaging mistakes there is, and this kills that specific failure mode.
So: good instinct, right direction. The nuance is about not overestimating what you bought.
Your config file probably doesn’t expand variables by magic
The first surprise: I dropped a ${SECRET}-style reference into a config value,
expected the runtime to substitute the environment variable at load, and it did
nothing. The literal string ${SECRET} came through verbatim. The runtime performed
no substitution when it read the file.
This bites people constantly. We assume config files interpolate environment variables because some do — but plenty don’t, or only do it in specific contexts. Substitution happens where something explicitly evaluates the value, not just because you wrote a variable-looking token into a file. In my case the value only resolved when the application’s own code, at the point of use, ran an explicit evaluate step on it.
Know your substitution boundary. “I wrote
${VAR}in a file” and “the system expands${VAR}” are different facts — verify which one is true instead of assuming the magic.
There’s a sharp edge on that evaluate step, too: naively evaluating every value
broke on plain literals that contained no variable at all — a static password with
no ${...} in it threw an error instead of passing through. The robust version had
to handle both shapes: a templated value to expand, and a plain literal to leave
alone. If you’re going to lean on interpolation, test it against both, because a
mixed fleet will have both.
A process environment is not a vault
The second thing is the one that actually matters for security posture. Once the secret resolves, it lives in the process environment — and a process environment is not private. Anyone with sufficient access to the host can read it:
/proc/<pid>/environexposes a process’s environment.ps ewwcan print it.- A container runtime’s
inspectcommand will happily show it.
So an environment variable is “much better than plaintext committed to a repo,” not “a secrets manager.” It moves the secret out of source control, which is the real win, but it does not give you encryption at rest, access auditing, rotation, or fine-grained authorization. Treating it as though it does is how a credential ends up casually exposed to anyone who can run a command on the box. It’s a step up the ladder, not the top of it — the threat model decides whether it’s high enough, the same way where encryption belongs is a threat-model question, not a default.
Reduce the exposure you can
If the environment-variable approach fits your threat model, tighten the obvious edges rather than leaving them loose:
- Prefer an environment file over an inline value. A root-owned,
0600environment file keeps the secret out of service metadata and process listings that an inline definition would leak. The variable still ends up in the process environment, but you’ve stopped advertising it in extra places. - Never log the resolved value. It’s easy to “just print it once to confirm,” and that line outlives the debugging session in a log file you forgot about.
- Scope host access. Since host access equals secret access here, the people who can run commands on the box are your authorization boundary. Act like it.
Know the reload semantics
One operational footgun: an environment variable is read at process start. If you change the secret, reloading the config file is not enough — the file gets re-read but the environment value is fixed at launch, so the process keeps using the old one until it fully restarts. People expect a config reload to pick up the new secret and are baffled when it doesn’t. Rotating the credential means restarting the process, full stop. Write that down where the next person will look.
Match the mechanism to the threat model
None of this is an argument against environment variables. For a lab service, an internal tool, or anything where “not in the repo, restricted host access” is a proportionate bar, an environment file is a reasonable, low-friction middle ground. The mistake is using it for high-value, long-lived secrets that genuinely need rotation, audit trails, and authorization — that’s what a real secrets store or KMS is for, and reaching for one is the right call when the stakes warrant it.
The throughline is the same one I keep coming back to with secrets: there’s a ladder, not a binary. Plaintext in git is the bottom rung; keeping secrets out of git entirely is the floor you should never be below; an environment file is a real step up; a managed vault is the top. Know which rung you’re on, why it’s high enough for this secret, and what’s above you if it isn’t. If you’ve drawn that line differently for your own systems, I’m curious where.