← Open Source

@faizahmed/secret-keystore

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

A secure secrets management library for Node.js applications using AWS KMS encryption.

Available on npm: @faizahmed/secret-keystore

Design principle: the only thing a developer ever handles is a KMS Key ID — which is not a secret. No private keys, no passphrases, no key material. AWS KMS holds all key material server-side and authorizes access via IAM, and decrypted values live only in memory (never in process.env, never on disk) to keep the blast radius of any RCE as small as possible.

Guides & Deep-Dives

A full walkthrough lives on the blog, as part of the Mastering Encryption series:

Table of Contents

Features

  • Multi-Format Support — Encrypt secrets in .env, JSON, and YAML files
  • Zero-Config Loaderconfig() discovers and cascades your .env files, decrypts them, and loads everything into an in-memory keystore in one call
  • Encrypt & Decrypt CLI — Full command-line round-trip (encrypt and decrypt)
  • Pattern Matching — Use glob patterns (**) to select keys at any depth
  • AWS KMS Encryption — Supports symmetric and asymmetric (RSA) keys; uses envelope encryption for RSA (no plaintext size limit)
  • Key-ID-Only — Developers only ever share a non-secret KMS Key ID; no key material to leak
  • IAM Role by Default — Uses IAM roles for authentication (explicit credentials require opt-in)
  • Secure In-Memory Storage — Decrypted values stored with AES-256-GCM encryption in memory
  • Never in process.env — Decrypted secrets are only accessible via the keystore API
  • TTL & Auto-Refresh — Automatic secret expiry and re-decryption
  • Nitro Enclave Attestation — Full attestation lifecycle with automatic 5-minute refresh
  • Dual API — Content-based (convenient) + Object-based (flexible)
  • Comment Preservation — Preserves comments and formatting in config files
  • Security-First Dependencies — Minimal dependencies for security-sensitive operations
  • Battle-Tested — Offline test suite (node:test, mocked KMS) with CI across Node 18/20/22, plus lint, format, and type-definition checks

Important Limitations

⚠️ This library is for SERVER-SIDE use only. It will NOT work with client-side code.

❌ Does NOT Work With

ScenarioReason
Next.js NEXT_PUBLIC_* variablesThese are embedded into client-side JavaScript at build time and exposed to browsers. Browsers cannot access AWS KMS.
Next.js Client ComponentsClient components run in the browser, which has no access to AWS KMS or server-side Node.js APIs.
NestJS with Client-Side RenderingAny code that runs in the browser cannot decrypt KMS-encrypted values.
React/Vue/Angular frontend appsFrontend JavaScript runs in users’ browsers, not on servers with AWS access.
Static Site Generation (SSG) at build timeBuild-time secrets would be embedded in static HTML/JS, defeating the purpose.

Why These Don’t Work

┌─────────────────────────────────────────────────────────────────────────────┐
│                           CLIENT-SIDE (Browser)                              │
│                                                                              │
│   ❌ No AWS credentials                                                      │
│   ❌ No access to AWS KMS API                                                │
│   ❌ No Node.js crypto module                                                │
│   ❌ NEXT_PUBLIC_* variables are bundled into JS at BUILD time               │
│   ❌ Cannot make authenticated AWS API calls                                 │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│                           SERVER-SIDE (Node.js)                              │
│                                                                              │
│   ✅ Has AWS IAM role or credentials                                         │
│   ✅ Can call AWS KMS Decrypt API                                            │
│   ✅ Has Node.js crypto module                                               │
│   ✅ Secrets decrypted at RUNTIME in memory                                  │
│   ✅ Values never sent to browser                                            │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

✅ Works With

