Safeguard
Security Guides

Secrets Management in Node.js: From .env to Zero-Standing-Credentials

Hardcoded tokens and committed .env files are still the fastest way to lose a cloud account. Here is a maturity ladder for Node.js secrets — from native --env-file to managed vaults and short-lived OIDC credentials.

Priya Mehta
Security Researcher
5 min read

The most common way companies lose control of their cloud environment isn't an exotic exploit — it's a credential that was never supposed to be public. A long-lived API key hardcoded in a source file, a .env committed to git two years ago, an NPM_TOKEN sitting in plaintext in a CI settings page. Node.js has picked up native tooling for handling secrets, and the surrounding ecosystem has matured toward short-lived, injected credentials. This guide is a maturity ladder: find where you are, then climb one rung.

Rung 0: The anti-patterns to eliminate first

Before improving anything, remove the worst offenders:

// NEVER: hardcoded secret in source
const stripe = new Stripe("sk_live_51H...actualkey");

// NEVER: secret checked into a committed file
// config/production.json → { "dbPassword": "hunter2" }

Hardcoded secrets are the highest-severity finding a scanner can produce, because they're valid the moment the repo is cloned, forked, or leaked — and git remembers them forever even after you "delete" them in a later commit.

Rung 1: Environment variables, kept out of git

The baseline is to read secrets from the environment and never commit the file that holds them. Node 20.6+ reads .env files natively — no dotenv dependency required:

node --env-file=.env server.js
const dbUrl = process.env.DATABASE_URL;
if (!dbUrl) throw new Error("DATABASE_URL is required"); // fail fast, don't guess

The rules that make this safe:

  • Add .env (and .env.*) to .gitignore before the first commit.
  • Commit a .env.example with keys but no values, so onboarding is easy without leaking anything.
  • Validate presence at startup and crash loudly if a secret is missing — a silent undefined becomes a bug or an insecure default.

This is the floor, not the ceiling. Environment variables are readable by anything in the process, appear in crash dumps, and are easy to accidentally log.

Rung 2: A managed secret store

Move secrets out of static files entirely and into a purpose-built store — AWS Secrets Manager, Google Secret Manager, Azure Key Vault, or HashiCorp Vault. Your app fetches them at startup using the platform identity it already has (an IAM role, a workload identity), so there's no bootstrap secret to protect.

import { SecretsManagerClient, GetSecretValueCommand }
  from "@aws-sdk/client-secrets-manager";

const client = new SecretsManagerClient({});
async function loadSecret(id) {
  const res = await client.send(new GetSecretValueCommand({ SecretId: id }));
  return JSON.parse(res.SecretString);
}
const db = await loadSecret("prod/db"); // authenticated by the instance's IAM role

Now access is audited, secrets can be rotated centrally without a redeploy, and there's no plaintext file to leak.

Rung 3: Runtime injection and least privilege

Prefer injecting secrets at deploy time (mounted as a tmpfs file or fetched at boot) over baking them into an image or a build. Scope each credential to exactly what it needs: a service that only reads from one S3 bucket gets a policy for that bucket, not s3:*. Combine this with Node's --permission model so even a compromised dependency can't read secret files it wasn't granted.

Rung 4: Zero standing credentials

The top rung eliminates long-lived secrets where you can. Two big wins in 2026:

  • OIDC-based cloud access: CI pipelines exchange a short-lived, workflow-scoped OIDC token for temporary cloud credentials instead of storing a static access key. GitHub Actions, GitLab, and the major clouds all support this now.
  • Trusted publishing to npm: publish packages via OIDC so there's no long-lived NPM_TOKEN to steal — closing the exact vector behind several 2025 npm package compromises.

Credentials that expire in minutes can't be exfiltrated and reused weeks later.

The maturity ladder at a glance

RungApproachKey risk it removes
0Remove hardcoded secretsInstant compromise on repo access
1--env-file, gitignoredCommitted secrets
2Managed secret storePlaintext files, no rotation
3Runtime injection + least privilegeBroad blast radius
4OIDC / short-lived credentialsLong-lived token theft

Don't forget the secrets already leaked

Climbing the ladder protects new secrets. You still have to find the ones already sitting in your git history, your logs, and your CI configuration. A secret that was committed in 2023 and "removed" in 2024 is still in the history and must be rotated, not just deleted — deletion doesn't invalidate a live credential.

A pre-commit hook that blocks secret-shaped strings, plus a history scan, catches both the next leak and the last one.

How Safeguard helps

Secrets hygiene needs continuous detection, not a one-time cleanup. Safeguard scans your repositories and build artifacts for exposed credentials — API keys, tokens, private keys, connection strings — across current code and history, and flags them with the context to prioritize rotation. The Safeguard CLI runs the same detection pre-commit and in CI so a secret fails the build before it ever reaches a shared branch, and Griffin AI helps triage which exposures are live and high-impact. Because secrets often ride along in container layers, secure container scanning inspects image layers for baked-in credentials too. See how the platform fits your workflow on the comparison hub.

Get started

Wherever you are on the ladder, the next rung is a concrete, achievable step — and detecting what's already leaked is the fastest risk reduction available. Create a free account at app.safeguard.sh/register and read the secret-scanning setup guide at docs.safeguard.sh.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.