Twelve-Factor Config Research — and Its Blind Spot for Secrets
A rule that reshaped how apps are configured
If you’ve set DATABASE_URL or STRIPE_SECRET_KEY as an environment variable instead of
writing it into a config file, you’re following a rule that has a name and a source: The
Twelve-Factor App, written by Adam Wiggins at Heroku and published
in 2011. Factor III states it without hedging: “the twelve-factor app stores config in
environment variables.” Its stated reason is architectural discipline — “strict separation of
config from code” — because, as the methodology puts it, “config varies substantially across
deploys, code does not.”
That rule won. It’s the default pattern in Docker, Kubernetes, every PaaS, and nearly every framework’s documentation. And the underlying problem it targets is real, not aesthetic.
The measured cost of getting config wrong
A peer-reviewed study at SOSP 2011 (Yin, Ma, Zheng, Zhou, Bairavasundaram, and Pasupathy) examined 546 real-world misconfigurations across a commercial storage system and several open-source projects. 70–85.5% were simple parameter mistakes — not exotic bugs, but the kind of error that externalizing config into a single, consistent mechanism is specifically meant to reduce. Twelve-Factor’s push toward environment variables for config is, in that light, a genuine response to a documented failure mode, not just a stylistic preference from one company’s engineering blog.
Where the definition quietly goes too far
Here’s the gap: Twelve-Factor defines config as “everything that is likely to vary between deploys,” and its own examples list resource handles, per-deploy values, and credentials side by side, with no distinct handling for the third category. A port number and a database password both satisfy the definition. Nothing in the methodology tells you they need different protection.
Why the same mechanism is a different risk for secrets
OWASP’s Secrets Management Cheat Sheet
addresses this gap directly: “environment variables are generally accessible to all processes
and may be included in logs or system dumps. Using environment variables is therefore not
recommended unless the other methods are not possible.” That’s not a vague warning — it has a
concrete technical basis. Linux’s own proc_pid_environ(5) man
page confirms that a process’s
environment is readable directly from /proc/[pid]/environ by anything with ptrace-equivalent
access to that process — not a hypothetical side channel, a documented, standard OS interface.
Child processes inherit the full parent environment by default, too, so a single secret set at
the top of a deploy fans out to every subprocess whether or not it needs that credential.
The scale this produces in practice
Treating secrets exactly like ordinary config — no separate lifecycle, no distinct storage — is
a large part of why they end up committed. GitGuardian’s State of Secrets Sprawl
2024 found 12.8 million new
secrets leaked on public GitHub in 2023 (up 28% year over year), and — more telling — that
more than 90% of exposed secrets were still valid five or more days later. If a leaked
credential were treated with the urgency its actual risk deserves, that number would be far
lower. It’s high because the credential was never handled as anything more sensitive than a
LOG_LEVEL value in the first place.
Keeping the ergonomics, fixing the storage
None of this is an argument against injecting secrets as environment variables at runtime —
that part of the twelve-factor pattern is genuinely convenient, and most application code
expects it. The fix belongs one layer earlier: what’s stored durably, in git, on disk, or in a
shared file, should never be the plaintext value. envseal keeps the developer
experience twelve-factor promises — secrets still arrive as ordinary environment variables when
the process runs — while encrypting each one individually with age
for the only artifact that’s actually committed or shared. The .env file Twelve-Factor
popularized becomes ciphertext at rest and plaintext only inside the running process, for as
long as it’s needed.
The same conflation shows up on the collaboration side, too: a “config value” pasted into a Confluence runbook or a Jira ticket while debugging a deploy gets zero scrutiny, right up until it turns out to be a real credential. That’s the surface Secret Sentinel watches — the parts of the twelve-factor pattern that never touch git at all.
Frequently asked questions
Does the Twelve-Factor App actually recommend putting secrets in environment variables?
Yes, literally. Factor III says "the twelve-factor app stores config in environment variables" and defines config as "everything that is likely to vary between deploys" — a definition broad enough to include database URLs and API keys without distinguishing them from a port number or a log level.
Is there real evidence that misconfiguration causes production failures?
Yes. A peer-reviewed 2011 study at SOSP examined 546 real-world misconfigurations across commercial and open-source systems and found that 70–85.5% were simple parameter mistakes — strong empirical support for the twelve-factor goal of externalizing and standardizing config in the first place.
If Twelve-Factor recommends environment variables, why does OWASP discourage them for secrets specifically?
Because a process's environment is a different security boundary than its config value. OWASP's Secrets Management Cheat Sheet states environment variables "are generally accessible to all processes and may be included in logs or system dumps," and Linux's own proc(5) documentation confirms any process with ptrace-equivalent access can read another process's environment directly from /proc/[pid]/environ.
What's the practical fix that keeps the twelve-factor developer experience?
Keep secrets arriving in the app's environment at runtime — that ergonomic pattern isn't the problem — but stop storing the plaintext value anywhere long-lived. envseal encrypts each secret individually and injects it into the process environment only at execution time, so the durable, committed artifact in git is ciphertext, not a plaintext .env file.