Skip to main content

Command Palette

Search for a command to run...

Encrypt Your .env with One Command: The secret-keystore CLI

Create a KMS key, scope a least-privilege IAM policy, then encrypt, decrypt, rotate, edit, and inspect your config — a hands-on tour of every command, copy-paste ready.

Updated
5 min read
Encrypt Your .env with One Command: The secret-keystore CLI
F
Consultant - Lead Software Engineer at Talendy Holdings. Bootstrapping our own Data Centre services. I lead the development and management of innovative software products and frameworks at GeekyAnts, leveraging a wide range of technologies including OpenStack, Postgres, MySQL, GraphQL, Docker, Redis, API Gateway, Dapr, NodeJS, NextJS, and Laravel (PHP). With over 9 years of hands-on experience, I specialize in agile software development, CI/CD implementation, security, scaling, design, architecture, and cloud infrastructure. My expertise extends to Metal as a Service (MaaS), Unattended OS Installation, OpenStack Cloud, Data Centre Automation & Management, and proficiency in utilizing tools like OpenNebula, Firecracker, FirecrackerContainerD, Qemu, and OpenVSwitch. I guide and mentor a team of engineers, ensuring we meet our goals while fostering strong relationships with internal and external stakeholders. I contribute to various open-source projects on GitHub and share industry and technology insights on my blog at blog.faizahmed.in. I hold an Engineer's Degree in Computer Science and Engineering from Raj Kumar Goel Engineering College and have multiple relevant certifications showcased on my LinkedIn skill badges.

Part 2 of 3 on @faizahmed/secret-keystore. Part 1 covered the threat model; this part is pure hands-on. By the end you'll have an encrypted .env and know every command that touches it.

Step 0: a KMS key and a scoped IAM policy

You need one KMS key. A symmetric key is the right default (cheaper, no size limits, fewer moving parts):

aws kms create-key --description "secret-keystore"

aws kms create-alias \
  --alias-name alias/my-app-secrets \
  --target-key-id <key-id-from-previous-command>

Now scope the permissions. Two distinct surfaces:

Encrypting (locally or in CI) needs Encrypt + DescribeKey:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "SecretKeystoreEncrypt",
    "Effect": "Allow",
    "Action": ["kms:Encrypt", "kms:DescribeKey"],
    "Resource": "arn:aws:kms:us-east-1:YOUR_ACCOUNT:key/YOUR_KEY_ID"
  }]
}

Decrypting (your running app) needs only Decrypt + DescribeKey. This is the policy that ships to production — least privilege, one key:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "SecretKeystoreDecrypt",
    "Effect": "Allow",
    "Action": ["kms:Decrypt", "kms:DescribeKey"],
    "Resource": "arn:aws:kms:us-east-1:YOUR_ACCOUNT:key/YOUR_KEY_ID"
  }]
}

DescribeKey is what lets the library auto-detect symmetric vs RSA. Scope by key ARN at minimum; tighten with encryption-context conditions if your deployment supports it.

Step 1: install and scaffold

npm install @faizahmed/secret-keystore

# scaffold a starter .env (refuses to overwrite 
# an existing one)
npx @faizahmed/secret-keystore init

init drops a template like this:

# Reserved keys (never encrypted):
KMS_KEY_ID=alias/my-app-secrets
AWS_REGION=us-east-1

# Your secrets:
DB_PASSWORD=change-me
API_KEY=change-me

KMS_KEY_ID and AWS_REGION stay plaintext — they're configuration, not secrets. Everything else is fair game.

Step 2: encrypt

# Encrypt specific keys
npx @faizahmed/secret-keystore encrypt \
  --kms-key-id="alias/my-app-secrets" \
  --keys="DB_PASSWORD,API_KEY"

# Or all non-reserved keys
npx @faizahmed/secret-keystore encrypt \ 
  --kms-key-id="alias/my-app-secrets"

The file is rewritten in place:

KMS_KEY_ID=alias/my-app-secrets
AWS_REGION=us-east-1
DB_PASSWORD=ENC[AQICAHh2nZPq...]
API_KEY=ENC[AQICAHh2nZPq...]

That ENC[...] wrapper is the marker the library uses to know what to decrypt; everything else is passed through untouched, comments and all. You can now commit this to a private repo — without the KMS key and IAM access, it's noise.

Useful flags:

  • --patterns="**.password,**.secret" — glob-match keys (great for nested JSON/YAML).

  • --exclude="PUBLIC_URL" — skip keys.

  • --output="./.env.enc" — write somewhere else instead of in place.

  • --dry-run — preview which keys would be encrypted.

  • --format=json|yaml — override auto-detection (it reads the file extension by default).

  • --use-credentials — use AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY instead of the ambient IAM role (handy locally).

