Mystery DigitalTalk to us
Secrets management beyond the .env file
SecuritySecrets Management

Secrets management beyond the .env file

Marcus Whitfield · 2026-06-11 · 4 min read

Every engineering org starts the same way. A .env file, gitignored, copied by hand or through a 1Password note called something like "prod secrets," loaded by dotenv at boot. It works, and for a five-person team running two services it's genuinely the right call: anything more sophisticated is over-engineering at that scale. The problem is that nobody ever picks a deliberate moment to stop doing it. The file just keeps growing until one day it has forty keys in it, six services read it, and nobody on the team can tell you with confidence which of those keys are still used.

Where it actually breaks

The failure isn't that .env files are insecure in some abstract sense. It's that they collapse three things that need to stay separate as a system grows: identity, scope, and lifetime.

A .env file has no identity. The database password in it isn't "the payments service's database password," it's just a string, and any process that can read the file gets it, whether that process needed it or not. We've walked into engagements where a reporting job pulled from the same .env as the primary API purely because it was easier to inherit the file than provision new credentials, and that job's dependency on an internal npm registry became the path a compromised package used to reach the production database.

It has no scope. One shared file, or one shared vault note, tends to accumulate credentials for every environment and every downstream system a team has ever touched, because removing a line feels riskier than leaving it. Nobody wants to be the one who deletes a Stripe key and breaks billing three deploys later. So the file only grows, and every service pulling from it holds credentials it doesn't need, for systems it doesn't talk to.

And it has no lifetime. A database password sitting in a .env file is not rotating on any schedule, it's rotating when someone remembers, or after an incident forces the question. We've seen production database credentials that were four years old, predating half the engineers who now had access to them through onboarding scripts nobody had audited.

None of this matters much at five services. It matters enormously at fifty, because the blast radius of one leaked file scales with everything that reads it, and past a certain size, something is always reading it: a CI job, a contractor's laptop, a staging environment that quietly points at prod.

What replaces it

The shift that actually fixes this isn't "use a fancier secrets store instead of a text file." It's separating where a secret is issued from where it's consumed, and making both scoped and time-limited by default.

Concretely: services authenticate to a secrets broker (Vault, AWS Secrets Manager with IAM, GCP Secret Manager with workload identity federation) using an identity they already have, a Kubernetes service account, an EC2 instance role, a CI job's OIDC token, rather than a static credential baked into their environment. The broker issues short-lived, narrowly scoped credentials at runtime: this specific service, this specific database, a token that expires in an hour instead of a password valid until someone remembers to change it. Vault's dynamic database secrets engine is the clean version of this: it creates a new database user with a bounded TTL on every lease request and revokes it automatically on expiry, so there's no long-lived credential to leak in the first place.

Rotation stops being a manual, coordinated event and becomes a property of the system. When credentials expire in an hour by default, "rotation" is just what happens when nobody's watching. The engineering work moves from "who has to know about this rotation and update their config" to "does every service correctly handle a mid-request credential refresh," which is a much smaller and more testable problem.

Access scoping becomes policy, not tribal knowledge. A Vault policy or an IAM role attached to a workload identity is an explicit, reviewable statement of what that service can read, and it shows up in a diff. Compare that to a .env file where the honest answer to "what can the notifications service access" is "open the file and read every line," which nobody does until something has already gone wrong.

The migration is smaller than it looks

Teams put this off because it sounds like a quarter of infrastructure work. In practice the highest-leverage first step is narrow: move your database and third-party API credentials to a broker with per-service scoping, leave lower-stakes config (feature flags, non-secret settings) in plain environment variables, and don't try to migrate everything on day one. The goal isn't zero secrets in environment variables. It's zero standing credentials that outlive their need and zero shared files whose blast radius is "everything that ever read it."

← Back to blog