The Goal: A Secret Should Never Touch Source Control

Most credential breaches come from the boring sources: a .env in a public repo, an API key in a Slack message, a JSON service-account file copied to a laptop. Secrets management is the discipline of keeping credentials in one place, rotating them automatically, and giving each consumer the narrowest possible access for the shortest possible time.

πŸ€” Sound familiar?
  • git log -p in your repo would surface at least three live API keys
  • The same DB password has been in use since 2021
  • Service accounts have Owner on subscriptions β€œjust in case”
  • When someone leaves, their PAT lives on for months

The 2026 baseline: a managed vault as the source of truth, workload identity instead of static keys, automated rotation, and pre-commit + CI secret scanning that fails the build.

The Hierarchy of Secret Quality


flowchart TB
    A[Workload Identity / mTLS<br/>no secret at all] --> B[Short-lived tokens from STS]
    B --> C[Vault-issued dynamic secrets]
    C --> D[Vault-stored static secrets, auto-rotated]
    D --> E[Static secrets in env vars, manually rotated]
    E --> F[Plaintext in .env in repo<br/>(don't)]
    classDef bad fill:#fee
    class F bad
      

Climb as high as you can. Cloud-native services let you skip secrets entirely via managed identity, IRSA, or workload identity federation β€” always the first choice when available.

The Vault Pattern

HashiCorp Vault, Azure Key Vault, AWS Secrets Manager, GCP Secret Manager,Doppler, 1Password Secrets Automationβ€” pick one, standardize on it. Apps don't store secrets; they authenticate (using workload identity) and fetch on startup or on demand.

// Azure Key Vault from a Node.js app, no client secret in env
import { DefaultAzureCredential } from '@azure/identity';
import { SecretClient } from '@azure/keyvault-secrets';

const credential = new DefaultAzureCredential(); // managed identity in prod, az login locally
const client = new SecretClient(`https://${vaultName}.vault.azure.net`, credential);

const dbConn = (await client.getSecret('db-connection-string')).value!;

// AWS equivalent:
//   const sm = new SecretsManagerClient({});
//   const out = await sm.send(new GetSecretValueCommand({ SecretId: 'prod/db' }));
//
// GCP equivalent:
//   const [version] = await client.accessSecretVersion({ name: 'projects/.../secrets/db/versions/latest' });

Dynamic Secrets β€” The Step Beyond Storage

Vault, AWS Secrets Manager (with the Lambda rotation pattern), and GCP can issue brand new credentials on demand, scoped to the requester, with a short TTL. A microservice asks Vault for a Postgres credential; Vault creates a one-hour DB user, returns the creds, the app uses them, the user expires. No long-lived secret ever existed.

Rotation

Static secrets must rotate on a schedule. Automate it; never make rotation a human task. Most vault platforms have built-in rotators for common backends (Postgres, MySQL, S3 keys, service principals). The rule of thumb:

Secret typeRotation interval
Database passwords30 days (or dynamic)
API keys to internal services90 days
Cloud service principals90 days, or replace with workload identity
Signing keys (JWT, code signing)1 year, overlapping with kid
Encryption keys (DEKs)Yearly + on suspected compromise

Envelope Encryption

Don't encrypt large data with a vault-managed key directly β€” too slow, too many calls. Instead, generate a Data Encryption Key (DEK) per object, encrypt the data with the DEK, encrypt the DEK with a Key Encryption Key (KEK) held in the vault, and store the encrypted DEK next to the ciphertext. This is how AWS KMS, Azure Key Vault, and GCP KMS expect you to use them.

// Envelope encryption with AWS KMS
import { KMSClient, GenerateDataKeyCommand, DecryptCommand } from '@aws-sdk/client-kms';
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';

const kms = new KMSClient({});

async function encrypt(plaintext: Buffer) {
  const { Plaintext, CiphertextBlob } = await kms.send(new GenerateDataKeyCommand({
    KeyId: KMS_KEY_ARN, KeySpec: 'AES_256',
  }));
  const iv = randomBytes(12);
  const cipher = createCipheriv('aes-256-gcm', Buffer.from(Plaintext!), iv);
  const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
  return { ct, iv, tag: cipher.getAuthTag(), wrappedKey: Buffer.from(CiphertextBlob!) };
}

Secret Scanning β€” Catch It Before It Lands

Three layers:

  1. Pre-commit: gitleaks, detect-secrets as Husky/pre-commit hooks. Stops most accidents on the developer's machine.
  2. CI: same scanners on every PR, fail the build on a finding.
  3. Provider-side: GitHub Secret Scanning + Push Protection, GitLab Secret Detection. Catches what slipped through.

What to Do When a Secret Leaks

The clock starts the moment it's committed (even to a private repo β€” assume compromised). The response order is non-negotiable:

  1. Rotate the credential immediately. Don't wait to investigate first.
  2. Revoke all sessions and tokens issued with it.
  3. Review logs for use of the credential from unexpected sources.
  4. Purge history if possible (BFG, git-filter-repo). It's already public β€” treat this as evidence cleanup, not mitigation.
  5. Postmortem: how did it get there, and why didn't the pre-commit hook catch it?

Local Development Without Real Secrets

Devs should never have prod credentials on a laptop. Patterns that work:

  • Per-developer dev environments with their own dev-tier services and dev-tier secrets.
  • Vault dynamic secrets scoped to a developer's identity, expiring at end of day.
  • Doppler-style CLI wrappers (doppler run -- npm start) that inject env vars at runtime, never write them to disk.

Pitfalls

One vault, many apps, no scoping

If every app can read every secret, the vault is just a slightly fancier .env. Scope access per workload identity to the exact secrets it needs. Audit access logs.

Rotation that breaks at the worst time

A rotation that requires a manual restart will be deferred forever. Apps must fetch secrets on demand (with a short cache) so that a rotation is picked up automatically β€” or be designed for zero-downtime credential swap.

Secret sprawl across CI providers

GitHub Actions secrets + Vercel envs + Doppler + a vault is four sources of truth. Pick the vault as canonical and sync (one-way) to CI providers via OIDC federation or a small sync job. Auditors will ask, and you should be able to answer in one place.