A year ago I'd have told you a .env file was fine.
Then we patched a CVSS 10.0 RCE in Next.js (CVE-2025-66478) and spent the next two days rotating every secret we owned — because we couldn't prove which ones an attacker could have read. They were all sitting in process.env. One env dump away from gone.
That incident is why I built @faizahmed/secret-keystore.
The actual problem isn't committing .env
Everyone knows not to commit secrets. The part that hurts you is what happens the moment your process is compromised. The default Node setup:
require('dotenv').config(); // every secret → process.env, at startup
Attacker gets code execution (a dep RCE, an SSRF, a framework CVE). Their first move:
env
One line. Every DB password, API key, and JWT secret you own, in plaintext, in one place. That's your blast radius — and then you're rotating everything and hoping, because you can't prove what leaked.
The idea: a KMS Key ID is not a secret
The whole design rests on one decision: the only thing a developer ever handles is an AWS KMS Key ID — which isn't sensitive. It's a pointer. The key material never leaves KMS, and access is gated by IAM. No private keys, no passphrases, nothing for anyone to leak.
Your .env stores ciphertext:
DB_PASSWORD=ENC[AQICAHh2nZPq...]
API_KEY=ENC[AQICAHh2nZPq...]
At runtime, values are decrypted on demand into an in-memory store — and never put back into process.env. So the next RCE leaks the handful of keys your code actually touched, not the entire vault.
In practice
npm install @faizahmed/secret-keystore
# encrypt the secrets in your .env (in place)
npx @faizahmed/secret-keystore encrypt --kms-key-id="alias/my-key"
Load them at runtime without ever touching process.env:
const { config } = require('@faizahmed/secret-keystore');
const secrets = await config({ kmsKeyId: 'alias/my-key' });
const dbPassword = secrets.get('DB_PASSWORD');
// decrypted, in memory only
Or, for an app you don't want to modify, inject into the child process and run it:
npx @faizahmed/secret-keystore run \
--kms-key-id="alias/my-key" -- \
node server.js
There's also rotate, edit, keys, status, and import — plus optional AWS Nitro Enclave attestation when you need to prove what's running.
Being honest about what it does NOT do
It's not magic, and the README says so. An attacker with full code execution inside your process can still call the keystore or scrape memory. You still patch and rotate!
What it removes is bulk exposure: no single env dump that hands over everything, no plaintext in git, and secret access that's per-key and grep-able.
It's also AWS-only by design, that's the point. The moment you hand a human a private key or passphrase (age, PGP, password-based tools), you recreate the leak risk KMS exists to remove.
If you need multi-cloud, SOPS is the better fit. If you're already on Secrets Manager/SSM, use those - same KMS underneath; this is for teams who want encrypted config files in their existing workflow.
The full write-up
I wrote a 4-part series with the complete threat model, every CLI command, the runtime/config() internals, and a comparison to dotenvx / SOPS / Secrets Manager:
- The Complete Guide
- Part 1 — Your .env Is a Loaded Gun (threat model)
- Part 2 — The CLI
- Part 3 — Runtime, rotation & attestation
Repo (with runnable Next.js + NestJS examples): github.com/faizahmedfarooqui/secret-keystore
If you ship Node on AWS and have ever had to "rotate everything and hope," this is the pattern I wish I'd had before the incident. Feedback and hard questions welcome.
Top comments (0)