← Open Source

@faizahmed/secret-keystore

Encrypt your .env with AWS KMS and decrypt on demand, so no plaintext hits process.env.

@faizahmed/secret-keystore is a Node.js library for teams that keep secrets in .env, JSON, or YAML and want them encrypted at rest with AWS KMS, decrypted on demand into a small in-memory keystore, and never written back into process.env as plaintext. You handle only KMS Key IDs (which are not secrets); the actual key material stays server-side in KMS, and decrypted values exist only in memory for the moment you read them.

It exists for one reason: to shrink blast radius. If your config file leaks into git history, a log line, or a process.env dump during an incident, an attacker gets ENC[...] ciphertext instead of every password you own. This is a server-side tool for Node backends, API routes, Server Components, NestJS services, and Lambda. It is not for browsers, NEXT_PUBLIC_* variables, or anything that runs client-side, because those environments cannot reach KMS.

The problem it solves

The default pattern in Node is that every secret lands in process.env in plaintext at boot. That means one config dump, one over-eager logger, or one committed .env exposes all of them at once. process.env is effectively a single flat file of your entire secret inventory, readable by any code in the process.

secret-keystore breaks that single point of failure. Secrets sit in your config as KMS ciphertext, useless without the KMS key and the right IAM permissions. At runtime, only the keys you explicitly ask for get decrypted, and they go into an isolated keystore rather than the global environment. Access becomes per-key and greppable: you can trace every .get( call to see exactly where a secret is used. It does not defend against a fully compromised process or memory scraping after decryption, and it is honest about that; what it removes is the bulk-exposure path.

What you get

The library covers the whole lifecycle, from encrypting values in place to reading them safely at runtime, with a CLI for the operational parts and a programmatic API for everything else. Content-preserving operations keep your comments and formatting intact when they rewrite a file.

  • Multi-format support for .env, JSON, and YAML, with selective encryption by explicit key list or glob patterns.
  • A CLI covering encrypt, decrypt, run, rotate, edit, init, keys, status, and import (the read commands print names, never values).
  • IAM role-based authentication by default, so there are no long-lived credentials to manage.
  • An in-memory keystore that adds AES-256-GCM encryption on top, with optional TTL, auto-refresh, and secure wipe.
  • Symmetric KMS keys by default, plus RSA envelope encryption for payloads past the 4 KB direct-encrypt limit.
  • Optional AWS Nitro Enclave attestation with auto-refresh, and TypeScript definitions included.

Quick look

Install the package, then encrypt the values you care about with the CLI. KMS_KEY_ID and the AWS_* keys stay in plaintext because they are needed to perform the decryption.

npm install @faizahmed/secret-keystore
npx @faizahmed/secret-keystore encrypt \
  --kms-key-id="arn:aws:kms:us-east-1:123456789012:key/abcd-1234" \
  --keys="DB_PASSWORD,API_KEY"

Your DB_PASSWORD value now reads ENC[AQICAHh2nZPq...] on disk. At runtime you build the keystore and read from it. The decrypted value lives only on the variable you assign it to; process.env.DB_PASSWORD still holds the ciphertext.

const { createSecretKeyStore } = require('@faizahmed/secret-keystore');
const fs = require('node:fs');

const content = fs.readFileSync('./.env', 'utf-8');

const keyStore = await createSecretKeyStore(
  { type: 'env', content },
  process.env.KMS_KEY_ID,
  {
    paths: ['DB_PASSWORD', 'API_KEY'],
    aws: { region: process.env.AWS_REGION }
  }
);

const dbPassword = keyStore.get('DB_PASSWORD'); // decrypted in-memory

If you prefer zero config, config() discovers and cascades your .env files (.env then .env.local then .env.<NODE_ENV> then .env.<NODE_ENV>.local, later wins), decrypts them, and hands back the same keystore interface. It does not copy anything into process.env unless you opt in. Call keyStore.destroy() on shutdown to wipe secrets from memory.

How it works

For symmetric keys, the library calls KMS Encrypt and Decrypt directly, which fits any value under the 4 KB limit. For RSA keys or larger payloads it switches to envelope encryption: it generates a random AES-256 data key locally, encrypts the secret with AES-256-GCM (no size limit), then sends only the small AES key to KMS to be wrapped. The stored ENC[...] blob packs the KMS-encrypted key, IV, ciphertext, and auth tag together. Decryption reverses it: KMS unwraps the data key, and the library uses it to recover the payload.

The keystore itself is a second layer. Decrypted values are held under AES-256-GCM in memory rather than in process.env, with optional TTL and auto-refresh so stale secrets can expire. For enclave deployments, attestation generates an ephemeral RSA-4096 key pair, fetches a Nitro attestation document, and calls KMS Decrypt with a Recipient parameter so KMS returns CiphertextForRecipient that only your enclave can unwrap; the document auto-refreshes on its five-minute expiry.

For the full API, CLI reference, IAM policy, and the runnable Next.js and NestJS examples, see the repo.