ScenarioExample
Next.js API Routessrc/app/api/*/route.ts
Next.js Server ComponentsComponents without "use client"
Next.js Server ActionsFunctions with "use server"
NestJS ServicesBackend services running on Node.js
Express/Fastify APIsAny Node.js backend
AWS LambdaServerless functions
Background JobsCron jobs, workers, etc.

Next.js Example: What Works vs What Doesn’t

// ❌ WRONG: Client Component - will NOT work
"use client";
import { getSecret } from "@/lib/keystore";

export function ClientComponent() {
  // This will fail - browsers can't access AWS KMS
  const secret = await getSecret("API_KEY"); // ❌ Error!
  return <div>{secret}</div>;
}
// ❌ WRONG: NEXT_PUBLIC_* variables - CANNOT be encrypted
// These are embedded in client JS at build time
const apiKey = process.env.NEXT_PUBLIC_API_KEY; // Already exposed to browser!
// ✅ CORRECT: API Route (Server-Side)
// src/app/api/data/route.ts
import { getSecret } from "@/lib/keystore";

export async function GET() {
  // This works - runs on server with AWS access
  const apiKey = await getSecret("API_KEY"); // ✅ Decrypted on server

  const data = await fetchExternalAPI(apiKey);
  return Response.json(data); // Only send non-sensitive data to client
}
// ✅ CORRECT: Server Component (no "use client")
// src/app/dashboard/page.tsx
import { getSecret } from "@/lib/keystore";

export default async function DashboardPage() {
  // This works - Server Components run on the server
  const dbPassword = await getSecret("DB_PASSWORD"); // ✅ Server-side only

  const data = await fetchFromDatabase(dbPassword);
  return <Dashboard data={data} />; // Render with data, not secrets
}

Summary

Variable TypeCan Encrypt?Where It Runs
Regular env vars (DB_PASSWORD)✅ YesServer only
NEXT_PUBLIC_* vars❌ NoBundled into client JS
Server Component code✅ YesServer only
Client Component code❌ NoBrowser
API Route code✅ YesServer only

Prerequisites

  • Node.js >= 18.0.0
  • AWS account with KMS access
  • AWS IAM role (recommended) or explicit credentials for local development

Installation

From npm Registry

The package is published to the public npm registry. Install with:

# npm
npm install @faizahmed/secret-keystore

# pnpm
pnpm add @faizahmed/secret-keystore

Note: The package includes @aws-sdk/client-kms as a dependency.

Local Development / Docker Builds

When working with local development or Docker builds where file: references don’t work (e.g., the path is outside the Docker build context), you can pack the library into a tarball:

Step 1: Pack the Library
# From the secret-keystore directory

# npm
npm pack

# pnpm
pnpm pack

# This creates: faizahmed-secret-keystore-1.1.1.tgz (scoped package name)

Or use the provided script:

# Pack and move to a specific directory (e.g., consumer project)
npm run pack:local   # or: pnpm run pack:local
Step 2: Install the Tarball

In your consumer project:

# Copy the tarball to your project
cp ../secret-keystore/faizahmed-secret-keystore-1.1.1.tgz ./

# Install from tarball
# npm
npm install ./faizahmed-secret-keystore-1.1.1.tgz

# pnpm
pnpm add ./faizahmed-secret-keystore-1.1.1.tgz

Or add a script to your consumer’s package.json:

{
  "scripts": {
    "pack:keystore": "cd ../secret-keystore && npm pack --pack-destination ../your-project",
    "install:keystore": "npm run pack:keystore && npm install ./faizahmed-secret-keystore-*.tgz"
  }
}
Step 3: Update package.json Reference

After installing, your package.json will reference the local tarball:

{
  "dependencies": {
    "@faizahmed/secret-keystore": "file:./faizahmed-secret-keystore-1.1.1.tgz"
  }
}
Docker Build Configuration

For Docker builds, copy the tarball into the build context:

# Stage 1: Install Dependencies
FROM node:22 AS deps
WORKDIR /app

# Copy package files
COPY package.json yarn.lock ./

# Copy the local tarball (must be in the Docker build context)
COPY faizahmed-secret-keystore-1.1.1.tgz ./

# Install dependencies (tarball is referenced in package.json)
RUN yarn install --frozen-lockfile

Important: The tarball file must be in your project directory (Docker build context). You cannot reference paths outside the build context like ../secret-keystore.

Complete Workflow Example
# 1. In the keystore library directory
cd secret-keystore
npm pack          # or: pnpm pack
mv faizahmed-secret-keystore-*.tgz ../your-consumer-project/

# 2. In your consumer project
cd ../your-consumer-project

# Update package.json to reference the tarball
# "dependencies": { "@faizahmed/secret-keystore": "file:./faizahmed-secret-keystore-1.1.1.tgz" }

npm install       # or: pnpm install

# 3. Commit the tarball to your repo (for CI/CD)
git add faizahmed-secret-keystore-*.tgz
git commit -m "Add @faizahmed/secret-keystore tarball for Docker builds"

Optional: YAML Support

For YAML files with complex features (anchors, aliases, multi-line strings), install js-yaml:

# npm
npm install js-yaml

# pnpm
pnpm add js-yaml

Without js-yaml, the library uses a simple built-in parser that handles basic YAML structures. If your YAML uses advanced features without js-yaml installed, you’ll get a clear error message.

Quick Start

Step 1: Prepare Your Configuration

Create a .env file with your secrets:

# AWS Configuration (never encrypted)
KMS_KEY_ID=arn:aws:kms:us-east-1:123456789012:key/abcd-1234
AWS_REGION=us-east-1

# Your secrets (will be encrypted)
DB_PASSWORD=mysecretpassword
API_KEY=sk-1234567890abcdef
JWT_SECRET=super-secret-jwt-key

# Non-sensitive values
DB_HOST=localhost
PORT=3000

Step 2: Encrypt Your Secrets

Run the CLI to encrypt secrets:

# Encrypt specific keys (kms-key-id is REQUIRED)
npx @faizahmed/secret-keystore encrypt \
  --kms-key-id="arn:aws:kms:us-east-1:123456789012:key/abcd-1234" \
  --keys="DB_PASSWORD,API_KEY,JWT_SECRET"

# Or encrypt all keys (except reserved ones)
npx @faizahmed/secret-keystore encrypt \
  --kms-key-id="alias/my-key"

# For local development, use explicit credentials
npx @faizahmed/secret-keystore encrypt \
  --kms-key-id="alias/my-key" \
  --use-credentials

Your .env file is updated in-place:

# AWS Configuration (never encrypted)
KMS_KEY_ID=arn:aws:kms:us-east-1:123456789012:key/abcd-1234
AWS_REGION=us-east-1

# Encrypted values
DB_PASSWORD=ENC[AQICAHh2nZPq...base64...]
API_KEY=ENC[AQICAHh2nZPq...base64...]
JWT_SECRET=ENC[AQICAHh2nZPq...base64...]

Step 3: Use in Your Application

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

async function bootstrap() {
  // Read your config file
  const content = fs.readFileSync('./.env', 'utf-8');
  const kmsKeyId = process.env.KMS_KEY_ID;  // You extract the KMS key

  // Initialize the keystore
  const keyStore = await createSecretKeyStore(
    { type: 'env', content },
    kmsKeyId,  // REQUIRED
    {
      paths: ['DB_PASSWORD', 'API_KEY', 'JWT_SECRET'],
      aws: { region: process.env.AWS_REGION }
    }
  );

  // Access decrypted secrets from the keystore
  const dbPassword = keyStore.get('DB_PASSWORD');  // → "mysecretpassword"
  const apiKey = keyStore.get('API_KEY');          // → "sk-1234567890abcdef"

  // process.env still contains encrypted values (safe!)
  console.log(process.env.DB_PASSWORD);  // → "ENC[AQICAHh...encrypted...]"

  // Start your application
  connectToDatabase({ password: dbPassword });
}

bootstrap();

CLI Reference

npx @faizahmed/secret-keystore <command> [options]

Commands

CommandDescription
encryptEncrypt selected values in a config file (writes ENC[...] in place or to --output)
decryptDecrypt the ENC[...] values in a config file (writes plaintext in place or to --output)
runDecrypt and launch a command with secrets injected into the child process’s environment
rotateRe-encrypt a file under a new KMS Key ID (requires --old-kms-key-id)
editDecrypt → open in $EDITOR → re-encrypt on save (via a 0600 temp file, then shredded)
initScaffold a starter .env
keysList the keys/paths in a file — names only, never values
statusShow which keys are encrypted vs plaintext — names only, never values
importEncrypt an existing plaintext .env in place (migration from plain dotenv)

All commands share the options below and auto-detect the format from the file extension.

Options

OptionRequiredDefaultDescription
--kms-key-id=<id>YesKMS Key ID (ARN, UUID, or alias)
--old-kms-key-id=<id>For rotateThe current key the file is encrypted with (rotate’s source key)
--path=<path>No./.envPath to the config file
--format=<format>Noauto-detectFile format: env, json, yaml
--keys=<keys>NoAll keysComma-separated list of keys to encrypt
--patterns=<patterns>NoGlob patterns (e.g., **.password,**.secret)
--exclude=<keys>NoKeys/paths to exclude from encryption
--region=<region>NoFrom envAWS region
--output=<path>NoOverwrite inputOutput file path
--use-credentialsNoUse explicit AWS credentials instead of IAM role
--dry-runNoPreview what would be encrypted
--help, -hShow help message
--version, -vShow version number

Authentication

By default, the CLI uses IAM role for AWS authentication. This is recommended for production.

To use explicit credentials (e.g., for local development):

# Set credentials in environment
export AWS_ACCESS_KEY_ID=your-access-key
export AWS_SECRET_ACCESS_KEY=your-secret-key

# Run with --use-credentials flag
npx @faizahmed/secret-keystore encrypt \
  --kms-key-id="alias/my-key" \
  --use-credentials

Reserved Keys

These keys are never encrypted (required for encryption/decryption process):

  • KMS_KEY_ID
  • AWS_REGION
  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • AWS_SESSION_TOKEN

CLI Examples

# Encrypt all keys in .env
npx @faizahmed/secret-keystore encrypt \
  --kms-key-id="alias/my-key"

# Encrypt specific keys only
npx @faizahmed/secret-keystore encrypt \
  --kms-key-id="arn:aws:kms:us-east-1:123456789:key/abc-123" \
  --keys="DB_PASSWORD,API_KEY"

# Encrypt YAML file with patterns
npx @faizahmed/secret-keystore encrypt \
  --path="./secrets.yaml" \
  --kms-key-id="alias/my-key" \
  --patterns="**.password,**.secret_key"

# Dry run to preview changes
npx @faizahmed/secret-keystore encrypt \
  --kms-key-id="alias/my-key" \
  --dry-run

# Output to a different file
npx @faizahmed/secret-keystore encrypt \
  --path="./.env" \
  --output="./.env.encrypted" \
  --kms-key-id="alias/my-key"

# Decrypt all encrypted values in place
npx @faizahmed/secret-keystore decrypt \
  --path="./.env" \
  --kms-key-id="alias/my-key"

# Decrypt to a separate plaintext file
npx @faizahmed/secret-keystore decrypt \
  --path="./.env.encrypted" \
  --output="./.env" \
  --kms-key-id="alias/my-key"

# Run your app with secrets injected into its environment (no plaintext on disk)
npx @faizahmed/secret-keystore run \
  --kms-key-id="alias/my-key" -- node server.js

# Rotate a file from an old key to a new key (re-encrypts only encrypted values)
npx @faizahmed/secret-keystore rotate \
  --old-kms-key-id="alias/old-key" \
  --kms-key-id="alias/new-key"

# Edit an encrypted file in your $EDITOR (re-encrypts on save)
npx @faizahmed/secret-keystore edit \
  --kms-key-id="alias/my-key" --path="./.env"

# Inspect a file without revealing any values
npx @faizahmed/secret-keystore status --path="./.env"
npx @faizahmed/secret-keystore keys   --path="./.env"

# Migrate an existing plaintext .env to encrypted, in place
npx @faizahmed/secret-keystore import \
  --kms-key-id="alias/my-key" --path="./.env"

run vs config(): run is for apps you don’t want to modify — it injects secrets into the spawned child’s environment (so the child’s env can see them; the parent never does). config() keeps secrets in your app’s in-memory store and out of env entirely, but requires the app to call it. Both avoid writing plaintext to disk.

Tip: For running your app, prefer run or the in-memory config() loader over decrypting to a plaintext file on disk.

Library API

Single Value Operations

const { encryptKMSValue, decryptKMSValue } = require('@faizahmed/secret-keystore');

// Encrypt a single value
const ciphertext = await encryptKMSValue(
  'my-secret-password',
  'arn:aws:kms:us-east-1:123456789:key/abc-123',  // kmsKeyId (REQUIRED)
  { aws: { region: 'us-east-1' } }
);
// Returns: "ENC[AQICAHh...]"

// Decrypt a single value
const plaintext = await decryptKMSValue(
  'ENC[AQICAHh...]',
  'arn:aws:kms:us-east-1:123456789:key/abc-123',
  { aws: { region: 'us-east-1' } }
);
// Returns: "my-secret-password"

Multiple Values Operations

const { encryptKMSValues, decryptKMSValues } = require('@faizahmed/secret-keystore');

// Encrypt multiple values
const result = await encryptKMSValues(
  { DB_PASSWORD: 'secret123', API_KEY: 'sk-12345' },
  'alias/my-key',
  { aws: { region: 'us-east-1' } }
);
// result.values = { DB_PASSWORD: 'ENC[...]', API_KEY: 'ENC[...]' }
// result.encrypted = ['DB_PASSWORD', 'API_KEY']

Content-Based Operations

For working with file content (preserves comments and formatting):

const {
  encryptKMSEnvContent,
  encryptKMSJsonContent,
  encryptKMSYamlContent,
  decryptKMSEnvContent,
  decryptKMSJsonContent,
  decryptKMSYamlContent
} = require('@faizahmed/secret-keystore');
const fs = require('node:fs');

// Encrypt ENV content
const envContent = fs.readFileSync('./.env', 'utf-8');
const kmsKeyId = process.env.KMS_KEY_ID;

const result = await encryptKMSEnvContent(envContent, kmsKeyId, {
  paths: ['DB_PASSWORD', 'API_KEY'],
  aws: { region: 'us-east-1' }
});

fs.writeFileSync('./.env', result.content);

// Encrypt YAML content with patterns
const yamlContent = fs.readFileSync('./secrets.yaml', 'utf-8');
const yamlResult = await encryptKMSYamlContent(yamlContent, kmsKeyId, {
  patterns: ['**.password', '**.secret_key'],
  preserve: { comments: true, formatting: true }
});

Object-Based Operations

For advanced use cases where you handle parsing/serialization:

const { encryptKMSObject, decryptKMSObject } = require('@faizahmed/secret-keystore');
const yaml = require('js-yaml');
const fs = require('node:fs');

// Parse YAML yourself
const config = yaml.load(fs.readFileSync('./secrets.yaml', 'utf-8'));
const kmsKeyId = config.kms.key_id;

// Encrypt object
const result = await encryptKMSObject(config, kmsKeyId, {
  patterns: ['**.password', '**.secret_key'],
  exclude: { paths: ['kms.key_id'] }
});

// Serialize and write yourself
fs.writeFileSync('./secrets.yaml', yaml.dump(result.object));

Function Summary

FunctionPurpose
encryptKMSValue(plaintext, kmsKeyId, options?)Encrypt single value using KMS
decryptKMSValue(ciphertext, kmsKeyId, options?)Decrypt single value using KMS
encryptKMSValues(values, kmsKeyId, options?)Encrypt flat key-value pairs using KMS
decryptKMSValues(values, kmsKeyId, options?)Decrypt flat key-value pairs using KMS
encryptKMSObject(obj, kmsKeyId, options?)Encrypt nested object using KMS
decryptKMSObject(obj, kmsKeyId, options?)Decrypt nested object using KMS
encryptKMSEnvContent(content, kmsKeyId, options?)Encrypt ENV string using KMS
decryptKMSEnvContent(content, kmsKeyId, options?)Decrypt ENV string using KMS
encryptKMSJsonContent(content, kmsKeyId, options?)Encrypt JSON string using KMS
decryptKMSJsonContent(content, kmsKeyId, options?)Decrypt JSON string using KMS
encryptKMSYamlContent(content, kmsKeyId, options?)Encrypt YAML string using KMS
decryptKMSYamlContent(content, kmsKeyId, options?)Decrypt YAML string using KMS
isJsYamlAvailable()Check if js-yaml is installed
parseYaml(content)Parse YAML to object (uses js-yaml if available)
serializeYaml(obj)Serialize object to YAML string
config(options)Discover + cascade .env files, decrypt, and load into an in-memory keystore
resolveEnvFiles(options?)Resolve the ordered list of .env files for the cascade
rotateKMSContent(content, format, oldKeyId, newKeyId, options?)Re-encrypt a file’s encrypted values under a new KMS Key ID

Note: kmsKeyId is REQUIRED in all functions. The library does not search content for it.

Runtime Keystore

Zero-Config Loader: config()

The fastest way to load secrets at runtime. config() discovers and cascades your .env files, decrypts the ENC[...] values via KMS, and loads everything into an in-memory SecretKeyStore — in a single call.

const { config } = require('@faizahmed/secret-keystore');

// Discovers .env, .env.local, .env.<NODE_ENV>, .env.<NODE_ENV>.local (later wins)
const secrets = await config({ kmsKeyId: 'alias/my-key' });

const dbPassword = secrets.get('DB_PASSWORD'); // decrypted, in-memory only
const all = secrets.getAll();

process.on('SIGTERM', () => secrets.destroy());

Cascade order (later files override earlier ones):

.env  →  .env.local  →  .env.<NODE_ENV>  →  .env.<NODE_ENV>.local

Security by design:

  • Decrypted values live only in the returned keystore’s memory — never written to disk, and never placed in process.env. This deliberately keeps secrets off env, so an attacker with code execution can’t simply dump them.
  • kmsKeyId is required and explicit — there is no environment-variable fallback.
  • Plaintext (non-ENC[...]) values are passed through unchanged; only encrypted values hit KMS.

Options:

OptionDefaultDescription
kmsKeyId— (required)KMS Key ID (ARN, UUID, or alias)
cwdprocess.cwd()Base directory for file discovery
pathExplicit file path(s); skips the cascade when set
nodeEnvprocess.env.NODE_ENVEnvironment name used in the cascade
populateProcessEnvfalseOpt-in: also copy decrypted values into process.env (discouraged — widens RCE blast radius; logs a warning)
processEnvprocess.envTarget object when populateProcessEnv is enabled

config() also accepts all keystore options (TTL, autoRefresh, security, aws, etc.), which are forwarded to the underlying keystore.

Migrating from dotenv? dotenv populates process.env; config() deliberately does not. Read secrets from the returned store (secrets.get('KEY')) instead. If you must have process.env behavior, set populateProcessEnv: true and accept the larger blast radius.

createSecretKeyStore(source, kmsKeyId, options?)

Creates and initializes a secure in-memory keystore with decrypted secrets. Use this directly when you already have the file content/object in hand; use config() when you want automatic file discovery and cascading.

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

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

const keyStore = await createSecretKeyStore(
  { type: 'env', content },
  kmsKeyId,
  {
    paths: ['DB_PASSWORD', 'API_KEY', 'JWT_SECRET'],
    aws: { region: 'us-east-1' },
    security: { inMemoryEncryption: true },
    access: { ttl: 3600000, autoRefresh: true }
  }
);

// Use secrets
const dbPassword = keyStore.get('DB_PASSWORD');

// Cleanup on shutdown
process.on('SIGTERM', () => keyStore.destroy());

Source Types

// ENV file content
{ type: 'env', content: 'KEY=value\nKEY2=value2' }

// JSON content
{ type: 'json', content: '{"key": "value"}' }

// YAML content
{ type: 'yaml', content: 'key: value' }

// Pre-parsed object
{ type: 'object', object: { key: 'value' } }

// Flat key-value pairs
{ type: 'values', values: { KEY: 'value' } }

KeyStore Methods

MethodReturnsDescription
get(key)string | undefinedGet a decrypted secret
getSection(path)object | undefinedGet a nested section
getAll()Record<string, string>Get all decrypted secrets
has(key)booleanCheck if a key exists
keys()string[]Get all available key names
isInitialized()booleanCheck if keystore is ready
getMetadata()objectGet keystore metadata
getAccessStats(key)object | nullGet access statistics for a key
refresh()Promise<void>Re-decrypt all secrets
clear()voidClear all secrets from memory
clearKey(key)voidClear a specific key
destroy()voidDestroy keystore and wipe memory

TTL and Auto-Refresh

ttlautoRefreshBehavior
nullSecrets never expire
3600000trueAuto re-decrypt on next get() after expiry
3600000falseThrow error, user calls refresh() manually

Configuration Options

Layered Options Structure

{
  // AWS Configuration
  aws: {
    credentials: {
      accessKeyId: string,
      secretAccessKey: string,
      sessionToken?: string
    },
    region: string
  },

  // Attestation (Nitro Enclaves) - Full lifecycle managed internally
  attestation: {
    enabled: boolean,           // Default: false
    required: boolean,          // Default: false
    fallbackToStandard: boolean, // Default: true
    endpoint: string,           // Attestation endpoint URL (e.g., Anjuna)
    timeout: number,            // Request timeout (ms), Default: 10000
    userData: string            // Optional user data for attestation
  },

  // Path Selection
  paths: string[],              // Explicit paths
  patterns: string[],           // Glob patterns (** only)
  exclude: {
    paths: string[],
    patterns: string[]
  },

  // Content Preservation
  preserve: {
    comments: boolean,          // Default: true
    formatting: boolean,        // Default: true
    anchors: boolean            // Default: true (YAML)
  },

  // Keystore-specific
  security: {
    inMemoryEncryption: boolean, // Default: true
    secureWipe: boolean          // Default: true
  },
  access: {
    ttl: number | null,         // Secret expiry (ms)
    autoRefresh: boolean,       // Default: true
    accessLimit: number,        // Max access count
    clearOnAccess: boolean      // Default: false
  },
  validation: {
    noProcessEnvLeak: boolean,  // Default: true
    throwOnMissingKey: boolean  // Default: false
  },

  // Logging
  logger: Logger,
  logLevel: 'debug' | 'info' | 'warn' | 'error' | 'silent'
}

How It Works

KMS Key Types: Symmetric vs Asymmetric

The library detects the key type via AWS KMS DescribeKey and chooses the right encryption method:

Key typeEncryption methodPlaintext size limit
Symmetric (default CMK)Direct KMS Encrypt/Decrypt4 KB per value
Asymmetric (RSA)Envelope encryptionNo limit

Envelope encryption (RSA only): For RSA keys, plaintext is too large for direct RSA encryption (e.g. ~190 bytes for RSA_2048). The library generates a random AES-256 data key (DEK), encrypts your secret with the DEK (AES-256-GCM), and encrypts only the DEK with KMS. The stored value is ENC[base64(envelope)] where the envelope contains the KMS-encrypted DEK plus IV, ciphertext, and auth tag. Decryption: KMS decrypts the DEK, then the library decrypts the payload with the DEK. Existing values encrypted with symmetric keys or with direct RSA (small payloads) remain valid; new encrypts with RSA use envelope format automatically.

Build-Time: Encrypting Secrets

sequenceDiagram
    participant Dev as Developer
    participant CLI as CLI
    participant KMS as AWS KMS
    participant File as Config File

    Dev->>CLI: npx encrypt --kms-key-id="..." --keys="DB_PASS"
    CLI->>File: Read config file
    File-->>CLI: DB_PASS=mysecretpassword
    CLI->>KMS: Encrypt("mysecretpassword")
    KMS-->>CLI: AQICAHh...encrypted...
    CLI->>File: Write ENC[AQICAHh...]
    CLI-->>Dev: ✅ Encrypted 1 key

Runtime: Decrypting Secrets

sequenceDiagram
    participant App as Your App
    participant KS as KeyStore
    participant KMS as AWS KMS
    participant Mem as Secure Memory

    App->>KS: createSecretKeyStore(source, kmsKeyId)
    KS->>KS: Parse content
    KS->>KMS: Decrypt(ENC[AQICAHh...])
    KMS-->>KS: mysecretpassword
    KS->>Mem: Store with AES-256-GCM
    KS-->>App: KeyStore ready

    App->>KS: keyStore.get('DB_PASS')
    KS->>Mem: Retrieve & decrypt
    Mem-->>KS: mysecretpassword
    KS-->>App: "mysecretpassword"

Key Points

Stageprocess.env.DB_PASSkeyStore.get('DB_PASS')
Before encryptionmysecretpassword
After encryptionENC[AQICAHh...]
At runtime (after init)ENC[AQICAHh...]mysecretpassword

Security: Decrypted values are never stored in process.env. They exist only in the keystore’s secure memory with additional AES-256-GCM encryption.

Examples

Complete working sample applications are available in the examples/ directory:

FrameworkPathDescription
NestJSexamples/nestjs/Full NestJS app with global KeyStoreModule
Next.jsexamples/nextjs/Next.js 14 App Router with Server Components

Running Examples Locally

# 1. Install the main package dependencies
cd secret-keystore
pnpm install

# 2. Install example dependencies
cd examples/nestjs   # or examples/nextjs
pnpm install

# 3. Configure environment
cp .env.example .env  # or .env.local for Next.js

# 4. Update KMS_KEY_ID in .env with your actual KMS key

# 5. Encrypt secrets
pnpm run encrypt:keys

# 6. Run the app
pnpm run start:dev    # NestJS
pnpm run dev          # Next.js

See each example’s README for detailed setup instructions.

Nitro Enclave Attestation

For maximum security in AWS Nitro Enclaves, the library provides full attestation lifecycle management.

How It Works

When attestation is enabled:

  1. Key Pair Generation — Library generates ephemeral RSA-4096 key pair
  2. Document Fetch — Fetches attestation document from Anjuna/Nitro endpoint (includes public key)
  3. Attested Decrypt — Sends KMS Decrypt with Recipient parameter containing attestation document
  4. CMS Unwrap — KMS returns CiphertextForRecipient (CMS EnvelopedData), library unwraps with private key
  5. Auto-Refresh — If document expires (5-min AWS limit), library automatically regenerates and retries
sequenceDiagram
    participant App as Your App
    participant AM as AttestationManager
    participant Anjuna as Nitro/Anjuna
    participant KMS as AWS KMS

    App->>AM: decryptWithAttestation()
    AM->>AM: Generate RSA-4096 key pair
    AM->>Anjuna: Request attestation doc (with public key)
    Anjuna-->>AM: Attestation document
    AM->>KMS: Decrypt + Recipient{AttestationDoc}
    KMS-->>AM: CiphertextForRecipient (CMS EnvelopedData)
    AM->>AM: Unwrap CMS with private key (PKIjs)
    AM-->>App: Decrypted plaintext

Enabling Attestation

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

const keyStore = await createSecretKeyStore(
  { type: 'env', content },
  kmsKeyId,
  {
    attestation: {
      enabled: true,
      required: true,  // Fail if attestation unavailable
      endpoint: 'http://localhost:8080/attestation',  // Anjuna/Nitro endpoint
      timeout: 10000,
      userData: 'optional-user-data'
    }
  }
);

Using AttestationManager Directly

For advanced use cases, you can use the AttestationManager directly:

const { createAttestationManager } = require('@faizahmed/secret-keystore');
const { KMSClient } = require('@aws-sdk/client-kms');

// Create and initialize the manager
const attestationManager = await createAttestationManager({
  endpoint: 'http://localhost:8080/attestation',
  timeout: 10000,
  logger: console
});

// Use for KMS decrypt with attestation
const kmsClient = new KMSClient({ region: 'us-east-1' });
const plaintext = await attestationManager.decryptWithAttestation(
  kmsClient,
  ciphertextBlob,
  kmsKeyId,
  { encryptionContext: { ... } }
);

// Check status
console.log(attestationManager.getStatus());
// { initialized: true, hasDocument: true, documentAge: 45000, ... }

// Cleanup
attestationManager.destroy();

5-Minute Auto-Refresh

AWS KMS requires attestation documents to be less than 5 minutes old. The library handles this automatically:

ScenarioLibrary Behavior
First requestInitialize (generate key pair, fetch doc)
Document < 5 min oldUse cached document
Document expired (KMS rejects)Regenerate key pair, fetch new doc, retry once
Anjuna/Nitro unavailableThrow ATTESTATION_FETCH_FAILED error

Note: Attestation is only available inside AWS Nitro Enclaves. Outside enclaves, enable fallbackToStandard: true to use standard KMS decrypt.

Error Handling

The library provides a comprehensive error hierarchy:

const {
  createSecretKeyStore,
  SecretKeyStoreError,    // Base error class
  KmsError,               // AWS KMS errors
  AttestationError,       // Attestation failures
  ContentError,           // Content parsing errors
  PathError,              // Path resolution errors
  EncryptionError,        // Encryption failures
  DecryptionError,        // Decryption failures
  KeystoreError,          // Keystore operation errors
  ValidationError         // Validation failures
} = require('@faizahmed/secret-keystore');

try {
  const keyStore = await createSecretKeyStore(source, kmsKeyId, options);
  const secret = keyStore.get('DB_PASSWORD');
} catch (error) {
  if (error instanceof KmsError) {
    console.error('KMS Error:', error.code, error.message);
  } else if (error instanceof KeystoreError) {
    console.error('Keystore Error:', error.code, error.message);
  } else if (error instanceof AttestationError) {
    console.error('Attestation Error:', error.code, error.message);
  }
}

Error Codes

CategoryCodes
KMSKMS_KEY_NOT_FOUND, KMS_KEY_DISABLED, KMS_ACCESS_DENIED, KMS_INVALID_CIPHERTEXT, KMS_THROTTLED
AttestationATTESTATION_INIT_FAILED, ATTESTATION_DOCUMENT_EXPIRED, ATTESTATION_KEY_PAIR_FAILED, ATTESTATION_FETCH_FAILED, ATTESTATION_CMS_UNWRAP_FAILED
KeystoreKEYSTORE_NOT_INITIALIZED, KEYSTORE_DESTROYED, SECRET_NOT_FOUND, SECRET_EXPIRED

TypeScript Support

The package includes TypeScript definitions:

import * as fs from 'node:fs';
import {
  createSecretKeyStore,
  SecretKeyStore,
  KeystoreSource,
  KeystoreOptions,
  encryptKMSEnvContent,
  encryptKMSYamlContent,
  ContentResult,
  // Attestation exports
  AttestationManager,
  createAttestationManager
} from '@faizahmed/secret-keystore';

const source: KeystoreSource = {
  type: 'env',
  content: fs.readFileSync('./.env', 'utf-8')
};

const options: KeystoreOptions = {
  paths: ['DB_PASSWORD', 'API_KEY'],
  security: { inMemoryEncryption: true },
  access: { ttl: 3600000, autoRefresh: true }
};

const keyStore: SecretKeyStore = await createSecretKeyStore(
  source,
  kmsKeyId,
  options
);

const password: string | undefined = keyStore.get('DB_PASSWORD');

Troubleshooting

”kms-key-id is REQUIRED”

The --kms-key-id option is required for all CLI operations. Provide your KMS key:

npx @faizahmed/secret-keystore encrypt --kms-key-id="your-kms-key-id"

“Could not load credentials”

The package uses IAM roles by default. If you’re seeing this error:

In production (EC2/ECS/EKS/Lambda):

  • Ensure your instance/task/pod has an IAM role attached
  • Verify the IAM role has kms:Encrypt and kms:Decrypt permissions

In local development:

  • Use --use-credentials flag with the CLI
  • Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables

”AccessDeniedException”

Your AWS credentials don’t have permission to use the KMS key:

{
  "Effect": "Allow",
  "Action": ["kms:Encrypt", "kms:Decrypt", "kms:DescribeKey"],
  "Resource": "arn:aws:kms:REGION:ACCOUNT:key/KEY-ID"
}

Already encrypted values being skipped

This is expected behavior. The library automatically detects and skips values that are already encrypted (prefixed with ENC[) to prevent double-encryption.

Development

This project uses pnpm and the built-in Node test runner — no heavyweight test framework, zero runtime test dependencies.

# Install dev dependencies
pnpm install

# Run the test suite (node:test, fully offline — KMS is mocked)
pnpm test

# Tests with coverage (enforces a minimum threshold)
pnpm test:coverage

# Lint, format check, and type-definition check
pnpm lint
pnpm format:check
pnpm typecheck

Quality gates. Every push and pull request runs CI across Node 18, 20, and 22:

GateToolWhat it checks
Testsnode:test + aws-sdk-client-mock95 tests covering both symmetric and RSA-envelope KMS paths, content/object/keystore/CLI/config()/rotation
Coveragec8Minimum coverage thresholds enforced
LintESLintCode correctness
FormatPrettierConsistent style
Typestsc --strictindex.d.ts compiles and stays in sync with runtime error codes

All tests run without an AWS account or network access — KMS is mocked with a reversible stand-in that exercises both the direct-encrypt (symmetric) and envelope (RSA) code paths.

Security

This package provides multiple layers of protection:

LayerProtection
IAM Role DefaultUses IAM roles by default — no credentials to manage
Encryption at RestSecrets in config files are KMS-encrypted ciphertext
Access ControlIAM policies + KMS key policies control decryption
Runtime IsolationDecrypted values never in process.env
Memory ProtectionAdditional AES-256-GCM encryption in keystore memory
Full AttestationComplete Nitro Enclave attestation lifecycle with auto-refresh
Security-First DependenciesMinimal third-party dependencies for security operations

Attestation Highlights

  • Fully Managed — Library handles ephemeral key pairs, document fetching, and CMS unwrapping
  • Auto-Refresh — Automatically regenerates attestation materials on 5-minute expiry
  • CMS Support — Unwraps KMS CiphertextForRecipient using PKIjs/asn1js
  • Zero Config — Just enable attestation and point to your Anjuna/Nitro endpoint

📖 Read the full Security documentation →

License

MIT — see LICENSE.