JSON and YAML work the same way:

npx @faizahmed/secret-keystore encrypt \
  --path="./config.yaml" \
  --kms-key-id="alias/my-app-secrets" \
  --patterns="**.password,**.apiKey"

Step 3: inspect — without leaking anything

Two read-only commands that never print values:

# Just the key names
npx @faizahmed/secret-keystore keys --path="./.env"
# DB_PASSWORD
# API_KEY

# Which keys are encrypted vs still plaintext
npx @faizahmed/secret-keystore status --path="./.env"
#   🔒 encrypted  DB_PASSWORD
#   🔓 plaintext  PUBLIC_URL
# 📊 1 encrypted, 1 plaintext, 2 total

status is the one you run in CI to catch a secret someone forgot to encrypt.

Decrypt (when you actually need the file back)

# In place
npx @faizahmed/secret-keystore decrypt \
  --kms-key-id="alias/my-app-secrets"

# To a separate file
npx @faizahmed/secret-keystore decrypt \
  --path="./.env.enc" --output="./.env" \
  --kms-key-id="alias/my-app-secrets"

For running your app, prefer run or the in-memory loader (Part 3) over decrypting to disk — but decrypt is there when you need the plaintext file.

Edit without ever hand-writing ciphertext

You can't sanely edit ENC[AQICAHh2...] by hand. edit handles the round-trip:

EDITOR=vim npx @faizahmed/secret-keystore edit \
  --kms-key-id="alias/my-app-secrets" --path="./.env"

It decrypts into a 0600-permission temp file, opens your $EDITOR, re-encrypts exactly the keys that were encrypted before, writes back to the original, then shreds the temp file (overwrite + delete). Plaintext touches disk only for the seconds your editor is open, in a restricted file. (Want zero plaintext on disk ever? Skip edit and edit the source before encrypting.)

Rotate keys without re-typing secrets

Key rotation is the command most tools make painful. Here it's one line — decrypt with the old key, re-encrypt with the new one, in a single pass:

npx @faizahmed/secret-keystore rotate \
  --old-kms-key-id="alias/old-key" \
  --kms-key-id="alias/new-key"

It only touches values that were already encrypted; plaintext stays plaintext. Perfect for a scheduled rotation or for the "rotate everything" fire drill after an incident.

Migrating an existing plaintext .env

Already have a plaintext .env from the dotenv days? import encrypts it in place:

npx @faizahmed/secret-keystore import \
  --kms-key-id="alias/my-app-secrets"

(It's encrypt tuned for migration — all non-reserved keys, in place — with friendlier output.)

Run your app with secrets injected

The headline command. Prefix your normal start command and the secrets are decrypted and handed to the child process's environment — no code change:

npx @faizahmed/secret-keystore run \
  --kms-key-id="alias/my-app-secrets" -- node server.js

run reads your .env cascade, decrypts via KMS, and spawns node server.js with the decrypted values in its env. Your app reads process.env.DB_PASSWORD as usual. The parent CLI process never holds them.

One honest caveat: because the child gets the values in its environment, code running inside that child can read them via env. That's unavoidable for any "run a process with secrets" tool. If you want secrets to stay out of process.env entirely, that's exactly what the config() loader in Part 3 is for.

The whole command set

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

encrypt   Encrypt selected values (in place or --output)
decrypt   Decrypt ENC[...] values (in place or --output)
run       Decrypt and launch a command with secrets in the
          child's env
rotate    Re-encrypt under a new key (needs --old-kms-key-id)
edit      Decrypt → $EDITOR → re-encrypt (secure temp file)
init      Scaffold a starter .env
keys      List key names (no values)
status    Show encrypted vs plaintext (no values)
import    Encrypt an existing plaintext .env in place

That's the full CLI. Everything auto-detects .env/JSON/YAML from the extension, and everything takes the same --kms-key-id, --region, and --use-credentials flags.

Next: Part 3 — Loading Secrets Without Leaking Them → — the config() loader, the keystore API, TTL for Lambda, Docker, production rotation, and Nitro attestation.

Mastering Encryption: A Practical Guide for Developers

Part 16 of 17

Learn encryption fundamentals, from Symmetric vs Asymmetric Encryption to Envelope Encryption and AWS KMS implementation. Clear explanations, real-world use cases, and easy-to-follow diagrams to help developers secure their data.

Up next

Loading Secrets at Runtime Without Leaking Them: config(), the Keystore, and run

The dotenv replacement that never touches process.env — plus TTL for Lambda cold starts, Docker, rotating keys in production, and Nitro Enclave attestation for when you need to prove it.