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.

Diagram: Twelve-Factor App's single 'config' category splits in practice into ordinary config, like a port number or log level that is safe to expose, and secrets, like an API key or database password, which need injection, rotation, and access control that the config-in-env-vars pattern alone does not provide.

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.