mdashikjs/blog
All posts
Secrets Management in Production: HashiCorp Vault and Beyond
Security

Secrets Management in Production: HashiCorp Vault and Beyond

Security5 min

Secrets Management in Production: HashiCorp Vault and Beyond

Hardcoded secrets in .env files committed to Git is how breaches happen. Here is how we moved to centralized secrets management with HashiCorp Vault and never looked back.

SecurityVaultSecrets ManagementDevOps
Share:

The problem with .env files

  1. They get committed. Someone forgets to .gitignore the production .env file. Now your database password is in Git history forever.
  2. They spread. Developers copy .env files over Slack. Each copy is a potential leak.
  3. No rotation. Changing a secret means updating every server and every developer's local file.
  4. No audit trail. Who accessed which secret? When? You have no idea.

HashiCorp Vault: the centralized answer

Vault stores secrets centrally, provides access control, generates dynamic credentials, and logs every access.

Setting up the KV secrets engine

# Enable the key-value secrets engine
vault secrets enable -version=2 kv

# Store a secret
vault kv put kv/production/api \
  DATABASE_URL="postgresql://prod:s3cret@db.example.com:5432/app" \
  STRIPE_KEY="sk_live_abc123" \
  JWT_SECRET="super-secret-key"

# Read it
vault kv get kv/production/api

Access policies

Policies control who can read which secrets:

# policies/api-production.hcl
path "kv/data/production/api" {
  capabilities = ["read"]
}

path "kv/data/production/api" {
  capabilities = ["read", "update"]
  # Only CI/CD can update
  required_parameters = ["role"]
}
vault policy write api-production policies/api-production.hcl

Fetching secrets at startup

import Vault from 'node-vault';

const vault = Vault({
  apiVersion: 'v1',
  endpoint: process.env.VAULT_ADDR,
  token: process.env.VAULT_TOKEN,
});

async function loadSecrets(): Promise<Record<string, string>> {
  const result = await vault.read('kv/data/production/api');
  return result.data.data;
}

// At startup
const secrets = await loadSecrets();
const db = new Pool({ connectionString: secrets.DATABASE_URL });

Dynamic database credentials

Vault's killer feature: it can generate short-lived database credentials on the fly:

# Configure the database secrets engine
vault secrets enable database
vault write database/config/mydb \
  plugin_name=postgresql-database-plugin \
  connection_url="postgresql://{{username}}:{{password}}@db:5432/app" \
  allowed_roles="api-readonly" \
  username="vault_admin" \
  password="vault_admin_pass"

vault write database/roles/api-readonly \
  db_name=mydb \
  creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}';" \
  default_ttl=1h \
  max_ttl=24h

Now every time your app starts, it gets a unique username and password that expires in 1 hour. If credentials are leaked, the blast radius is one hour, not forever.

Simpler alternatives

Vault is powerful but operationally heavy. For smaller teams:

AWS Secrets Manager

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

const client = new SecretsManagerClient({ region: 'ap-south-1' });

async function getSecret(name: string) {
  const command = new GetSecretValueCommand({ SecretId: name });
  const response = await client.send(command);
  return JSON.parse(response.SecretString!);
}

Integrates natively with ECS, Lambda, and other AWS services. Supports automatic rotation.

Doppler

For teams that want secrets management without running infrastructure:

# Install and configure
doppler setup

# Run your app with injected secrets
doppler run -- node dist/main.js

Doppler injects secrets as environment variables. No code changes. Supports environments, access control, and audit logs.

The minimum viable secrets policy

  1. Never commit secrets to Git. Use .gitignore and pre-commit hooks that scan for patterns like sk_live_, password=, etc.
  2. Rotate secrets quarterly. At minimum. Ideally, use dynamic credentials.
  3. Use different secrets per environment. Staging and production must have different database passwords.
  4. Audit access. Know who read which secret and when.
  5. Encrypt at rest. Wherever secrets are stored, they should be encrypted.
MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.