<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Faiz Ahmed Farooqui</title>
    <description>The latest articles on DEV Community by Faiz Ahmed Farooqui (@faizahmedfarooqui).</description>
    <link>https://dev.to/faizahmedfarooqui</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3982774%2F6372f4e0-2c68-4dd6-a56d-658d8dbb2a1c.png</url>
      <title>DEV Community: Faiz Ahmed Farooqui</title>
      <link>https://dev.to/faizahmedfarooqui</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/faizahmedfarooqui"/>
    <language>en</language>
    <item>
      <title>Encrypt your .env with AWS KMS: Secrets that never touch process.env</title>
      <dc:creator>Faiz Ahmed Farooqui</dc:creator>
      <pubDate>Sat, 13 Jun 2026 14:24:01 +0000</pubDate>
      <link>https://dev.to/faizahmedfarooqui/encrypt-your-env-with-aws-kms-secrets-that-never-touch-processenv-180m</link>
      <guid>https://dev.to/faizahmedfarooqui/encrypt-your-env-with-aws-kms-secrets-that-never-touch-processenv-180m</guid>
      <description>&lt;p&gt;A year ago I'd have told you a .env file was fine.&lt;/p&gt;

&lt;p&gt;Then we patched a &lt;strong&gt;CVSS 10.0 RCE&lt;/strong&gt; in Next.js (&lt;a href="https://nextjs.org/blog/CVE-2025-66478" rel="noopener noreferrer"&gt;CVE-2025-66478&lt;/a&gt;) 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.&lt;/p&gt;

&lt;p&gt;That incident is why I built &lt;a href="https://www.npmjs.com/package/@faizahmed/secret-keystore" rel="noopener noreferrer"&gt;&lt;code&gt;@faizahmed/secret-keystore&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual problem isn't committing &lt;code&gt;.env&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Everyone knows not to commit secrets. The part that hurts you is what happens the moment your process is compromised. The default Node setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// every secret → process.env, at startup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attacker gets code execution (a dep RCE, an SSRF, a framework CVE). Their first move:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line. Every DB password, API key, and JWT secret you own, in plaintext, in one place. That's your &lt;strong&gt;blast radius&lt;/strong&gt; — and then you're rotating everything and hoping, because you can't prove what leaked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: a KMS Key ID is not a secret
&lt;/h2&gt;

&lt;p&gt;The whole design rests on one decision: &lt;strong&gt;the only thing a developer ever handles is an AWS KMS Key ID&lt;/strong&gt; — 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.&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;.env&lt;/code&gt; stores ciphertext:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB_PASSWORD=ENC[AQICAHh2nZPq...]
API_KEY=ENC[AQICAHh2nZPq...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At runtime, values are decrypted &lt;strong&gt;on demand into an in-memory store&lt;/strong&gt; — and never put back into &lt;code&gt;process.env&lt;/code&gt;. So the next RCE leaks the handful of keys your code actually touched, not the entire vault.&lt;/p&gt;

&lt;h3&gt;
  
  
  In practice
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @faizahmed/secret-keystore

&lt;span class="c"&gt;# encrypt the secrets in your .env (in place)&lt;/span&gt;
npx @faizahmed/secret-keystore encrypt &lt;span class="nt"&gt;--kms-key-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"alias/my-key"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load them at runtime without ever touching &lt;code&gt;process.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@faizahmed/secret-keystore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secrets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;kmsKeyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alias/my-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dbPassword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// decrypted, in memory only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, for an app you don't want to modify, inject into the child process and run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @faizahmed/secret-keystore run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--kms-key-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"alias/my-key"&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  node server.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also &lt;code&gt;rotate&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, &lt;code&gt;keys&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, and &lt;code&gt;import&lt;/code&gt; — plus optional AWS Nitro Enclave attestation when you need to prove what's running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Being honest about what it does NOT do
&lt;/h2&gt;

&lt;p&gt;It's not magic, and the README says so. An attacker with full code execution &lt;strong&gt;inside&lt;/strong&gt; your process can still call the keystore or scrape memory. You still patch and rotate! &lt;/p&gt;

&lt;p&gt;What it removes is bulk exposure: no single &lt;strong&gt;env&lt;/strong&gt; dump that hands over everything, no plaintext in git, and secret access that's per-key and grep-able.&lt;/p&gt;

&lt;p&gt;It's also &lt;strong&gt;AWS-only by design&lt;/strong&gt;, 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. &lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full write-up
&lt;/h2&gt;

&lt;p&gt;I wrote a 4-part series with the complete threat model, every CLI command, the runtime/&lt;code&gt;config()&lt;/code&gt; internals, and a comparison to dotenvx / SOPS / Secrets Manager:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.faizahmed.in/encrypted-env-aws-kms-nodejs-complete-guide" rel="noopener noreferrer"&gt;The Complete Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.faizahmed.in/nodejs-secrets-threat-model-aws-kms" rel="noopener noreferrer"&gt;Part 1 — Your .env Is a Loaded Gun (threat model)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.faizahmed.in/secret-keystore-cli-encrypt-env-aws-kms" rel="noopener noreferrer"&gt;Part 2 — The CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.faizahmed.in/secret-keystore-runtime-config-loader-nodejs" rel="noopener noreferrer"&gt;Part 3 — Runtime, rotation &amp;amp; attestation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repo (with runnable Next.js + NestJS examples): &lt;a href="https://github.com/faizahmedfarooqui/secret-keystore" rel="noopener noreferrer"&gt;github.com/faizahmedfarooqui/secret-keystore&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>backend</category>
      <category>security</category>
    </item>
  </channel>
</rss>
